Last active
January 15, 2026 22:08
-
-
Save acamino/1736ed935bb97d9404fac06690855026 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| openapi: 3.1.0 | |
| info: | |
| title: Shopify Bridge Batch Ingestion API | |
| version: 0.1.1 | |
| description: | | |
| REST API for batch ingestion of e-commerce products, POS products, inventory, and price, promotion codes, shipping promotions and associated data (ex: flags) into the Follett Shopify Bridge system. | |
| This API provides an alternative to file-based ingestion, enabling programmatic data submission | |
| with per-entry error tracking and job status monitoring. | |
| > **Note:** To enable the Download button, uncheck the **CORS** checkbox in the top-right corner. | |
| ## Key Features | |
| - **Batch Processing**: Submit up to 10,000 entries per request | |
| - **Per-Entry Tracking**: Client-provided identifiers for precise error correlation | |
| - **Flexible Processing**: Synchronous for small batches, asynchronous for large ones | |
| - **Idempotent Operations**: Optional idempotency keys for safe retries | |
| ## Processing Modes | |
| | Batch Size | Mode | Response | | |
| |------------|------|----------| | |
| | ≤ 100 entries | Synchronous | `200`/`207` with full results | | |
| | > 100 entries | Asynchronous | `202` with job_id for polling | | |
| ## Rate Limits | |
| | Limit | Value | | |
| |-------|-------| | |
| | Requests per minute | 60 | | |
| | Entries per minute | 10,000 | | |
| | Payload size | 10 MB | | |
| | Entries per request | 10,000 | | |
| contact: | |
| name: API Support | |
| email: support@example.com | |
| license: | |
| name: Proprietary | |
| identifier: LicenseRef-Proprietary | |
| servers: | |
| - url: https://api.example.com | |
| description: Production | |
| - url: https://staging-api.example.com | |
| description: Staging | |
| - url: http://localhost:8080 | |
| description: Local Development | |
| tags: | |
| - name: Products | |
| description: Product and variant ingestion | |
| - name: POS | |
| description: Point of Sale product updates | |
| - name: Inventory | |
| description: Inventory level management | |
| - name: Prices | |
| description: Price updates and promotions | |
| - name: Jobs | |
| description: Asynchronous job monitoring | |
| security: | |
| - BearerAuth: [] | |
| - ApiKeyAuth: [] | |
| paths: | |
| /api/v1/ingest/products: | |
| post: | |
| operationId: ingestProducts | |
| summary: Ingest products with variants | |
| description: | | |
| Submit product data with one or more variants per entry. | |
| Each entry represents a complete product with its variants. SKUs are automatically | |
| padded to 9 digits for numeric values. | |
| ## Processing Rules | |
| | Rule | Details | | |
| |------|---------| | |
| | Product matching | By handle (auto-generated from title if missing) | | |
| | Variant matching | By SKU within product (9-digit padding for numeric SKUs) | | |
| | Multi-variant support | Up to 3 option dimensions (option1, option2, option3) | | |
| | Handle format | Lowercase alphanumeric with hyphens only | | |
| | Initial load validation | `vertex.product_class` column required for initial load files | | |
| | Metafield format | Descriptor format: `metafield.namespace.key.type` | | |
| | Boolean normalization | Y/N, true/false → "True"/"False" | | |
| | Taxonomy padding | Zero-padded to 3 digits (dept, sub_department, class, sub_class) | | |
| | Store scoping | Products are store-scoped (not location-scoped) | | |
| ## Safeguards | |
| | Safeguard | Description | | |
| |-----------|-------------| | |
| | Optimistic concurrency | Version field prevents conflicting updates | | |
| | Duplicate detection | Prevents re-creation of existing products | | |
| | Batch processing | 100 records per batch with transactional boundaries | | |
| | Change detection | Skips unchanged products (compares title, data, metafields) | | |
| | Sync preservation | ShopifyProductID, SyncStatus, SyncAttempts preserved during updates | | |
| | Product options | Auto-populated from variant values post-processing | | |
| | Outbox pattern | Ensures reliable Shopify sync with retry capability | | |
| ## Limitations | |
| | Limit | Value | | |
| |-------|-------| | |
| | Max entries per request | 10,000 | | |
| | Max payload size | 10 MB | | |
| | Batch size (file processing) | 100 | | |
| | SKU uniqueness | Per product+store (not globally) | | |
| | Handle uniqueness | Per store | | |
| | Initial load requirement | `vertex.product_class` column required | | |
| tags: | |
| - Products | |
| requestBody: | |
| required: true | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ProductIngestRequest' | |
| examples: | |
| singleProduct: | |
| summary: Single product with variants | |
| value: | |
| idempotency_key: "import-2025-01-06-001" | |
| entries: | |
| - entry_id: "prod-001" | |
| data: | |
| handle: "classic-blue-shirt" | |
| title: "Classic Blue Shirt" | |
| description: "<p>A timeless classic.</p>" | |
| vendor: "Acme Clothing" | |
| product_type: "Shirts" | |
| tags: "clothing,shirts,blue" | |
| published: true | |
| variants: | |
| - sku: "CBS-S-BLU" | |
| price: 49.99 | |
| compare_at_price: 59.99 | |
| barcode: "123456789012" | |
| option1_name: "Size" | |
| option1_value: "Small" | |
| option2_name: "Color" | |
| option2_value: "Blue" | |
| - sku: "CBS-M-BLU" | |
| price: 49.99 | |
| option1_name: "Size" | |
| option1_value: "Medium" | |
| option2_name: "Color" | |
| option2_value: "Blue" | |
| minimalProduct: | |
| summary: Minimal product (required fields only) | |
| value: | |
| entries: | |
| - entry_id: "prod-minimal" | |
| data: | |
| variants: | |
| - sku: "SIMPLE-001" | |
| price: 19.99 | |
| responses: | |
| '200': | |
| description: All entries processed successfully | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/SyncIngestResponse' | |
| '202': | |
| description: Batch accepted for asynchronous processing | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/AsyncIngestResponse' | |
| '207': | |
| description: Partial success - some entries failed | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/SyncIngestResponse' | |
| '400': | |
| $ref: '#/components/responses/BadRequest' | |
| '401': | |
| $ref: '#/components/responses/Unauthorized' | |
| '413': | |
| $ref: '#/components/responses/PayloadTooLarge' | |
| '422': | |
| $ref: '#/components/responses/UnprocessableEntity' | |
| '429': | |
| $ref: '#/components/responses/RateLimitExceeded' | |
| '500': | |
| $ref: '#/components/responses/InternalServerError' | |
| /api/v1/ingest/pos: | |
| post: | |
| operationId: ingestPOS | |
| summary: Ingest POS product updates | |
| description: | | |
| Submit Point of Sale product updates. Each entry creates or updates | |
| one product with a single variant. | |
| ## Processing Rules | |
| | Rule | Details | | |
| |------|---------| | |
| | Product-variant mapping | One product = one variant (1:1 enforced by handle generation) | | |
| | Handle format | `{normalized-title}-{store_number}-{formatted_sku}` | | |
| | Status handling | Status field ignored (always treated as ACTIVE in Shopify) | | |
| | Store number default | "9975" if missing | | |
| | Vendor default | "Unknown" if empty | | |
| | Product type | Always "POS" | | |
| | Tax code storage | Both `variants.tax_code` AND `vertex.product_class` metafield | | |
| | Barcode formatting | Removes `.0` suffix, validates digits only | | |
| | POSOnly flag | Always set to `true` | | |
| ## Default Values | |
| | Field | Default | Condition | | |
| |-------|---------|-----------| | |
| | Store Number | "9975" | If empty | | |
| | Title | "POS Product {row}" | If empty | | |
| | Vendor | "Unknown" | If empty | | |
| | Product Type | "POS" | Always | | |
| | Price | 0.0 | If empty or parse error | | |
| ## Safeguards | |
| | Safeguard | Description | | |
| |-----------|-------------| | |
| | Store validation | Ensures only active stores processed | | |
| | Duplicate SKU handling | Unique handle generation includes store+SKU | | |
| | Price change tracking | Price history stored in JSONB | | |
| | Change detection | Product-level and variant-level comparison | | |
| | Metafield type overrides | From Shopify definitions cache | | |
| | Atomic updates | Variant changes trigger product sync | | |
| ## Limitations | |
| | Limitation | Details | | |
| |------------|---------| | |
| | No multi-variant | POS design constraint (1 product = 1 variant) | | |
| | Store validation | Store number must exist in database | | |
| | Barcode format | Must be numeric (invalid values → empty string) | | |
| | No images | Files array always empty for POS products | | |
| | No tags | Tags array always empty for POS products | | |
| tags: | |
| - POS | |
| requestBody: | |
| required: true | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/POSIngestRequest' | |
| examples: | |
| posUpdate: | |
| summary: POS product update | |
| value: | |
| entries: | |
| - entry_id: "pos-001" | |
| data: | |
| sku: "12345" | |
| store_id: "STORE-101" | |
| title: "Seasonal Item" | |
| vendor: "Local Vendor" | |
| price: 24.99 | |
| barcode: "987654321098" | |
| variant_tax_code: "P0000000" | |
| responses: | |
| '200': | |
| description: All entries processed successfully | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/SyncIngestResponse' | |
| '202': | |
| description: Batch accepted for asynchronous processing | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/AsyncIngestResponse' | |
| '207': | |
| description: Partial success - some entries failed | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/SyncIngestResponse' | |
| '400': | |
| $ref: '#/components/responses/BadRequest' | |
| '401': | |
| $ref: '#/components/responses/Unauthorized' | |
| '413': | |
| $ref: '#/components/responses/PayloadTooLarge' | |
| '422': | |
| $ref: '#/components/responses/UnprocessableEntity' | |
| '429': | |
| $ref: '#/components/responses/RateLimitExceeded' | |
| '500': | |
| $ref: '#/components/responses/InternalServerError' | |
| /api/v1/ingest/inventory: | |
| post: | |
| operationId: ingestInventory | |
| summary: Ingest inventory level updates | |
| description: | | |
| Submit inventory level updates for existing variants. | |
| **Processing Modes** (via `options.mode`): | |
| | Mode | Behavior | | |
| |------|----------| | |
| | `initial` | Sets absolute quantity values | | |
| | `delta` | Adjusts quantities by the specified amounts (default) | | |
| --- | |
| ## Initial Inventory Load (`mode: initial`) | |
| ### Processing Rules | |
| | Rule | Details | | |
| |------|---------| | |
| | on_hand mutation | `inventorySetQuantities` (absolute SET) | | |
| | on_order mutation | `inventoryAdjustQuantities` (delta ADJUST) | | |
| | Quantity mapping | on_hand → "available", on_order → "incoming" | | |
| | Location resolution | Uses store_number + location_name | | |
| | Location default | If location_name empty/"0": uses location where name = store_number | | |
| | child_store routing | Defaults to location_name (or store_number if empty) | | |
| | Duplicate SKU support | Updates ALL matching variants | | |
| ### Safeguards (Initial) | |
| | Safeguard | Description | | |
| |-----------|-------------| | |
| | Location validation | Location must be active | | |
| | Store validation | Store must be active | | |
| | Transaction wrapping | Outbox + audit events atomic | | |
| | Placeholder handling | Location/inventory item ID placeholders if Shopify IDs missing | | |
| ### Limitations (Initial) | |
| | Limitation | Details | | |
| |------------|---------| | |
| | Location requirement | Location must exist in database | | |
| | Location status | Location must be active | | |
| | No dynamic creation | Cannot create locations dynamically | | |
| | Zero handling | on_hand defaults to 0 if empty (not skipped) | | |
| | Quantity parsing | Float truncated to int (e.g., 105.99 → 105) | | |
| --- | |
| ## Delta Inventory (`mode: delta`) | |
| ### Processing Rules | |
| | Rule | Details | | |
| |------|---------| | |
| | Mutation | `inventoryAdjustQuantities` (delta ADJUST) for both types | | |
| | Delta type: ADJUSTMENT | Uses quantity_name "available" | | |
| | Delta type: ON_ORDER | Uses quantity_name "incoming" | | |
| | Quantity direction | Positive (increase) or negative (decrease) | | |
| | No CAS checks | Trusts file delta values directly | | |
| | No local tracking | No local quantity state maintained | | |
| ### Delta Type Mapping | |
| | File Value | Normalized | Shopify quantity_name | | |
| |------------|------------|----------------------| | |
| | "Adjustment" | ADJUSTMENT | available | | |
| | "On-Order" / "ON_ORDER" | ON_ORDER | incoming | | |
| ### 3-Tier Variant Lookup (Delta) | |
| | Tier | Action | | |
| |------|--------| | |
| | Tier 1 | Local database lookup by SKU+store+child_store | | |
| | Tier 2 | Sync missing variants from Shopify if product exists locally | | |
| | Tier 3 | Full Shopify product fetch + local creation | | |
| ### Safeguards (Delta) | |
| | Safeguard | Description | | |
| |-----------|-------------| | |
| | Delta type validation | Rejects invalid types | | |
| | Location validation | Location must be active | | |
| | Store validation | Store must be active | | |
| | Transaction isolation | Per-update atomicity | | |
| ### Limitations (Delta) | |
| | Limitation | Details | | |
| |------------|---------| | |
| | Delta type values | Must be "On-Order" or "Adjustment" (case-insensitive) | | |
| | No drift detection | No local inventory state tracking | | |
| | File accuracy | Relies entirely on file delta accuracy | | |
| | No validation | Cannot validate final quantity result | | |
| | Location status | Location must exist and be active | | |
| tags: | |
| - Inventory | |
| requestBody: | |
| required: true | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/InventoryIngestRequest' | |
| examples: | |
| deltaUpdate: | |
| summary: Delta inventory adjustment | |
| value: | |
| options: | |
| mode: "delta" | |
| entries: | |
| - entry_id: "inv-001" | |
| data: | |
| sku: "CBS-S-BLU" | |
| store: "STORE-101" | |
| on_hand: 5 | |
| - entry_id: "inv-002" | |
| data: | |
| sku: "CBS-M-BLU" | |
| store: "STORE-101" | |
| on_hand: -2 | |
| initialSet: | |
| summary: Set absolute inventory levels | |
| value: | |
| options: | |
| mode: "initial" | |
| entries: | |
| - entry_id: "inv-003" | |
| data: | |
| sku: "CBS-S-BLU" | |
| store: "STORE-101" | |
| location_name: "Main Warehouse" | |
| on_hand: 100 | |
| on_order: 50 | |
| responses: | |
| '200': | |
| description: All entries processed successfully | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/SyncIngestResponse' | |
| '202': | |
| description: Batch accepted for asynchronous processing | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/AsyncIngestResponse' | |
| '207': | |
| description: Partial success - some entries failed | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/SyncIngestResponse' | |
| '400': | |
| $ref: '#/components/responses/BadRequest' | |
| '401': | |
| $ref: '#/components/responses/Unauthorized' | |
| '413': | |
| $ref: '#/components/responses/PayloadTooLarge' | |
| '422': | |
| $ref: '#/components/responses/UnprocessableEntity' | |
| '429': | |
| $ref: '#/components/responses/RateLimitExceeded' | |
| '500': | |
| $ref: '#/components/responses/InternalServerError' | |
| /api/v1/ingest/prices: | |
| post: | |
| operationId: ingestPrices | |
| summary: Ingest price updates | |
| description: | | |
| Submit price updates for existing variants. | |
| ## Processing Rules | |
| | Rule | Details | | |
| |------|---------| | |
| | Price types | R (Regular), S (Sale), C (Clearance), CS (Clearance Sale) | | |
| | Type R | Sets price, clears compare_at_price | | |
| | Type S/C | Sets price, preserves/sets compare_at_price from old price | | |
| | Type CS | Combines Sale + Clearance flags | | |
| | Change tolerance | $0.01 (float precision protection) | | |
| | Metafield flags | `custom.sale_flag`, `custom.clearance_flag` auto-set based on type | | |
| | child_store routing | Defaults to parent store if empty or "0" | | |
| | Duplicate SKU support | Updates ALL matching variants | | |
| ## Price Type Behavior | |
| | Type | Price | compare_at_price | sale_flag | clearance_flag | | |
| |------|-------|------------------|-----------|----------------| | |
| | R (Regular) | new_price | cleared (nil) | false | false | | |
| | S (Sale) | new_price | old_price (if transitioning from R) | true | false | | |
| | C (Clearance) | new_price | old_price (if transitioning from R) | false | true | | |
| | CS (Clearance Sale) | new_price | old_price (if transitioning from R) | true | true | | |
| ## 2-Tier Variant Lookup | |
| | Tier | Action | | |
| |------|--------| | |
| | Tier 1 | Local database lookup by SKU+store+child_store (includes duplicate SKU resolution via child_store metafield) | | |
| | Tier 2 | Shopify API fallback (fetches product, creates locally, returns variants) | | |
| ## Safeguards | |
| | Safeguard | Description | | |
| |-----------|-------------| | |
| | Price tolerance | $0.01 (skips sub-penny changes) | | |
| | Skip unchanged | Skips update if price AND price_type unchanged | | |
| | Store validation | Validates store exists and is active | | |
| | Transaction isolation | Each SKU update in separate transaction | | |
| | Price history | Tracked in `variant.Data["price_history"]` JSONB array | | |
| | Max sync attempts | 3 attempts via outbox pattern | | |
| ## Limitations | |
| | Limitation | Details | | |
| |------------|---------| | |
| | Variant requirement | Variants must exist (creates via Tier 2 if needed) | | |
| | Negative prices | Rejected during validation | | |
| | Store status | Store must be active | | |
| | Price type default | Defaults to "R" if invalid | | |
| | Max sync attempts | 3 via outbox pattern | | |
| tags: | |
| - Prices | |
| requestBody: | |
| required: true | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/PriceIngestRequest' | |
| examples: | |
| regularPrice: | |
| summary: Regular price update | |
| value: | |
| entries: | |
| - entry_id: "price-001" | |
| data: | |
| sku: "CBS-S-BLU" | |
| store_id: "STORE-101" | |
| price: 44.99 | |
| price_type: "R" | |
| salePrice: | |
| summary: Sale price with compare_at | |
| value: | |
| entries: | |
| - entry_id: "price-002" | |
| data: | |
| sku: "CBS-S-BLU" | |
| store_id: "STORE-101" | |
| price: 34.99 | |
| price_type: "S" | |
| responses: | |
| '200': | |
| description: All entries processed successfully | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/SyncIngestResponse' | |
| '202': | |
| description: Batch accepted for asynchronous processing | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/AsyncIngestResponse' | |
| '207': | |
| description: Partial success - some entries failed | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/SyncIngestResponse' | |
| '400': | |
| $ref: '#/components/responses/BadRequest' | |
| '401': | |
| $ref: '#/components/responses/Unauthorized' | |
| '413': | |
| $ref: '#/components/responses/PayloadTooLarge' | |
| '422': | |
| $ref: '#/components/responses/UnprocessableEntity' | |
| '429': | |
| $ref: '#/components/responses/RateLimitExceeded' | |
| '500': | |
| $ref: '#/components/responses/InternalServerError' | |
| /api/v1/jobs/{job_id}: | |
| get: | |
| operationId: getJobStatus | |
| summary: Get job status and summary | |
| description: | | |
| Retrieve the current status and summary statistics for an asynchronous job. | |
| Poll this endpoint to track progress of large batch submissions. | |
| Once status is `completed`, `completed_with_errors`, or `failed`, | |
| use the results endpoint to retrieve detailed per-entry outcomes. | |
| tags: | |
| - Jobs | |
| parameters: | |
| - $ref: '#/components/parameters/JobId' | |
| responses: | |
| '200': | |
| description: Job status retrieved successfully | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/JobStatus' | |
| examples: | |
| processing: | |
| summary: Job in progress | |
| value: | |
| job_id: "550e8400-e29b-41d4-a716-446655440000" | |
| status: "processing" | |
| created_at: "2025-01-06T10:30:00Z" | |
| updated_at: "2025-01-06T10:30:45Z" | |
| progress_percent: 65 | |
| summary: | |
| total: 1000 | |
| processed: 650 | |
| created: 400 | |
| updated: 250 | |
| errors: 0 | |
| completed: | |
| summary: Job completed with errors | |
| value: | |
| job_id: "550e8400-e29b-41d4-a716-446655440000" | |
| status: "completed_with_errors" | |
| created_at: "2025-01-06T10:30:00Z" | |
| updated_at: "2025-01-06T10:32:15Z" | |
| completed_at: "2025-01-06T10:32:15Z" | |
| progress_percent: 100 | |
| summary: | |
| total: 1000 | |
| processed: 985 | |
| created: 600 | |
| updated: 385 | |
| errors: 15 | |
| '401': | |
| $ref: '#/components/responses/Unauthorized' | |
| '404': | |
| $ref: '#/components/responses/NotFound' | |
| '500': | |
| $ref: '#/components/responses/InternalServerError' | |
| /api/v1/jobs/{job_id}/results: | |
| get: | |
| operationId: getJobResults | |
| summary: Get paginated job results | |
| description: | | |
| Retrieve detailed per-entry results for a job. | |
| Results are returned in the same order as the original request entries. | |
| Use pagination parameters to retrieve large result sets efficiently. | |
| tags: | |
| - Jobs | |
| parameters: | |
| - $ref: '#/components/parameters/JobId' | |
| - name: status | |
| in: query | |
| description: Filter results by status | |
| schema: | |
| type: string | |
| enum: | |
| - success | |
| - error | |
| - skipped | |
| - name: limit | |
| in: query | |
| description: Maximum results per page | |
| schema: | |
| type: integer | |
| minimum: 1 | |
| maximum: 1000 | |
| default: 100 | |
| - name: offset | |
| in: query | |
| description: Number of results to skip | |
| schema: | |
| type: integer | |
| minimum: 0 | |
| default: 0 | |
| responses: | |
| '200': | |
| description: Results retrieved successfully | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/JobResults' | |
| '401': | |
| $ref: '#/components/responses/Unauthorized' | |
| '404': | |
| $ref: '#/components/responses/NotFound' | |
| '500': | |
| $ref: '#/components/responses/InternalServerError' | |
| /api/v1/jobs/{job_id}/errors: | |
| get: | |
| operationId: getJobErrors | |
| summary: Get job errors | |
| description: | | |
| Convenience endpoint to retrieve only failed entries from a job. | |
| Returns the original entry data alongside error details to facilitate | |
| debugging and resubmission of failed entries. | |
| tags: | |
| - Jobs | |
| parameters: | |
| - $ref: '#/components/parameters/JobId' | |
| responses: | |
| '200': | |
| description: Errors retrieved successfully | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/JobErrors' | |
| examples: | |
| withErrors: | |
| summary: Job with validation errors | |
| value: | |
| job_id: "550e8400-e29b-41d4-a716-446655440000" | |
| total_errors: 2 | |
| errors: | |
| - entry_id: "prod-005" | |
| error: | |
| type: "validation" | |
| message: "At least one variant is required" | |
| field: "variants" | |
| data: | |
| handle: "empty-product" | |
| title: "Product Without Variants" | |
| variants: [] | |
| - entry_id: "prod-008" | |
| error: | |
| type: "resolution" | |
| message: "Store not found: INVALID-STORE" | |
| field: "store_id" | |
| data: | |
| sku: "12345" | |
| store_id: "INVALID-STORE" | |
| '401': | |
| $ref: '#/components/responses/Unauthorized' | |
| '404': | |
| $ref: '#/components/responses/NotFound' | |
| '500': | |
| $ref: '#/components/responses/InternalServerError' | |
| components: | |
| securitySchemes: | |
| BearerAuth: | |
| type: http | |
| scheme: bearer | |
| description: | | |
| JWT or API key passed as Bearer token. | |
| ``` | |
| Authorization: Bearer <token> | |
| ``` | |
| ApiKeyAuth: | |
| type: apiKey | |
| in: header | |
| name: X-API-Key | |
| description: | | |
| API key passed in custom header. | |
| ``` | |
| X-API-Key: <api_key> | |
| ``` | |
| parameters: | |
| JobId: | |
| name: job_id | |
| in: path | |
| required: true | |
| description: System-generated job identifier (UUID) | |
| schema: | |
| type: string | |
| format: uuid | |
| examples: | |
| default: | |
| value: "550e8400-e29b-41d4-a716-446655440000" | |
| schemas: | |
| # ========================================================================= | |
| # Request Schemas | |
| # ========================================================================= | |
| ProductIngestRequest: | |
| type: object | |
| required: | |
| - entries | |
| properties: | |
| idempotency_key: | |
| type: string | |
| description: Client-provided key for idempotent retries | |
| maxLength: 255 | |
| examples: | |
| - "import-2025-01-06-batch-001" | |
| entries: | |
| type: array | |
| description: Array of product entries to process | |
| minItems: 1 | |
| maxItems: 10000 | |
| items: | |
| $ref: '#/components/schemas/ProductEntry' | |
| options: | |
| $ref: '#/components/schemas/IngestOptions' | |
| ProductEntry: | |
| type: object | |
| required: | |
| - entry_id | |
| - data | |
| properties: | |
| entry_id: | |
| type: string | |
| description: Client-provided identifier for error correlation | |
| maxLength: 255 | |
| examples: | |
| - "prod-001" | |
| data: | |
| $ref: '#/components/schemas/ProductData' | |
| ProductData: | |
| type: object | |
| required: | |
| - variants | |
| properties: | |
| handle: | |
| type: string | |
| description: URL-safe product identifier. Generated from title if not provided. | |
| pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$' | |
| maxLength: 255 | |
| examples: | |
| - "classic-blue-shirt" | |
| title: | |
| type: string | |
| description: Product title | |
| maxLength: 255 | |
| examples: | |
| - "Classic Blue Shirt" | |
| description: | |
| type: string | |
| description: Product description (HTML allowed) | |
| examples: | |
| - "<p>A timeless classic for any occasion.</p>" | |
| vendor: | |
| type: string | |
| description: Product vendor/manufacturer | |
| maxLength: 255 | |
| examples: | |
| - "Acme Clothing" | |
| product_type: | |
| type: string | |
| description: Product type or category | |
| maxLength: 255 | |
| examples: | |
| - "Shirts" | |
| tags: | |
| type: string | |
| description: Comma-separated list of tags | |
| examples: | |
| - "clothing,shirts,blue,summer" | |
| published: | |
| type: boolean | |
| description: Whether the product is visible in the storefront | |
| default: true | |
| metafields: | |
| type: object | |
| description: Custom metafields as namespace.key to value mapping | |
| additionalProperties: | |
| type: string | |
| examples: | |
| - custom.material: "100% Cotton" | |
| custom.care_instructions: "Machine wash cold" | |
| variants: | |
| type: array | |
| description: Product variants (at least one required) | |
| minItems: 1 | |
| items: | |
| $ref: '#/components/schemas/VariantData' | |
| VariantData: | |
| type: object | |
| required: | |
| - sku | |
| properties: | |
| sku: | |
| type: string | |
| description: Stock keeping unit. Numeric values are padded to 9 digits. | |
| maxLength: 255 | |
| examples: | |
| - "CBS-S-BLU" | |
| price: | |
| type: number | |
| format: double | |
| description: Selling price | |
| minimum: 0 | |
| default: 0 | |
| examples: | |
| - 49.99 | |
| compare_at_price: | |
| type: | |
| - number | |
| - "null" | |
| format: double | |
| description: Original price (displayed as strikethrough for sale items) | |
| minimum: 0 | |
| examples: | |
| - 59.99 | |
| cost: | |
| type: | |
| - number | |
| - "null" | |
| format: double | |
| description: Cost of goods (not visible to customers) | |
| minimum: 0 | |
| examples: | |
| - 22.50 | |
| barcode: | |
| type: | |
| - string | |
| - "null" | |
| description: Barcode (UPC, EAN, ISBN, etc.) | |
| maxLength: 255 | |
| examples: | |
| - "123456789012" | |
| weight: | |
| type: | |
| - number | |
| - "null" | |
| format: double | |
| description: Weight value | |
| minimum: 0 | |
| examples: | |
| - 200 | |
| weight_unit: | |
| type: string | |
| description: Weight unit | |
| enum: | |
| - g | |
| - kg | |
| - lb | |
| - oz | |
| default: "g" | |
| inventory_policy: | |
| type: string | |
| description: Behavior when inventory reaches zero | |
| enum: | |
| - deny | |
| - continue | |
| default: "deny" | |
| taxable: | |
| type: boolean | |
| description: Whether the variant is subject to taxes | |
| default: true | |
| requires_shipping: | |
| type: boolean | |
| description: Whether the variant requires shipping | |
| default: true | |
| tax_code: | |
| type: | |
| - string | |
| - "null" | |
| description: Tax code for tax calculation services | |
| maxLength: 255 | |
| examples: | |
| - "P0000000" | |
| option1_name: | |
| type: | |
| - string | |
| - "null" | |
| description: First option name (e.g., "Size", "Color") | |
| maxLength: 255 | |
| examples: | |
| - "Size" | |
| option1_value: | |
| type: | |
| - string | |
| - "null" | |
| description: First option value | |
| maxLength: 255 | |
| examples: | |
| - "Small" | |
| option2_name: | |
| type: | |
| - string | |
| - "null" | |
| description: Second option name | |
| maxLength: 255 | |
| option2_value: | |
| type: | |
| - string | |
| - "null" | |
| description: Second option value | |
| maxLength: 255 | |
| option3_name: | |
| type: | |
| - string | |
| - "null" | |
| description: Third option name | |
| maxLength: 255 | |
| option3_value: | |
| type: | |
| - string | |
| - "null" | |
| description: Third option value | |
| maxLength: 255 | |
| variant_image: | |
| type: | |
| - string | |
| - "null" | |
| format: uri | |
| description: URL to variant-specific image | |
| examples: | |
| - "https://cdn.example.com/images/cbs-small-blue.jpg" | |
| variant_image_alt: | |
| type: | |
| - string | |
| - "null" | |
| description: Alt text for variant image | |
| maxLength: 512 | |
| examples: | |
| - "Classic Blue Shirt - Small" | |
| variant_metafields: | |
| type: | |
| - object | |
| - "null" | |
| description: Variant-specific metafields | |
| additionalProperties: | |
| type: string | |
| POSIngestRequest: | |
| type: object | |
| required: | |
| - entries | |
| properties: | |
| idempotency_key: | |
| type: string | |
| maxLength: 255 | |
| entries: | |
| type: array | |
| minItems: 1 | |
| maxItems: 10000 | |
| items: | |
| $ref: '#/components/schemas/POSEntry' | |
| options: | |
| $ref: '#/components/schemas/IngestOptions' | |
| POSEntry: | |
| type: object | |
| required: | |
| - entry_id | |
| - data | |
| properties: | |
| entry_id: | |
| type: string | |
| maxLength: 255 | |
| data: | |
| $ref: '#/components/schemas/POSData' | |
| POSData: | |
| type: object | |
| required: | |
| - sku | |
| - store_id | |
| properties: | |
| sku: | |
| type: string | |
| description: Stock keeping unit (padded to 9 digits for numeric values) | |
| maxLength: 255 | |
| examples: | |
| - "12345" | |
| store_id: | |
| type: string | |
| description: Store number for routing | |
| maxLength: 50 | |
| examples: | |
| - "STORE-101" | |
| title: | |
| type: string | |
| description: Product title | |
| maxLength: 255 | |
| examples: | |
| - "Seasonal Item" | |
| vendor: | |
| type: string | |
| description: Product vendor | |
| maxLength: 255 | |
| default: "Unknown" | |
| description: | |
| type: | |
| - string | |
| - "null" | |
| description: Product description | |
| barcode: | |
| type: | |
| - string | |
| - "null" | |
| description: Barcode (UPC, EAN, etc.) | |
| maxLength: 255 | |
| price: | |
| type: number | |
| format: double | |
| minimum: 0 | |
| default: 0 | |
| compare_at_price: | |
| type: | |
| - number | |
| - "null" | |
| format: double | |
| minimum: 0 | |
| cost: | |
| type: | |
| - number | |
| - "null" | |
| format: double | |
| minimum: 0 | |
| status: | |
| type: string | |
| description: Product status (ignored - always treated as active) | |
| enum: | |
| - A | |
| - P | |
| - D | |
| default: "A" | |
| variant_tax_code: | |
| type: | |
| - string | |
| - "null" | |
| description: Tax code for tax calculation | |
| maxLength: 255 | |
| metafields: | |
| type: | |
| - object | |
| - "null" | |
| additionalProperties: | |
| type: string | |
| InventoryIngestRequest: | |
| type: object | |
| required: | |
| - entries | |
| properties: | |
| idempotency_key: | |
| type: string | |
| maxLength: 255 | |
| entries: | |
| type: array | |
| minItems: 1 | |
| maxItems: 10000 | |
| items: | |
| $ref: '#/components/schemas/InventoryEntry' | |
| options: | |
| allOf: | |
| - $ref: '#/components/schemas/IngestOptions' | |
| - type: object | |
| properties: | |
| mode: | |
| type: string | |
| description: | | |
| Processing mode: | |
| - `initial`: Sets absolute quantity values | |
| - `delta`: Adjusts quantities by specified amounts | |
| enum: | |
| - initial | |
| - delta | |
| default: "delta" | |
| InventoryEntry: | |
| type: object | |
| required: | |
| - entry_id | |
| - data | |
| properties: | |
| entry_id: | |
| type: string | |
| maxLength: 255 | |
| data: | |
| $ref: '#/components/schemas/InventoryData' | |
| InventoryData: | |
| type: object | |
| required: | |
| - sku | |
| - store | |
| properties: | |
| sku: | |
| type: string | |
| description: Stock keeping unit | |
| maxLength: 255 | |
| store: | |
| type: string | |
| description: Store number | |
| maxLength: 50 | |
| location_name: | |
| type: | |
| - string | |
| - "null" | |
| description: Location name for multi-location inventory. Defaults to store value. | |
| maxLength: 255 | |
| on_hand: | |
| type: integer | |
| description: Quantity on hand (absolute value or delta depending on mode) | |
| default: 0 | |
| on_order: | |
| type: integer | |
| description: Quantity on order | |
| default: 0 | |
| PriceIngestRequest: | |
| type: object | |
| required: | |
| - entries | |
| properties: | |
| idempotency_key: | |
| type: string | |
| maxLength: 255 | |
| entries: | |
| type: array | |
| minItems: 1 | |
| maxItems: 10000 | |
| items: | |
| $ref: '#/components/schemas/PriceEntry' | |
| options: | |
| $ref: '#/components/schemas/IngestOptions' | |
| PriceEntry: | |
| type: object | |
| required: | |
| - entry_id | |
| - data | |
| properties: | |
| entry_id: | |
| type: string | |
| maxLength: 255 | |
| data: | |
| $ref: '#/components/schemas/PriceData' | |
| PriceData: | |
| type: object | |
| required: | |
| - sku | |
| - store_id | |
| - price | |
| properties: | |
| sku: | |
| type: string | |
| description: Stock keeping unit | |
| maxLength: 255 | |
| store_id: | |
| type: string | |
| description: Store number | |
| maxLength: 50 | |
| child_store: | |
| type: | |
| - string | |
| - "null" | |
| description: Child store for variant routing. Defaults to store_id. | |
| maxLength: 50 | |
| price: | |
| type: number | |
| format: double | |
| description: New price value | |
| minimum: 0 | |
| price_type: | |
| type: string | |
| description: | | |
| Price type affecting compare_at_price behavior: | |
| - `R` (Regular): Sets price, clears compare_at_price | |
| - `S` (Sale): Sets price, preserves/sets compare_at_price | |
| - `C` (Clearance): Sets price, preserves/sets compare_at_price | |
| - `CS` (Clearance Sale): Combines C + S flags | |
| enum: | |
| - R | |
| - S | |
| - C | |
| - CS | |
| default: "R" | |
| IngestOptions: | |
| type: object | |
| properties: | |
| force_sync: | |
| type: boolean | |
| description: Create outbox entries even for unchanged data | |
| default: false | |
| validate_only: | |
| type: boolean | |
| description: Validate entries without persisting. Useful for dry-run testing. | |
| default: false | |
| # ========================================================================= | |
| # Response Schemas | |
| # ========================================================================= | |
| SyncIngestResponse: | |
| type: object | |
| required: | |
| - job_id | |
| - status | |
| - summary | |
| - results | |
| properties: | |
| job_id: | |
| type: string | |
| format: uuid | |
| description: System-generated job identifier | |
| examples: | |
| - "550e8400-e29b-41d4-a716-446655440000" | |
| status: | |
| type: string | |
| description: Overall job status | |
| enum: | |
| - completed | |
| - completed_with_errors | |
| - failed | |
| summary: | |
| $ref: '#/components/schemas/JobSummary' | |
| results: | |
| type: array | |
| description: Per-entry results in submission order | |
| items: | |
| $ref: '#/components/schemas/EntryResult' | |
| AsyncIngestResponse: | |
| type: object | |
| required: | |
| - job_id | |
| - status | |
| - message | |
| - links | |
| properties: | |
| job_id: | |
| type: string | |
| format: uuid | |
| description: System-generated job identifier | |
| examples: | |
| - "550e8400-e29b-41d4-a716-446655440000" | |
| status: | |
| type: string | |
| enum: | |
| - pending | |
| examples: | |
| - "pending" | |
| message: | |
| type: string | |
| description: Human-readable status message | |
| examples: | |
| - "Batch of 500 entries accepted for processing" | |
| links: | |
| type: object | |
| required: | |
| - status | |
| - results | |
| properties: | |
| status: | |
| type: string | |
| format: uri | |
| description: URL to poll for job status | |
| examples: | |
| - "/api/v1/jobs/550e8400-e29b-41d4-a716-446655440000" | |
| results: | |
| type: string | |
| format: uri | |
| description: URL to retrieve results when complete | |
| examples: | |
| - "/api/v1/jobs/550e8400-e29b-41d4-a716-446655440000/results" | |
| JobStatus: | |
| type: object | |
| required: | |
| - job_id | |
| - status | |
| - created_at | |
| - updated_at | |
| - summary | |
| - progress_percent | |
| properties: | |
| job_id: | |
| type: string | |
| format: uuid | |
| status: | |
| type: string | |
| enum: | |
| - pending | |
| - processing | |
| - completed | |
| - completed_with_errors | |
| - failed | |
| created_at: | |
| type: string | |
| format: date-time | |
| description: Job creation timestamp (ISO 8601) | |
| updated_at: | |
| type: string | |
| format: date-time | |
| description: Last update timestamp (ISO 8601) | |
| completed_at: | |
| type: | |
| - string | |
| - "null" | |
| format: date-time | |
| description: Completion timestamp (present when finished) | |
| summary: | |
| $ref: '#/components/schemas/JobSummary' | |
| progress_percent: | |
| type: integer | |
| description: Processing progress (0-100) | |
| minimum: 0 | |
| maximum: 100 | |
| JobSummary: | |
| type: object | |
| required: | |
| - total | |
| - processed | |
| - created | |
| - updated | |
| - errors | |
| properties: | |
| total: | |
| type: integer | |
| description: Total entries submitted | |
| minimum: 0 | |
| processed: | |
| type: integer | |
| description: Entries successfully processed | |
| minimum: 0 | |
| created: | |
| type: integer | |
| description: New records created | |
| minimum: 0 | |
| updated: | |
| type: integer | |
| description: Existing records updated | |
| minimum: 0 | |
| errors: | |
| type: integer | |
| description: Entries that failed | |
| minimum: 0 | |
| JobResults: | |
| type: object | |
| required: | |
| - job_id | |
| - total_results | |
| - results | |
| - pagination | |
| properties: | |
| job_id: | |
| type: string | |
| format: uuid | |
| total_results: | |
| type: integer | |
| description: Total matching results | |
| minimum: 0 | |
| results: | |
| type: array | |
| items: | |
| $ref: '#/components/schemas/EntryResult' | |
| pagination: | |
| $ref: '#/components/schemas/Pagination' | |
| JobErrors: | |
| type: object | |
| required: | |
| - job_id | |
| - total_errors | |
| - errors | |
| properties: | |
| job_id: | |
| type: string | |
| format: uuid | |
| total_errors: | |
| type: integer | |
| description: Total failed entries | |
| minimum: 0 | |
| errors: | |
| type: array | |
| items: | |
| $ref: '#/components/schemas/ErrorEntry' | |
| EntryResult: | |
| type: object | |
| required: | |
| - entry_id | |
| - status | |
| properties: | |
| entry_id: | |
| type: string | |
| description: Echo of client-provided entry_id | |
| status: | |
| type: string | |
| enum: | |
| - success | |
| - error | |
| - skipped | |
| action: | |
| type: | |
| - string | |
| - "null" | |
| description: Action taken (null if error) | |
| enum: | |
| - created | |
| - updated | |
| - unchanged | |
| product_id: | |
| type: | |
| - integer | |
| - "null" | |
| description: Created/updated product ID (on success) | |
| variant_ids: | |
| type: | |
| - array | |
| - "null" | |
| description: Created/updated variant IDs (on success) | |
| items: | |
| type: integer | |
| error: | |
| $ref: '#/components/schemas/EntryError' | |
| ErrorEntry: | |
| type: object | |
| required: | |
| - entry_id | |
| - error | |
| - data | |
| properties: | |
| entry_id: | |
| type: string | |
| error: | |
| $ref: '#/components/schemas/EntryError' | |
| data: | |
| type: object | |
| description: Original entry data for debugging | |
| additionalProperties: true | |
| EntryError: | |
| type: object | |
| required: | |
| - type | |
| - message | |
| properties: | |
| type: | |
| type: string | |
| description: | | |
| Error category: | |
| - `validation`: Business rule violation | |
| - `conversion`: Data type conversion failure | |
| - `database`: Database constraint violation | |
| - `resolution`: Reference lookup failure | |
| enum: | |
| - validation | |
| - conversion | |
| - database | |
| - resolution | |
| message: | |
| type: string | |
| description: Human-readable error description | |
| examples: | |
| - "SKU is required" | |
| field: | |
| type: | |
| - string | |
| - "null" | |
| description: Field that caused the error | |
| examples: | |
| - "sku" | |
| Pagination: | |
| type: object | |
| required: | |
| - limit | |
| - offset | |
| - has_more | |
| properties: | |
| limit: | |
| type: integer | |
| description: Current page size | |
| offset: | |
| type: integer | |
| description: Current offset | |
| has_more: | |
| type: boolean | |
| description: Whether more results are available | |
| # ========================================================================= | |
| # Error Response Schemas | |
| # ========================================================================= | |
| ErrorResponse: | |
| type: object | |
| required: | |
| - error | |
| properties: | |
| error: | |
| type: string | |
| description: Human-readable error message | |
| ValidationErrorResponse: | |
| type: object | |
| required: | |
| - error | |
| - validation_errors | |
| properties: | |
| error: | |
| type: string | |
| examples: | |
| - "Request validation failed" | |
| validation_errors: | |
| type: array | |
| items: | |
| type: object | |
| properties: | |
| field: | |
| type: string | |
| message: | |
| type: string | |
| RateLimitErrorResponse: | |
| type: object | |
| required: | |
| - error | |
| - retry_after | |
| properties: | |
| error: | |
| type: string | |
| examples: | |
| - "Rate limit exceeded" | |
| retry_after: | |
| type: integer | |
| description: Seconds until the rate limit resets | |
| examples: | |
| - 30 | |
| responses: | |
| BadRequest: | |
| description: All entries failed validation | |
| content: | |
| application/json: | |
| schema: | |
| type: object | |
| properties: | |
| error: | |
| type: string | |
| details: | |
| type: array | |
| items: | |
| type: object | |
| examples: | |
| default: | |
| value: | |
| error: "All entries failed validation" | |
| details: | |
| - entry_id: "prod-001" | |
| message: "SKU is required" | |
| Unauthorized: | |
| description: Missing or invalid authentication | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ErrorResponse' | |
| examples: | |
| default: | |
| value: | |
| error: "Invalid or missing API key" | |
| NotFound: | |
| description: Resource not found | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ErrorResponse' | |
| examples: | |
| default: | |
| value: | |
| error: "Job not found" | |
| PayloadTooLarge: | |
| description: Request payload exceeds size limit | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ErrorResponse' | |
| examples: | |
| default: | |
| value: | |
| error: "Payload exceeds 10MB limit" | |
| UnprocessableEntity: | |
| description: Invalid request schema | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ValidationErrorResponse' | |
| examples: | |
| default: | |
| value: | |
| error: "Request validation failed" | |
| validation_errors: | |
| - field: "entries" | |
| message: "must contain at least 1 item" | |
| RateLimitExceeded: | |
| description: Rate limit exceeded | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/RateLimitErrorResponse' | |
| examples: | |
| default: | |
| value: | |
| error: "Rate limit exceeded: 60 requests per minute" | |
| retry_after: 45 | |
| InternalServerError: | |
| description: Internal server error | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ErrorResponse' | |
| examples: | |
| default: | |
| value: | |
| error: "An unexpected error occurred" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment