Last active
January 30, 2026 20:20
-
-
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.6 | |
| description: | | |
| REST API for batch ingestion of product, POS, inventory, and price data into the 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. | |
| ## 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: ag@pthreemedia.com | |
| license: | |
| name: Proprietary | |
| identifier: LicenseRef-Proprietary | |
| servers: | |
| - url: https://shopify-bridge-api.up.railway.app | |
| description: QA Environment (Railway) | |
| - 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 | |
| - name: Admin | |
| description: Administrative endpoints for job and outbox management | |
| security: | |
| - BearerAuth: [] | |
| paths: | |
| /api/v1/ingest/products/{store_number}: | |
| post: | |
| operationId: ingestProducts | |
| summary: Ingest products with variants | |
| parameters: | |
| - $ref: '#/components/parameters/StoreNumber' | |
| 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: | |
| multiVariantProduct: | |
| summary: Multi-variant product with full metafields | |
| value: | |
| idempotency_key: "qa-test-products-001" | |
| options: | |
| force_sync: true | |
| entries: | |
| - entry_id: "product-1" | |
| data: | |
| handle: "1-4z-pwrbl-sm-e-wd-scarl-xs---925658-1" | |
| title: "University of New Mexico 1/4 Zip Powerblend Jacket" | |
| description: "This versatile, eco-friendly Powerblend 1/4 zip campus sweatshirt gives you a great casual look." | |
| vendor: "Champion" | |
| product_type: "Sweatshirts" | |
| tags: "collection:men,collection:clothing-and-accessories,collection:sweatshirts" | |
| published: true | |
| metafields: | |
| custom.product_number: "030631 CS2083/E1177237/2299" | |
| custom.manufacturer: "Champion Products" | |
| custom.fabric: "50% Cotton/50% Polyester" | |
| custom.embellishment: "Embroidered/Sewn" | |
| custom.merchandisehierarchy: "500004001998" | |
| custom.dept: "500" | |
| custom.deptname: "Mens Apparel" | |
| custom.vendornumber: "30631" | |
| custom.vendorstyle: "CS2083/E1177237" | |
| custom.seasoncode: "BNS" | |
| custom.vendor_partnumber: "CS2083" | |
| custom.san_number: "804-1814" | |
| custom.aap_flag: "False" | |
| custom.esd_flag: "False" | |
| custom.graphic_type: "Sustainable" | |
| taxonomy.department: "500" | |
| taxonomy.sub_department: "004" | |
| taxonomy.class: "001" | |
| taxonomy.sub_class: "998" | |
| merchandising.parent_store: "9975" | |
| merchandising.child_store: "2299" | |
| fulfillment.dsv_flag: "False" | |
| fulfillment.vendor: "CHAMPION PRODUCTS" | |
| fulfillment.vendor_number: "30631" | |
| variants: | |
| - sku: "18886985" | |
| price: 75 | |
| compare_at_price: 75 | |
| cost: 28.92 | |
| barcode: "18886985" | |
| taxable: true | |
| requires_shipping: true | |
| tax_code: "61000" | |
| inventory_policy: "deny" | |
| option1_name: "COLOR" | |
| option1_value: "Scarlet Red" | |
| option2_name: "SIZE" | |
| option2_value: "XSmall" | |
| variant_image: "https://bkstr.scene7.com/is/image/Bkstr/2299-CS2083-E1177237-Scarlet-Red" | |
| variant_metafields: | |
| vertex.product_class: "61000" | |
| custom.clearance_flag: "False" | |
| custom.sale_flag: "False" | |
| - sku: "18886986" | |
| price: 75 | |
| compare_at_price: 75 | |
| cost: 28.92 | |
| barcode: "18886986" | |
| taxable: true | |
| requires_shipping: true | |
| tax_code: "61000" | |
| inventory_policy: "deny" | |
| option1_name: "COLOR" | |
| option1_value: "Scarlet Red" | |
| option2_name: "SIZE" | |
| option2_value: "Small" | |
| variant_image: "https://bkstr.scene7.com/is/image/Bkstr/2299-CS2083-E1177237-Scarlet-Red" | |
| variant_metafields: | |
| vertex.product_class: "61000" | |
| custom.clearance_flag: "False" | |
| custom.sale_flag: "False" | |
| - entry_id: "product-2" | |
| data: | |
| handle: "12x40-cooling-towe-team1-------432088-1" | |
| title: "University of New Mexico 12x40 Cooling Towel" | |
| product_type: "Tailgate & Spirit" | |
| tags: "collection:tailgate-and-spirit,collection:gifts-and-collectibles" | |
| published: true | |
| metafields: | |
| custom.product_number: "066255 52300/58/2299" | |
| custom.manufacturer: "The Northwest Company" | |
| custom.embellishment: "Embroidered/Sewn" | |
| custom.merchandisehierarchy: "600011001001" | |
| custom.dept: "600" | |
| custom.deptname: "Gifts" | |
| custom.vendornumber: "66255" | |
| custom.vendorstyle: "52300/58" | |
| custom.seasoncode: "BNS" | |
| custom.vendor_partnumber: "52300" | |
| custom.san_number: "804-1814" | |
| custom.aap_flag: "False" | |
| custom.esd_flag: "False" | |
| custom.graphic_type: "Wordmark" | |
| taxonomy.department: "600" | |
| taxonomy.sub_department: "011" | |
| taxonomy.class: "001" | |
| taxonomy.sub_class: "001" | |
| merchandising.parent_store: "9975" | |
| merchandising.child_store: "2299" | |
| fulfillment.dsv_flag: "False" | |
| fulfillment.vendor: "The Northwest Group, LLC" | |
| fulfillment.vendor_number: "66255" | |
| variants: | |
| - sku: "27490402" | |
| price: 20 | |
| compare_at_price: 20 | |
| cost: 6.17 | |
| barcode: "190604737423" | |
| taxable: true | |
| requires_shipping: true | |
| tax_code: "61360" | |
| inventory_policy: "deny" | |
| option1_name: "COLOR" | |
| option1_value: "Team Color" | |
| variant_image: "https://bkstr.scene7.com/is/image/Bkstr/2299-52300-58-Team-Color" | |
| variant_metafields: | |
| vertex.product_class: "61360" | |
| custom.clearance_flag: "False" | |
| custom.sale_flag: "False" | |
| minimalProduct: | |
| summary: Minimal product (required fields only) | |
| value: | |
| entries: | |
| - entry_id: "prod-minimal" | |
| data: | |
| title: "Simple Product" | |
| 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/{store_number}: | |
| post: | |
| operationId: ingestPOS | |
| summary: Ingest POS product updates | |
| parameters: | |
| - $ref: '#/components/parameters/StoreNumber' | |
| 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 batch with metafields | |
| value: | |
| idempotency_key: "qa-test-pos-001" | |
| entries: | |
| - entry_id: "row-1" | |
| data: | |
| sku: "000000004" | |
| title: "SUPPLIES" | |
| vendor: "CATEGORY&DUMP SKU-NO SCAN CREDIT" | |
| description: "School Supplies" | |
| barcode: "000000000004" | |
| price: 0 | |
| compare_at_price: 0 | |
| cost: 0 | |
| tax_code: "76900" | |
| metafields: | |
| custom.dept: "400" | |
| taxonomy.department: "400" | |
| custom.deptname: "School Supplies" | |
| custom.vendornumber: "222222" | |
| custom.merchandisehierarchy: "500999000000" | |
| taxonomy.sub_department: "998" | |
| taxonomy.class: "998" | |
| taxonomy.sub_class: "998" | |
| merchandising.child_store: "2299" | |
| - entry_id: "row-2" | |
| data: | |
| sku: "000000005" | |
| title: "Emblematic Men's Apparel" | |
| vendor: "CATEGORY&DUMP SKU-NO SCAN CREDIT" | |
| description: "Mens Apparel" | |
| barcode: "000000000005" | |
| price: 0.01 | |
| compare_at_price: 0.01 | |
| cost: 0 | |
| tax_code: "61000" | |
| metafields: | |
| custom.dept: "500" | |
| taxonomy.department: "500" | |
| custom.deptname: "Mens Apparel" | |
| custom.vendornumber: "222222" | |
| custom.merchandisehierarchy: "500999000000" | |
| taxonomy.sub_department: "998" | |
| taxonomy.class: "998" | |
| taxonomy.sub_class: "998" | |
| merchandising.child_store: "2299" | |
| 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/{store_number}: | |
| post: | |
| operationId: ingestInventory | |
| summary: Ingest inventory level updates | |
| parameters: | |
| - $ref: '#/components/parameters/StoreNumber' | |
| 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: "27" | |
| 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: "27" | |
| 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: "27" | |
| 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' | |
| # ========================================================================= | |
| # Admin Endpoints | |
| # ========================================================================= | |
| /api/v1/admin/import-jobs/{id}: | |
| get: | |
| operationId: getImportJob | |
| summary: Get import job details | |
| description: | | |
| Retrieve detailed information about an import job including: | |
| - Job status and progress | |
| - Processing statistics (total, processed, errors) | |
| - File metadata (if file-based import) | |
| - Timestamps (created, updated, completed) | |
| This endpoint provides enriched data including sync error counts from the outbox. | |
| tags: | |
| - Admin | |
| parameters: | |
| - name: id | |
| in: path | |
| required: true | |
| description: Import job ID | |
| schema: | |
| type: integer | |
| format: int64 | |
| examples: | |
| default: | |
| value: 27 | |
| responses: | |
| '200': | |
| description: Import job retrieved successfully | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ImportJobResponse' | |
| examples: | |
| completed: | |
| summary: Completed import job | |
| value: | |
| import_job: | |
| id: 27 | |
| file_type: "api_product" | |
| source: "api" | |
| status: "completed" | |
| total_rows: 2 | |
| processed_rows: 2 | |
| error_rows: 0 | |
| sync_errors: 0 | |
| created_at: "2025-01-30T10:30:00Z" | |
| updated_at: "2025-01-30T10:30:05Z" | |
| '404': | |
| $ref: '#/components/responses/NotFound' | |
| '500': | |
| $ref: '#/components/responses/InternalServerError' | |
| /api/v1/admin/import-jobs/{id}/results: | |
| get: | |
| operationId: getImportJobResults | |
| summary: Get import job results | |
| description: | | |
| Retrieve all results for an import job with pagination support. | |
| Results include both successful entries and errors, with details about: | |
| - Entry ID (client-provided identifier) | |
| - Status (success, error, skipped) | |
| - Action taken (created, updated, unchanged) | |
| - Product and variant IDs (on success) | |
| - Error details (on failure) | |
| tags: | |
| - Admin | |
| parameters: | |
| - name: id | |
| in: path | |
| required: true | |
| description: Import job ID | |
| schema: | |
| type: integer | |
| format: int64 | |
| examples: | |
| default: | |
| value: 27 | |
| - 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/ImportJobResultsResponse' | |
| examples: | |
| withResults: | |
| summary: Job results with mixed outcomes | |
| value: | |
| results: | |
| - entry_id: "product-1" | |
| status: "success" | |
| action: "created" | |
| product_id: 649597 | |
| variant_ids: [1029834, 1029835] | |
| - entry_id: "product-2" | |
| status: "success" | |
| action: "updated" | |
| product_id: 649598 | |
| variant_ids: [1029836] | |
| pagination: | |
| limit: 100 | |
| offset: 0 | |
| total: 2 | |
| '404': | |
| $ref: '#/components/responses/NotFound' | |
| '500': | |
| $ref: '#/components/responses/InternalServerError' | |
| /api/v1/admin/import-jobs/{id}/outbox: | |
| get: | |
| operationId: getImportJobOutbox | |
| summary: Get outbox events for import job | |
| description: | | |
| Retrieve outbox events linked to an import job. | |
| Outbox events represent pending Shopify sync operations. Use this endpoint to: | |
| - Check if sync is complete (all events processed) | |
| - Identify failed syncs (events with status=failed) | |
| - Debug sync issues (view payloads and error messages) | |
| tags: | |
| - Admin | |
| parameters: | |
| - name: id | |
| in: path | |
| required: true | |
| description: Import job ID | |
| schema: | |
| type: integer | |
| format: int64 | |
| - name: status | |
| in: query | |
| description: Filter by outbox status (comma-separated) | |
| schema: | |
| type: string | |
| examples: | |
| pending: | |
| value: "pending" | |
| multiple: | |
| value: "pending,processing,failed" | |
| - name: limit | |
| in: query | |
| schema: | |
| type: integer | |
| default: 100 | |
| - name: offset | |
| in: query | |
| schema: | |
| type: integer | |
| default: 0 | |
| responses: | |
| '200': | |
| description: Outbox events retrieved successfully | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ImportJobOutboxResponse' | |
| '404': | |
| $ref: '#/components/responses/NotFound' | |
| '500': | |
| $ref: '#/components/responses/InternalServerError' | |
| /api/v1/admin/import-jobs/{id}/errors: | |
| get: | |
| operationId: getImportJobErrors | |
| summary: Get import job errors | |
| description: | | |
| Retrieve only error entries for an import job. | |
| Convenience endpoint that filters results to show only failures, | |
| making it easier to identify and debug processing issues. | |
| tags: | |
| - Admin | |
| parameters: | |
| - name: id | |
| in: path | |
| required: true | |
| description: Import job ID | |
| schema: | |
| type: integer | |
| format: int64 | |
| responses: | |
| '200': | |
| description: Errors retrieved successfully | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ImportJobErrorsResponse' | |
| '404': | |
| $ref: '#/components/responses/NotFound' | |
| '500': | |
| $ref: '#/components/responses/InternalServerError' | |
| components: | |
| securitySchemes: | |
| BearerAuth: | |
| type: http | |
| scheme: bearer | |
| description: | | |
| API key authentication via Bearer token. | |
| **Usage:** | |
| ``` | |
| Authorization: Bearer <your_api_key> | |
| ``` | |
| **Example curl request:** | |
| ```bash | |
| curl -X POST "https://shopify-bridge-api.up.railway.app/api/v1/ingest/products/9975" \ | |
| -H "Authorization: Bearer your-api-key-here" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"entries": [...]}' | |
| ``` | |
| Contact ag@pthreemedia.com to request an API key. | |
| parameters: | |
| JobId: | |
| name: job_id | |
| in: path | |
| required: true | |
| description: System-generated job identifier (integer as string) | |
| schema: | |
| type: string | |
| pattern: '^\d+$' | |
| examples: | |
| default: | |
| value: "27" | |
| StoreNumber: | |
| name: store_number | |
| in: path | |
| required: true | |
| description: Store number for routing the request | |
| schema: | |
| type: string | |
| examples: | |
| default: | |
| value: "9975" | |
| 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 | |
| description: | | |
| POS item data. Store number is provided via URL path parameter. | |
| Each POS entry creates one product with one variant (1:1 relationship). | |
| required: | |
| - sku | |
| properties: | |
| sku: | |
| type: string | |
| description: Stock keeping unit (padded to 9 digits for numeric values) | |
| maxLength: 255 | |
| examples: | |
| - "000000004" | |
| title: | |
| type: string | |
| description: Product title. Defaults to "POS Product {row}" if empty. | |
| maxLength: 255 | |
| examples: | |
| - "SUPPLIES" | |
| vendor: | |
| type: string | |
| description: Product vendor | |
| maxLength: 255 | |
| default: "Unknown" | |
| examples: | |
| - "CATEGORY&DUMP SKU-NO SCAN CREDIT" | |
| description: | |
| type: | |
| - string | |
| - "null" | |
| description: Product description | |
| examples: | |
| - "School Supplies" | |
| barcode: | |
| type: | |
| - string | |
| - "null" | |
| description: Barcode (UPC). Must be numeric; invalid values are converted to empty string. | |
| maxLength: 255 | |
| examples: | |
| - "000000000004" | |
| price: | |
| type: number | |
| format: double | |
| description: Selling price (POS_PRICE) | |
| minimum: 0 | |
| default: 0 | |
| examples: | |
| - 0 | |
| - 10.01 | |
| compare_at_price: | |
| type: | |
| - number | |
| - "null" | |
| format: double | |
| description: Original/regular price (REG_PRICE) | |
| minimum: 0 | |
| examples: | |
| - 0 | |
| - 0.01 | |
| cost: | |
| type: | |
| - number | |
| - "null" | |
| format: double | |
| description: Cost of goods | |
| minimum: 0 | |
| tax_code: | |
| type: | |
| - string | |
| - "null" | |
| description: | | |
| Tax code for tax calculation. | |
| Sets both `variants.tax_code` field AND `vertex.product_class` variant metafield. | |
| maxLength: 255 | |
| examples: | |
| - "76900" | |
| - "61000" | |
| metafields: | |
| type: | |
| - object | |
| - "null" | |
| description: | | |
| Product-level metafields as namespace.key to value mapping. | |
| Common POS metafields include: | |
| - `custom.dept` - Department number (padded to 3 digits) | |
| - `taxonomy.department` - Taxonomy department (padded to 3 digits) | |
| - `custom.deptname` - Department name | |
| - `custom.vendornumber` - Vendor number | |
| - `custom.merchandisehierarchy` - Merchandise hierarchy code | |
| - `taxonomy.sub_department` - Sub-department (padded to 3 digits) | |
| - `taxonomy.class` - Class (padded to 3 digits) | |
| - `taxonomy.sub_class` - Sub-class (padded to 3 digits) | |
| - `merchandising.child_store` - Child store number | |
| 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: int64 | |
| description: System-generated job identifier | |
| examples: | |
| - "27" | |
| 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: int64 | |
| description: System-generated job identifier | |
| examples: | |
| - "27" | |
| 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/27" | |
| results: | |
| type: string | |
| format: uri | |
| description: URL to retrieve results when complete | |
| examples: | |
| - "/api/v1/jobs/27/results" | |
| JobStatus: | |
| type: object | |
| required: | |
| - job_id | |
| - status | |
| - created_at | |
| - updated_at | |
| - summary | |
| - progress_percent | |
| properties: | |
| job_id: | |
| type: string | |
| format: int64 | |
| 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: int64 | |
| 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: int64 | |
| 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 | |
| # ========================================================================= | |
| # Admin Response Schemas | |
| # ========================================================================= | |
| ImportJobResponse: | |
| type: object | |
| required: | |
| - import_job | |
| properties: | |
| import_job: | |
| $ref: '#/components/schemas/ImportJob' | |
| ImportJob: | |
| type: object | |
| properties: | |
| id: | |
| type: integer | |
| format: int64 | |
| description: Import job ID | |
| file_id: | |
| type: | |
| - string | |
| - "null" | |
| description: File ID (for file-based imports) | |
| file_path: | |
| type: | |
| - string | |
| - "null" | |
| description: File path (for file-based imports) | |
| file_type: | |
| type: string | |
| description: Type of import (product, pos, inventory, api_product, api_pos, etc.) | |
| examples: | |
| - "api_product" | |
| - "pos" | |
| - "product" | |
| source: | |
| type: string | |
| description: Import source | |
| enum: | |
| - file | |
| - api | |
| status: | |
| type: string | |
| description: Job status | |
| enum: | |
| - pending | |
| - processing | |
| - completed | |
| - completed_with_errors | |
| - failed | |
| total_rows: | |
| type: integer | |
| description: Total entries in the job | |
| processed_rows: | |
| type: integer | |
| description: Successfully processed entries | |
| error_rows: | |
| type: integer | |
| description: Failed entries | |
| sync_errors: | |
| type: integer | |
| description: Number of failed outbox sync operations | |
| error_message: | |
| type: | |
| - string | |
| - "null" | |
| description: Error message (if failed) | |
| idempotency_key: | |
| type: | |
| - string | |
| - "null" | |
| description: Client-provided idempotency key | |
| store_id: | |
| type: | |
| - integer | |
| - "null" | |
| format: int64 | |
| description: Store ID (for API imports) | |
| created_at: | |
| type: string | |
| format: date-time | |
| updated_at: | |
| type: string | |
| format: date-time | |
| ImportJobResultsResponse: | |
| type: object | |
| required: | |
| - results | |
| - pagination | |
| properties: | |
| results: | |
| type: array | |
| items: | |
| $ref: '#/components/schemas/ImportResult' | |
| pagination: | |
| type: object | |
| properties: | |
| limit: | |
| type: integer | |
| offset: | |
| type: integer | |
| total: | |
| type: integer | |
| ImportResult: | |
| type: object | |
| properties: | |
| id: | |
| type: integer | |
| format: int64 | |
| import_job_id: | |
| type: integer | |
| format: int64 | |
| row_number: | |
| type: integer | |
| entry_id: | |
| type: | |
| - string | |
| - "null" | |
| description: Client-provided entry identifier | |
| status: | |
| type: string | |
| enum: | |
| - success | |
| - error | |
| - skipped | |
| action: | |
| type: | |
| - string | |
| - "null" | |
| description: Action taken (created, updated, unchanged) | |
| enum: | |
| - created | |
| - updated | |
| - unchanged | |
| product_id: | |
| type: | |
| - integer | |
| - "null" | |
| format: int64 | |
| variant_ids: | |
| type: | |
| - array | |
| - "null" | |
| items: | |
| type: integer | |
| format: int64 | |
| error_type: | |
| type: | |
| - string | |
| - "null" | |
| description: Error category (validation, database, conversion, resolution) | |
| error_message: | |
| type: | |
| - string | |
| - "null" | |
| error_column: | |
| type: | |
| - string | |
| - "null" | |
| created_at: | |
| type: string | |
| format: date-time | |
| ImportJobOutboxResponse: | |
| type: object | |
| required: | |
| - outbox_events | |
| - pagination | |
| properties: | |
| outbox_events: | |
| type: array | |
| items: | |
| $ref: '#/components/schemas/OutboxEvent' | |
| pagination: | |
| type: object | |
| properties: | |
| limit: | |
| type: integer | |
| offset: | |
| type: integer | |
| total: | |
| type: integer | |
| OutboxEvent: | |
| type: object | |
| properties: | |
| id: | |
| type: integer | |
| format: int64 | |
| aggregate_id: | |
| type: integer | |
| format: int64 | |
| description: Product or variant ID | |
| aggregate_type: | |
| type: string | |
| description: Entity type | |
| enum: | |
| - product | |
| - variant | |
| event_type: | |
| type: string | |
| description: Operation type | |
| enum: | |
| - create | |
| - update | |
| store_number: | |
| type: string | |
| sync_operation: | |
| type: | |
| - string | |
| - "null" | |
| description: Sync operation type (product.sync, pos.sync, inventory, price) | |
| status: | |
| type: string | |
| enum: | |
| - pending | |
| - processing | |
| - processed | |
| - failed | |
| attempts: | |
| type: integer | |
| max_attempts: | |
| type: integer | |
| error_message: | |
| type: | |
| - string | |
| - "null" | |
| created_at: | |
| type: string | |
| format: date-time | |
| updated_at: | |
| type: string | |
| format: date-time | |
| processed_at: | |
| type: | |
| - string | |
| - "null" | |
| format: date-time | |
| ImportJobErrorsResponse: | |
| type: object | |
| required: | |
| - errors | |
| properties: | |
| errors: | |
| type: array | |
| items: | |
| $ref: '#/components/schemas/ImportResult' | |
| total: | |
| type: integer | |
| # ========================================================================= | |
| # 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 Authorization header | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ErrorResponse' | |
| examples: | |
| missingHeader: | |
| summary: Missing Authorization header | |
| value: | |
| error: "Missing Authorization header" | |
| invalidFormat: | |
| summary: Invalid header format (must be "Bearer <token>") | |
| value: | |
| error: "Invalid Authorization header format" | |
| invalidKey: | |
| summary: Invalid or inactive API key | |
| value: | |
| error: "Invalid or inactive 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