Last active
July 9, 2026 20:35
-
-
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.3.3 | |
| 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 | | |
| |------------|------|----------| | |
| | ≤ 25 entries | Synchronous | `200`/`207` with full results | | |
| | > 25 entries | Asynchronous | `202` with job_id for polling | | |
| > **Note:** The synchronous threshold defaults to **25 entries** for all feed types | |
| > (products, POS, inventory, prices). Any request exceeding the threshold is processed | |
| > asynchronously and returns `202` with a `job_id` for polling. | |
| > Per-feed thresholds are configurable via `INGEST_SYNC_THRESHOLD_*` env vars. | |
| ## Rate Limits | |
| | Limit | Value | | |
| |-------|-------| | |
| | Requests per minute | 100 | | |
| | Payload size | 10 MB | | |
| | Entries per request | 10,000 | | |
| ## Request Correlation (X-Request-ID) | |
| Every endpoint accepts an optional `X-Request-ID` request header for | |
| correlating client and server logs. If omitted (or if the value does | |
| not match `^[A-Za-z0-9_-]{1,64}$`), the server generates a UUIDv7. | |
| For maximum compatibility the server also reads `Request-Id` as a | |
| fallback. The chosen value is always echoed on the `X-Request-ID` | |
| response header — on success and on error responses (4xx/5xx). | |
| For ingestion endpoints, the value is additionally: | |
| - Returned in the response body's `request_id` field. | |
| - Persisted to the `import_job_request_ids` audit table linked to | |
| the resulting `import_jobs.id`. Retries against the same | |
| `Idempotency-Key` with a new `X-Request-ID` produce additional | |
| audit rows; the oldest entry identifies the original request that | |
| produced the job. | |
| 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: 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 | | |
| | 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 | | |
| | Change detection | Skips unchanged products (compares title, data, metafields) | | |
| | 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 | | |
| | SKU uniqueness | Per product+store (not globally) | | |
| | Handle uniqueness | Per store | | |
| 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" | |
| custom.size_chart: "XS,S,M,L,XL,2XL" | |
| 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" | |
| custom.vendor_partnumber: "CS2083-XS" | |
| - 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" | |
| custom.vendor_partnumber: "CS2083-SM" | |
| - 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) | | |
| | 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` | | |
| ## Variant Metafields | |
| POS entries support an optional `variant_metafields` map for variant-level metafields. | |
| Only allowed keys are accepted (currently: `custom.vendor_partnumber`). Disallowed keys | |
| return a validation error. The `tax_code` field is automatically mapped to the | |
| `vertex.product_class` variant metafield. | |
| ## Default Values | |
| | Field | Default | Condition | | |
| |-------|---------|-----------| | |
| | 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" | |
| variant_metafields: | |
| custom.vendor_partnumber: "VPN-SUPPLIES-001" | |
| - 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" | |
| variant_metafields: | |
| custom.vendor_partnumber: "VPN-APPAREL-001" | |
| 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" | | |
| | pre_order mutation | `inventorySetQuantities` (absolute SET at PreOrder location) | | |
| | pre_order mapping | pre_order → "on_hand" at PreOrder {store_number} location | | |
| | 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) | | |
| ### Preorder Support (Initial) — gated by `FF_PREORDER_TAG_FLAG` | |
| When the store has a PreOrder location (`has_preorder_location=true`) and the feature flag is enabled: | |
| | Rule | Details | | |
| |------|---------| | |
| | PreOrder location | `pre_order` quantity set at "PreOrder {store_number}" location via `inventorySetQuantities` | | |
| | Product tag | `Pre-Order` tag added to product if any variant has `pre_order > 0`, removed otherwise | | |
| | Variant metafield | `custom.pre_order_flag` set to `true` when `on_order >= 1`, `false` otherwise | | |
| | Non-preorder stores | `pre_order` field silently ignored when store has no PreOrder location | | |
| | PreOrder location absent | If "PreOrder {store_number}" not found in DB, preorder entries skipped (warning logged) | | |
| --- | |
| ## Delta Inventory (`mode: delta`) | |
| ### Processing Rules | |
| | Rule | Details | | |
| |------|---------| | |
| | Mutation | `inventoryAdjustQuantities` (delta ADJUST) | | |
| | on_hand | Adjusts quantity_name "available" | | |
| | on_order | Adjusts quantity_name "incoming" | | |
| | pre_order | Adjusts at "PreOrder {store_number}" location (quantity_name "available") | | |
| | Quantity direction | Positive (increase) or negative (decrease) | | |
| | No local tracking | No local quantity state maintained | | |
| ### 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 | | |
| |------------|---------| | |
| | No drift detection | No local inventory state tracking | | |
| | No validation | Cannot validate final quantity result | | |
| | Location status | Location must exist and be active | | |
| ### Preorder Support (Delta) — gated by `FF_PREORDER_TAG_FLAG` | |
| When the store has a PreOrder location and the feature flag is enabled: | |
| | Rule | Details | | |
| |------|---------| | |
| | PreOrder location | `pre_order` delta adjusted at "PreOrder {store_number}" location via `inventoryAdjustQuantities` (quantity_name "available") | | |
| | pre_order=0 | Skipped — no PreOrder location adjust (adjust by 0 is a no-op) | | |
| | Product tag | `Pre-Order` tag added/removed based on aggregated pre_order state across variants | | |
| | Variant metafield | `custom.pre_order_flag` set based on projected on_order: queries Shopify for current `incoming`, computes `current + delta >= 1` | | |
| | Shopify query failure | Graceful degradation — inventory deltas still applied, flag/tag entries skipped | | |
| | Non-preorder stores | `pre_order` field silently ignored | | |
| 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" | |
| on_hand: 5 | |
| - entry_id: "inv-002" | |
| data: | |
| sku: "CBS-M-BLU" | |
| on_hand: -2 | |
| initialSet: | |
| summary: Set absolute inventory levels | |
| value: | |
| options: | |
| mode: "initial" | |
| entries: | |
| - entry_id: "inv-003" | |
| data: | |
| sku: "CBS-S-BLU" | |
| location_name: "Main Warehouse" | |
| on_hand: 100 | |
| on_order: 50 | |
| preorderInitial: | |
| summary: Initial inventory with preorder quantities | |
| value: | |
| options: | |
| mode: "initial" | |
| entries: | |
| - entry_id: "inv-005" | |
| data: | |
| sku: "CBS-S-BLU" | |
| on_hand: 100 | |
| on_order: 5 | |
| pre_order: 3 | |
| preorderDelta: | |
| summary: Delta inventory with preorder adjustment | |
| value: | |
| options: | |
| mode: "delta" | |
| entries: | |
| - entry_id: "inv-006" | |
| data: | |
| sku: "CBS-S-BLU" | |
| on_hand: 10 | |
| on_order: 3 | |
| pre_order: 2 | |
| 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/{store_number}: | |
| post: | |
| operationId: ingestPrices | |
| summary: Ingest price updates | |
| parameters: | |
| - $ref: '#/components/parameters/StoreNumber' | |
| 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 | | |
| | Explicit compare_at_price | When provided, overrides auto-derivation from price_type | | |
| | 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 | | |
| | Cost sync | Optional `cost` field syncs to Shopify via `inventoryItemUpdate` mutation | | |
| | Cost non-critical | Cost sync failure does not fail the price update | | |
| ## 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 | | |
| > **Note:** When `compare_at_price` is explicitly provided in the request, it overrides | |
| > the auto-derivation behavior above regardless of `price_type`. | |
| ## 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, price_type, AND compare_at_price 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 | | |
| | Negative compare_at_price | 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" | |
| price: 44.99 | |
| price_type: "R" | |
| saleWithCost: | |
| summary: Sale price with cost update | |
| value: | |
| entries: | |
| - entry_id: "price-002" | |
| data: | |
| sku: "CBS-S-BLU" | |
| price: 34.99 | |
| cost: 15.50 | |
| price_type: "S" | |
| clearanceSaleMultiStore: | |
| summary: Clearance sale with child_store routing | |
| value: | |
| entries: | |
| - entry_id: "price-003" | |
| data: | |
| sku: "000900027" | |
| child_store: "2299" | |
| price: 39.99 | |
| cost: 18.50 | |
| price_type: "CS" | |
| saleWithCompareAtPrice: | |
| summary: Sale price with explicit compare_at_price | |
| value: | |
| entries: | |
| - entry_id: "price-004" | |
| data: | |
| sku: "013004838" | |
| price: 19.99 | |
| compare_at_price: 29.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' | |
| # NOTE: The /api/v1/jobs/* endpoints are stubs (return 501 Not Implemented). | |
| # Job status polling and results retrieval use the admin import-jobs endpoints below. | |
| # The async ingest response (202) returns links pointing to: | |
| # - status: /api/v1/admin/import-jobs/{id} | |
| # - results: /api/v1/admin/import-jobs/{id}/results | |
| # ========================================================================= | |
| # Admin Endpoints | |
| # ========================================================================= | |
| /api/v1/admin/inventory/{store_number}: | |
| get: | |
| operationId: getVariantInventory | |
| summary: Get live variant inventory by SKU | |
| parameters: | |
| - $ref: '#/components/parameters/StoreNumber' | |
| - name: sku | |
| in: query | |
| required: true | |
| description: | | |
| SKU to look up. A SKU can map to multiple variants per store (duplicate SKUs); | |
| all matching variants are returned. Matched as provided (no 9-digit padding). | |
| schema: | |
| type: string | |
| examples: | |
| default: | |
| value: "000844035" | |
| - name: location | |
| in: query | |
| required: false | |
| description: | | |
| Optional location **name** (as stored for the store) to restrict results to a | |
| single location. Resolved to a Shopify Location GID before the lookup and applied | |
| server-side. When omitted, every location the item is stocked at is returned. | |
| schema: | |
| type: string | |
| examples: | |
| default: | |
| value: "9975" | |
| description: | | |
| Return **live** inventory for every variant matching a SKU within a store, | |
| optionally filtered to a single location. | |
| Inventory quantities are **not** stored locally (Shopify is the source of truth | |
| since `inventory_levels` was dropped), so this endpoint reads live from Shopify via | |
| a single `inventoryItems` GraphQL query. Because a SKU can map to multiple variants | |
| per store, all matching items are returned. | |
| ## Authentication | |
| Requires the `read:inventory` scope (the admin key is also accepted). Read-only | |
| consumers can be granted this scope without any write access. Rate-limited like the | |
| ingestion endpoints. | |
| ## Behavior | |
| | Step | Details | | |
| |------|---------| | |
| | Store resolution | `store_number` resolved from the local database | | |
| | Location resolution | When `location` is given, the name is resolved to a Shopify Location GID (local DB) and applied as a server-side filter | | |
| | Inventory read | One live `inventoryItems` query to Shopify (no local quantity state) | | |
| | Duplicate SKU support | All variants sharing the SKU are returned (see `meta.duplicate_sku_count`) | | |
| | Quantities | Six buckets per location: `available`, `on_hand`, `committed`, `incoming`, `reserved`, `safety_stock` (signed — `available` can be negative when oversold) | | |
| ## Status Codes | |
| | Status | Condition | | |
| |--------|-----------| | |
| | `200` | Inventory found. Also returned with an empty `locations` array for a variant not stocked at the filtered location | | |
| | `400` | `sku` query parameter missing or empty | | |
| | `401` | Missing/invalid Authorization header or invalid API key | | |
| | `403` | API key lacks the `read:inventory` scope | | |
| | `404` | Store not found, location not found for store, or SKU matched no inventory items | | |
| | `429` | Rate limit exceeded | | |
| | `502` | Failed to reach Shopify, or the inventory query failed | | |
| ## Limitations | |
| | Limitation | Details | | |
| |------------|---------| | |
| | Live read | No local inventory cache; every request hits Shopify | | |
| | Location filter | `location` must be an existing location **name** for the store | | |
| | SKU matching | Matched as provided (no 9-digit padding) | | |
| | Upstream timeout | 10s timeout on the Shopify query | | |
| tags: | |
| - Inventory | |
| responses: | |
| '200': | |
| description: Inventory retrieved successfully | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/VariantInventoryResponse' | |
| examples: | |
| duplicateSKU: | |
| summary: SKU mapped to multiple variants (no location filter) | |
| value: | |
| data: | |
| - inventory_item_id: "gid://shopify/InventoryItem/48882985435393" | |
| sku: "000844035" | |
| tracked: true | |
| duplicate_sku_count: 1 | |
| variant_id: "gid://shopify/ProductVariant/46769643880705" | |
| variant_title: "AUTO WINDSOCK 1-2C/WHITE/ - White" | |
| locations: | |
| - location_id: "gid://shopify/Location/85819195649" | |
| location_name: "9975" | |
| quantities: | |
| available: 8 | |
| on_hand: 8 | |
| committed: 0 | |
| incoming: 1 | |
| reserved: 0 | |
| safety_stock: 0 | |
| updated_at: "2026-05-11T15:25:56Z" | |
| - inventory_item_id: "gid://shopify/InventoryItem/48885539832065" | |
| sku: "000844035" | |
| tracked: true | |
| duplicate_sku_count: 1 | |
| variant_id: "gid://shopify/ProductVariant/46772190970113" | |
| variant_title: "AUTO WINDSOCK 1-2C/WHITE/ - White" | |
| locations: | |
| - location_id: "gid://shopify/Location/85819195649" | |
| location_name: "9975" | |
| quantities: | |
| available: 8 | |
| on_hand: 8 | |
| committed: 0 | |
| incoming: 1 | |
| reserved: 0 | |
| safety_stock: 0 | |
| updated_at: "2026-05-11T15:25:56Z" | |
| meta: | |
| store_number: "9975" | |
| sku: "000844035" | |
| location: null | |
| item_count: 2 | |
| duplicate_sku_count: 1 | |
| fetched_at: "2026-06-15T20:52:17Z" | |
| locationFiltered: | |
| summary: Location filter (location=9975) — one location returned per variant | |
| value: | |
| data: | |
| - inventory_item_id: "gid://shopify/InventoryItem/48882985435393" | |
| sku: "000844035" | |
| tracked: true | |
| duplicate_sku_count: 1 | |
| variant_id: "gid://shopify/ProductVariant/46769643880705" | |
| variant_title: "AUTO WINDSOCK 1-2C/WHITE/ - White" | |
| locations: | |
| - location_id: "gid://shopify/Location/85819195649" | |
| location_name: "9975" | |
| quantities: | |
| available: 8 | |
| on_hand: 8 | |
| committed: 0 | |
| incoming: 1 | |
| reserved: 0 | |
| safety_stock: 0 | |
| updated_at: "2026-05-11T15:25:56Z" | |
| - inventory_item_id: "gid://shopify/InventoryItem/48885539832065" | |
| sku: "000844035" | |
| tracked: true | |
| duplicate_sku_count: 1 | |
| variant_id: "gid://shopify/ProductVariant/46772190970113" | |
| variant_title: "AUTO WINDSOCK 1-2C/WHITE/ - White" | |
| locations: | |
| - location_id: "gid://shopify/Location/85819195649" | |
| location_name: "9975" | |
| quantities: | |
| available: 8 | |
| on_hand: 8 | |
| committed: 0 | |
| incoming: 1 | |
| reserved: 0 | |
| safety_stock: 0 | |
| updated_at: "2026-05-11T15:25:56Z" | |
| meta: | |
| store_number: "9975" | |
| sku: "000844035" | |
| location: "9975" | |
| item_count: 2 | |
| duplicate_sku_count: 1 | |
| fetched_at: "2026-06-15T22:27:25Z" | |
| '400': | |
| description: Missing required query parameter | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ErrorResponse' | |
| examples: | |
| missingSku: | |
| value: | |
| error: "sku query parameter is required" | |
| '401': | |
| $ref: '#/components/responses/Unauthorized' | |
| '403': | |
| $ref: '#/components/responses/Forbidden' | |
| '404': | |
| description: Store, location, or inventory not found | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ErrorResponse' | |
| examples: | |
| storeNotFound: | |
| summary: Store does not exist | |
| value: | |
| error: "store not found" | |
| locationNotFound: | |
| summary: Location name not found for store | |
| value: | |
| error: "location not found for store" | |
| noInventory: | |
| summary: SKU matched no inventory items | |
| value: | |
| error: "no inventory found for store_number+sku" | |
| '429': | |
| $ref: '#/components/responses/RateLimitExceeded' | |
| '502': | |
| $ref: '#/components/responses/BadGateway' | |
| /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 outcomes | |
| description: | | |
| Retrieve the processing outcomes for an import job. The response shape | |
| depends on the job's `source`. | |
| ## API-sourced jobs (`source: api`) | |
| Returns a per-entry rollup keyed by the client-provided `entry_id` | |
| (`ImportJobEntriesResponse`): | |
| - **`entries`** — one row per submitted entry, in `line_number` order, | |
| carrying `status` (`success`, `error`, `pending`, `skipped`, or | |
| `unknown`) and, for failed entries, a minimal `errors` array | |
| (`error_type`, `error_code`, `error_message`). | |
| - **`entries_pagination`** — paginates the `entries` array via | |
| `entries_limit` / `entries_offset`. | |
| Filter with `entry_id` (exact match) and/or `entry_status` | |
| (comma-separated). Unlike the error-only view below, this rollup includes | |
| successful entries, so every submitted entry is accounted for. | |
| ## File-sourced jobs | |
| Returns error-only outcomes in two collections (`ImportJobResultsResponse`): | |
| - **`import_errors`** — per-row processing errors recorded while transforming | |
| the request entries (validation, conversion, resolution, database). | |
| - **`sync_errors`** — outbox-side failures recorded while syncing the | |
| transformed records to Shopify. Defaults to `status=failed`; pass | |
| `sync_status=all` (or a comma-separated list) to widen the view. | |
| > **Important:** For file-sourced jobs this view **only returns errors**. | |
| > An empty `import_errors` array means every entry was processed without | |
| > issues; an empty `sync_errors` array means every Shopify sync completed | |
| > without failures. A response with both arrays empty indicates the job | |
| > finished end-to-end successfully — there is nothing to display. | |
| Each collection paginates independently (`limit`/`offset` for | |
| `import_errors`, `sync_limit`/`sync_offset` for `sync_errors`). | |
| tags: | |
| - Admin | |
| parameters: | |
| - name: id | |
| in: path | |
| required: true | |
| description: Import job ID | |
| schema: | |
| type: integer | |
| format: int64 | |
| examples: | |
| default: | |
| value: 27 | |
| - name: entries_limit | |
| in: query | |
| description: | | |
| Maximum `entries` per page (API-sourced jobs only). | |
| schema: | |
| type: integer | |
| minimum: 1 | |
| maximum: 500 | |
| default: 100 | |
| - name: entries_offset | |
| in: query | |
| description: | | |
| Number of `entries` to skip (API-sourced jobs only). | |
| schema: | |
| type: integer | |
| minimum: 0 | |
| default: 0 | |
| - name: entry_status | |
| in: query | |
| description: | | |
| Filter `entries` by status (API-sourced jobs only). Comma-separated | |
| list of values from `success`, `error`, `pending`, `skipped`, | |
| `unknown`. | |
| schema: | |
| type: string | |
| examples: | |
| errorsOnly: | |
| value: "error" | |
| multiple: | |
| value: "error,pending" | |
| - name: limit | |
| in: query | |
| description: Maximum import_errors per page (file-sourced jobs) | |
| schema: | |
| type: integer | |
| minimum: 1 | |
| maximum: 200 | |
| default: 50 | |
| - name: offset | |
| in: query | |
| description: Number of import_errors to skip (file-sourced jobs) | |
| schema: | |
| type: integer | |
| minimum: 0 | |
| default: 0 | |
| - name: entry_id | |
| in: query | |
| description: | | |
| Filter by client-provided entry_id (exact match). Applies to the | |
| `entries` rollup for API-sourced jobs and to `import_errors` for | |
| file-sourced jobs. | |
| schema: | |
| type: string | |
| - name: sync_limit | |
| in: query | |
| description: Maximum sync_errors per page | |
| schema: | |
| type: integer | |
| minimum: 1 | |
| maximum: 200 | |
| default: 50 | |
| - name: sync_offset | |
| in: query | |
| description: Number of sync_errors to skip | |
| schema: | |
| type: integer | |
| minimum: 0 | |
| default: 0 | |
| - name: sync_status | |
| in: query | |
| description: | | |
| Filter sync_errors by outbox status. Comma-separated list of values | |
| from `pending`, `processing`, `processed`, `failed`. Pass `all` to | |
| disable the filter. Defaults to `failed`. | |
| schema: | |
| type: string | |
| examples: | |
| default: | |
| value: "failed" | |
| all: | |
| value: "all" | |
| multiple: | |
| value: "failed,pending" | |
| - name: sync_error_type | |
| in: query | |
| description: Filter sync_errors by outbox error_type (comma-separated) | |
| schema: | |
| type: string | |
| - name: sync_error_code | |
| in: query | |
| description: Filter sync_errors by outbox error_code (comma-separated) | |
| schema: | |
| type: string | |
| responses: | |
| '200': | |
| description: | | |
| Outcomes retrieved successfully. API-sourced jobs return the | |
| `ImportJobEntriesResponse` per-entry rollup; file-sourced jobs return | |
| the `ImportJobResultsResponse` error-only collections (empty arrays | |
| mean no issues). | |
| content: | |
| application/json: | |
| schema: | |
| oneOf: | |
| - $ref: '#/components/schemas/ImportJobEntriesResponse' | |
| - $ref: '#/components/schemas/ImportJobResultsResponse' | |
| examples: | |
| entries: | |
| summary: API-sourced job — per-entry rollup | |
| value: | |
| entries: | |
| - entry_id: "hp-2434-025292194-1782208810-41" | |
| line_number: 41 | |
| status: "success" | |
| - entry_id: "hp-2434-029185670-1782208810-26" | |
| line_number: 26 | |
| status: "error" | |
| errors: | |
| - error_type: "resolution" | |
| error_code: "variant_resolve_failed" | |
| error_message: "Child store filtering failed for SKU 029185670: child_store metafield '2431' not found on any product for SKU 029185670" | |
| entries_pagination: | |
| total: 48 | |
| limit: 100 | |
| offset: 0 | |
| noErrors: | |
| summary: Job completed without issues | |
| value: | |
| import_errors: [] | |
| pagination: | |
| limit: 50 | |
| offset: 0 | |
| total: 0 | |
| sync_errors: [] | |
| sync_pagination: | |
| limit: 50 | |
| offset: 0 | |
| total: 0 | |
| withErrors: | |
| summary: Job with row-level and sync errors | |
| value: | |
| import_errors: | |
| - id: 91234 | |
| import_job_id: 27 | |
| row_number: 5 | |
| entry_id: "product-5" | |
| result: "error" | |
| error_type: "validation" | |
| error_code: "missing_required_field" | |
| column_name: "sku" | |
| error_message: "SKU is required" | |
| created_at: "2026-05-21T10:31:00Z" | |
| pagination: | |
| limit: 50 | |
| offset: 0 | |
| total: 1 | |
| sync_errors: | |
| - id: 55021 | |
| import_job_id: 27 | |
| store_number: "9975" | |
| aggregate_type: "variant" | |
| aggregate_id: 1029836 | |
| sync_operation: "price" | |
| status: "failed" | |
| attempts: 3 | |
| max_attempts: 3 | |
| error_type: "shopify_user_error" | |
| error_code: "invalid_price" | |
| error_message: "Price must be non-negative" | |
| sku: "000900027" | |
| product_handle: "classic-blue-shirt" | |
| created_at: "2026-05-21T10:32:00Z" | |
| updated_at: "2026-05-21T10:33:15Z" | |
| processed_at: "2026-05-21T10:33:15Z" | |
| sync_pagination: | |
| limit: 50 | |
| offset: 0 | |
| total: 1 | |
| '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' | |
| /api/v1/admin/metrics/sync: | |
| get: | |
| operationId: getSyncMetrics | |
| summary: Get sync-view system health snapshot | |
| description: | | |
| Point-in-time health snapshot of the outbound sync pipeline: connection | |
| pools, outbox processing (global and per store), and per-store Shopify | |
| leaky-bucket capacity. | |
| This is the canonical sync view. When disabled by configuration it returns | |
| `501`; the deprecated `GET /api/v1/admin/metrics` alias returns the same | |
| payload and is always available. | |
| Responses may be served from a short-lived cache. Within the cache window a | |
| scrape returns the last snapshot; if a refresh fails or times out, the last | |
| successful snapshot is returned (a `200` carrying its own `timestamp`, so | |
| staleness is visible). A `500` is returned only when no snapshot is | |
| available. | |
| tags: | |
| - Admin | |
| responses: | |
| '200': | |
| description: Sync-view snapshot | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/SyncMetricsResponse' | |
| '401': | |
| $ref: '#/components/responses/Unauthorized' | |
| '500': | |
| $ref: '#/components/responses/InternalServerError' | |
| '501': | |
| description: Endpoint disabled by configuration | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ErrorResponse' | |
| examples: | |
| disabled: | |
| value: | |
| error: "metrics/sync endpoint is not enabled" | |
| /api/v1/admin/metrics: | |
| get: | |
| operationId: getMetricsAlias | |
| deprecated: true | |
| summary: Get sync-view system health snapshot (deprecated alias) | |
| description: | | |
| Deprecated alias of `GET /api/v1/admin/metrics/sync`. Resolves to the same | |
| handler and returns an identical `SyncMetricsResponse`. Unlike | |
| `/metrics/sync`, this alias is never gated, so it keeps serving existing | |
| clients. New clients should use `/metrics/sync`. | |
| tags: | |
| - Admin | |
| responses: | |
| '200': | |
| description: Sync-view snapshot | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/SyncMetricsResponse' | |
| '401': | |
| $ref: '#/components/responses/Unauthorized' | |
| '500': | |
| $ref: '#/components/responses/InternalServerError' | |
| /api/v1/admin/metrics/ingestion: | |
| get: | |
| operationId: getIngestionMetrics | |
| summary: Get ingestion-view system health snapshot | |
| description: | | |
| Point-in-time health snapshot of the inbound ingestion pipeline: | |
| connection pools, real-time staging health (global and per store), and an | |
| import-job lifecycle summary. | |
| Staging metrics come from `staging_records` and are a short-window, | |
| real-time signal (throughput over the last 5 minutes, errors over the last | |
| hour); staging rows are deleted after retention, so these are not | |
| historical totals. Job metrics come from `import_jobs` and aggregate | |
| globally by status. | |
| A failing sub-query degrades gracefully: the affected section is omitted | |
| and the endpoint still returns `200` with everything that succeeded (an | |
| all-queries-fail case still returns `pools` and an empty `ingestion`). | |
| There is no fail-hard path; this endpoint always returns `200`. | |
| tags: | |
| - Admin | |
| responses: | |
| '200': | |
| description: Ingestion-view snapshot | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/IngestionMetricsResponse' | |
| examples: | |
| active: | |
| summary: One active store with in-flight staging and jobs | |
| value: | |
| timestamp: "2026-07-02T12:00:00Z" | |
| pools: | |
| api: | |
| database: | |
| max_open_connections: 25 | |
| open_connections: 3 | |
| in_use: 1 | |
| idle: 2 | |
| wait_count: 0 | |
| wait_duration_ms: 0 | |
| redis: | |
| total_conns: 4 | |
| idle_conns: 3 | |
| stale_conns: 0 | |
| hits: 120 | |
| misses: 5 | |
| timeouts: 0 | |
| config: | |
| db_max_open_conns: 25 | |
| redis_pool_size: 10 | |
| concurrency: 0 | |
| operation_concurrency: 0 | |
| batch_size: 0 | |
| ingestion: | |
| staging: | |
| global: | |
| pending: | |
| PRODUCT: 1240 | |
| PRICE: 300 | |
| processing: | |
| PRODUCT: 512 | |
| oldest_pending_seconds: 47 | |
| throughput_per_second: 830.5 | |
| error_last_hour: 12 | |
| by_store: | |
| "1234": | |
| pending: | |
| PRODUCT: 1240 | |
| processing: | |
| PRODUCT: 512 | |
| oldest_pending_seconds: 47 | |
| throughput_per_second: 830.5 | |
| error_last_hour: 12 | |
| jobs: | |
| in_flight: | |
| pending: 2 | |
| parsing: 1 | |
| staged: 3 | |
| processing: 5 | |
| oldest_in_flight_seconds: | |
| parsing: 12 | |
| staged: 90 | |
| processing: 320 | |
| completed_last_hour: 44 | |
| completed_with_errors_last_hour: 3 | |
| failed_last_hour: 1 | |
| '401': | |
| $ref: '#/components/responses/Unauthorized' | |
| 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: | |
| StoreNumber: | |
| name: store_number | |
| in: path | |
| required: true | |
| description: Store number for routing the request | |
| schema: | |
| type: string | |
| examples: | |
| default: | |
| value: "9975" | |
| XRequestIDHeader: | |
| name: X-Request-ID | |
| in: header | |
| required: false | |
| description: | | |
| Optional caller-supplied identifier for this HTTP call. If omitted, | |
| or if the value does not match `^[A-Za-z0-9_-]{1,64}$`, the server | |
| generates a UUIDv7. The chosen value is always echoed on the | |
| `X-Request-ID` response header. Applies to every endpoint in this | |
| API; see the introduction's "Request Correlation" section for the | |
| full contract and the persistence behavior for ingestion endpoints. | |
| schema: | |
| type: string | |
| pattern: '^[A-Za-z0-9_-]{1,64}$' | |
| maxLength: 64 | |
| examples: | |
| uuidv7: | |
| summary: Server-format (also produced when no header is supplied) | |
| value: "0190a5b8-c4e2-7b34-b234-d56789abcdef" | |
| opaque: | |
| summary: Caller-supplied opaque token (any UUID, ULID, or trace ID) | |
| value: "trace-7f8a92b1" | |
| RequestIDHeaderFallback: | |
| name: Request-Id | |
| in: header | |
| required: false | |
| description: | | |
| Fallback inbound header (RFC-draft-aligned; RFC 6648 drops the X- | |
| prefix). Read only when `X-Request-ID` is absent. Same validation | |
| rules as `X-Request-ID`. The response always uses `X-Request-ID` | |
| — never `Request-Id` — so callers don't have to disambiguate | |
| between two response headers. | |
| schema: | |
| type: string | |
| pattern: '^[A-Za-z0-9_-]{1,64}$' | |
| maxLength: 64 | |
| headers: | |
| XRequestIDResponse: | |
| description: | | |
| Correlation identifier for this HTTP call. Echoes the caller's | |
| `X-Request-ID` (or `Request-Id`) when one was supplied, otherwise | |
| a server-generated UUIDv7. Always present on every response, | |
| including 4xx/5xx — useful for support tickets where the caller | |
| already has the value before the body was parsed. | |
| schema: | |
| type: string | |
| pattern: '^[A-Za-z0-9_-]{1,64}$' | |
| example: "0190a5b8-c4e2-7b34-b234-d56789abcdef" | |
| 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 | |
| gift_card: | |
| type: boolean | |
| description: >- | |
| Whether the product is a gift card. Can only be set at creation; | |
| Shopify does not allow changing it on existing products. On updates | |
| the field is accepted but ignored for the Shopify sync: the product | |
| keeps its original gift card status and no error is returned. | |
| default: false | |
| gift_card_template_suffix: | |
| type: | |
| - string | |
| - "null" | |
| description: >- | |
| Theme template suffix used when customers view the gift card | |
| (renders templates/gift_card.<suffix>.liquid). Only applies when | |
| gift_card is true. Unlike gift_card, it can be changed on updates. | |
| Empty or whitespace-only values are treated as absent and fall back | |
| to the default. | |
| maxLength: 255 | |
| default: "gift_card" | |
| examples: | |
| - "birthday" | |
| 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" | |
| custom.size_chart: "S,M,L,XL,XXL" | |
| 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. For gift card products | |
| (gift_card: true) Shopify requires non-taxable variants: the | |
| default becomes false and sending true is rejected with a | |
| validation error. The import job results endpoint records it | |
| with error_code `gift_card_taxable`. | |
| 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 | |
| - `custom.size_chart` - Size chart (single line text) | |
| additionalProperties: | |
| type: string | |
| variant_metafields: | |
| type: | |
| - object | |
| - "null" | |
| description: | | |
| Variant-level metafields as namespace.key to value mapping. | |
| Only allowed keys are accepted. Currently allowed: | |
| - `custom.vendor_partnumber` - Vendor part number for the variant | |
| Disallowed keys return a validation error. | |
| The `tax_code` field is automatically mapped to `vertex.product_class` | |
| and does not need to be included here. | |
| 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 | |
| description: | | |
| Inventory update data. Store number is provided via URL path parameter. | |
| required: | |
| - sku | |
| properties: | |
| sku: | |
| type: string | |
| description: Stock keeping unit | |
| maxLength: 255 | |
| 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 | |
| pre_order: | |
| type: integer | |
| description: | | |
| Preorder quantity. Only processed when the store has a PreOrder location configured. | |
| - **Initial mode**: Sets absolute quantity at the "PreOrder {store_number}" location. | |
| - **Delta mode**: Adjusts quantity at the PreOrder location. Skipped when value is 0. | |
| - When `FF_PREORDER_TAG_FLAG` is enabled, also drives: | |
| - `Pre-Order` product tag (added when any variant has pre_order > 0) | |
| - `custom.pre_order_flag` variant metafield (true when on_order >= 1) | |
| - Silently ignored for stores without a PreOrder location. | |
| 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 | |
| description: | | |
| Price update data. Store number is provided via URL path parameter. | |
| required: | |
| - sku | |
| - price | |
| properties: | |
| sku: | |
| type: string | |
| description: Stock keeping unit | |
| maxLength: 255 | |
| child_store: | |
| type: | |
| - string | |
| - "null" | |
| description: Child store for variant routing. Defaults to parent store_number. | |
| maxLength: 50 | |
| price: | |
| type: number | |
| format: double | |
| description: New price value | |
| minimum: 0 | |
| cost: | |
| type: | |
| - number | |
| - "null" | |
| format: double | |
| description: | | |
| Cost of goods (unit cost). Optional. When provided, the cost is synced | |
| to Shopify via `inventoryItemUpdate` after the price update succeeds. | |
| If omitted, the existing cost in Shopify remains unchanged. | |
| minimum: 0 | |
| examples: | |
| - 15.50 | |
| 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 | |
| When `compare_at_price` is explicitly provided, it overrides the | |
| auto-derivation described above. | |
| enum: | |
| - R | |
| - S | |
| - C | |
| - CS | |
| default: "R" | |
| compare_at_price: | |
| type: | |
| - number | |
| - "null" | |
| format: double | |
| description: | | |
| Explicit compare-at price (displayed as strikethrough in Shopify). | |
| When provided, overrides the automatic compare_at_price derivation | |
| from price_type transitions. If omitted, compare_at_price is | |
| auto-derived based on price_type. | |
| minimum: 0 | |
| examples: | |
| - 29.99 | |
| IngestOptions: | |
| type: object | |
| properties: | |
| force_sync: | |
| type: boolean | |
| description: | | |
| Create outbox entries even for unchanged data (re-syncs to Shopify | |
| regardless of change detection). | |
| When omitted, the server default applies — controlled by the | |
| `FORCE_SYNC` / `processing.force_sync` configuration, which also | |
| governs file-based processing. When provided, the request value | |
| overrides the server default; send `false` to opt out even if the | |
| server default is `true`. | |
| > **Production note:** `FORCE_SYNC` is currently set to `true` in | |
| > production, so by default unchanged data is re-synced to Shopify. | |
| > Send `force_sync: false` on a request to skip re-syncing unchanged | |
| > records (change detection applies). | |
| 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" | |
| request_id: | |
| type: string | |
| description: | | |
| The `X-Request-ID` used for this HTTP call. Mirrors the | |
| `X-Request-ID` response header for caller convenience and | |
| for logging integrations that consume the JSON body. | |
| examples: | |
| - "0190a5b8-c4e2-7b34-b234-d56789abcdef" | |
| 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" | |
| request_id: | |
| type: string | |
| description: | | |
| The `X-Request-ID` used for this HTTP call. Mirrors the | |
| `X-Request-ID` response header. | |
| examples: | |
| - "0190a5b8-c4e2-7b34-b234-d56789abcdef" | |
| 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/admin/import-jobs/27" | |
| results: | |
| type: string | |
| format: uri | |
| description: URL to retrieve results when complete | |
| examples: | |
| - "/api/v1/admin/import-jobs/27/results" | |
| 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 | |
| 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' | |
| 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" | |
| # ========================================================================= | |
| # 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) | |
| request_id: | |
| type: string | |
| description: | | |
| The `X-Request-ID` of the GET request that produced this | |
| response. Distinct from `created_by_request_id`, which is the | |
| `X-Request-ID` of the original POST whose execution produced | |
| this `import_job` row. | |
| examples: | |
| - "0190a5b8-c4e2-7b34-b234-d56789abcdef" | |
| created_by_request_id: | |
| type: string | |
| description: | | |
| The `X-Request-ID` of the call whose execution produced this | |
| `import_job` row. Sourced from the audit trail | |
| (`import_job_request_ids`) as the oldest entry for this job. | |
| Omitted when the job has no audit entries (e.g. jobs created | |
| before the audit table existed). | |
| examples: | |
| - "0190a5b7-9d42-7000-89ab-c0ffeed00ds1" | |
| request_ids: | |
| type: array | |
| description: | | |
| Full audit trail of `X-Request-ID` values that have hit this | |
| job, ordered by `occurred_at` ascending. Length is 1 in the | |
| common case; >1 when the caller retried with the same | |
| `Idempotency-Key` and a new `X-Request-ID`. Omitted when the | |
| job has no audit entries. | |
| items: | |
| $ref: '#/components/schemas/RequestIDHistoryEntry' | |
| created_at: | |
| type: string | |
| format: date-time | |
| updated_at: | |
| type: string | |
| format: date-time | |
| RequestIDHistoryEntry: | |
| type: object | |
| description: | | |
| One observed `X-Request-ID` recorded against an `import_job` in | |
| the `import_job_request_ids` audit table. Multiple rows per job | |
| exist when the caller retried the same `Idempotency-Key` with a | |
| new `X-Request-ID`; the oldest entry identifies the request whose | |
| execution produced the job. | |
| required: | |
| - request_id | |
| - occurred_at | |
| properties: | |
| request_id: | |
| type: string | |
| description: The X-Request-ID observed against the job. | |
| examples: | |
| - "0190a5b7-9d42-7000-89ab-c0ffeed00ds1" | |
| occurred_at: | |
| type: string | |
| format: date-time | |
| description: When this request_id was recorded against the job. | |
| ImportJobEntriesResponse: | |
| type: object | |
| description: | | |
| Per-entry rollup returned for API-sourced import jobs. One row per | |
| submitted entry, keyed by the client-provided `entry_id`, paginated via | |
| `entries_pagination`. Includes successful entries, so every submitted | |
| entry is accounted for. | |
| required: | |
| - entries | |
| - entries_pagination | |
| properties: | |
| request_id: | |
| type: string | |
| description: | | |
| The `X-Request-ID` of this HTTP call. Mirrors the response header. | |
| examples: | |
| - "0190a5b8-c4e2-7b34-b234-d56789abcdef" | |
| entries: | |
| type: array | |
| description: Per-entry outcomes, in `line_number` order. | |
| items: | |
| $ref: '#/components/schemas/EntryOutcome' | |
| entries_pagination: | |
| type: object | |
| description: Pagination for `entries` | |
| properties: | |
| limit: | |
| type: integer | |
| offset: | |
| type: integer | |
| total: | |
| type: integer | |
| EntryOutcome: | |
| type: object | |
| description: | | |
| A single entry's processing outcome, keyed by the client-provided | |
| `entry_id`. The `errors` array is present only for failed entries. | |
| required: | |
| - entry_id | |
| - status | |
| properties: | |
| entry_id: | |
| type: string | |
| description: Client-provided identifier echoed back from the request. | |
| examples: | |
| - "hp-2434-029185670-1782208810-26" | |
| line_number: | |
| type: integer | |
| description: 1-based position of the entry in the submitted batch. | |
| examples: | |
| - 26 | |
| status: | |
| type: string | |
| description: Aggregated outcome for the entry. | |
| enum: | |
| - success | |
| - error | |
| - pending | |
| - skipped | |
| - unknown | |
| errors: | |
| type: array | |
| description: | | |
| Minimal per-error detail. Present only when `status` is `error`. | |
| items: | |
| $ref: '#/components/schemas/EntryOutcomeError' | |
| EntryOutcomeError: | |
| type: object | |
| description: | | |
| Minimal error detail for a failed entry — exactly three string fields. | |
| required: | |
| - error_type | |
| - error_code | |
| - error_message | |
| properties: | |
| error_type: | |
| type: string | |
| examples: | |
| - "resolution" | |
| error_code: | |
| type: string | |
| examples: | |
| - "variant_resolve_failed" | |
| error_message: | |
| type: string | |
| examples: | |
| - "Child store filtering failed for SKU 029185670: child_store metafield '2431' not found on any product for SKU 029185670" | |
| ImportJobResultsResponse: | |
| type: object | |
| description: | | |
| Errors-only response for the import job results endpoint. Two independently | |
| paginated collections: `import_errors` (per-row processing errors) and | |
| `sync_errors` (outbox/Shopify sync failures). Empty arrays mean no issues | |
| were recorded for that stage. | |
| required: | |
| - import_errors | |
| - pagination | |
| - sync_errors | |
| - sync_pagination | |
| properties: | |
| import_errors: | |
| type: array | |
| description: | | |
| Per-row processing errors recorded while transforming entries. | |
| Empty array means every entry processed without issues. | |
| items: | |
| $ref: '#/components/schemas/ImportResult' | |
| pagination: | |
| type: object | |
| description: Pagination for `import_errors` | |
| properties: | |
| limit: | |
| type: integer | |
| offset: | |
| type: integer | |
| total: | |
| type: integer | |
| sync_errors: | |
| type: array | |
| description: | | |
| Outbox-side failures recorded while syncing transformed records to | |
| Shopify. Empty array means every Shopify sync completed successfully. | |
| items: | |
| $ref: '#/components/schemas/SyncError' | |
| sync_pagination: | |
| type: object | |
| description: Pagination for `sync_errors` | |
| properties: | |
| limit: | |
| type: integer | |
| offset: | |
| type: integer | |
| total: | |
| type: integer | |
| ImportResult: | |
| type: object | |
| description: | | |
| A single per-row processing outcome. In practice only error and skipped | |
| rows are persisted, so this schema's `result` will almost always be | |
| `error` (or `skipped`) when returned by `import_errors`. | |
| 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 | |
| result: | |
| type: string | |
| description: Outcome status. Typically `error` in errors-only responses. | |
| 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_code: | |
| type: | |
| - string | |
| - "null" | |
| description: Machine-readable error code (e.g. `missing_required_field`, `gift_card_taxable`) | |
| column_name: | |
| type: | |
| - string | |
| - "null" | |
| description: Column or field that triggered the error | |
| column_value: | |
| type: | |
| - string | |
| - "null" | |
| description: Offending value (when applicable) | |
| error_message: | |
| type: | |
| - string | |
| - "null" | |
| created_at: | |
| type: string | |
| format: date-time | |
| SyncError: | |
| type: object | |
| description: | | |
| A single outbox-side sync failure (Shopify mutation attempt) linked to | |
| an import job. Distinct from `ImportResult` — that one captures the | |
| per-row transform outcome; this one captures the per-aggregate Shopify | |
| sync outcome. | |
| properties: | |
| id: | |
| type: integer | |
| format: int64 | |
| import_job_id: | |
| type: | |
| - integer | |
| - "null" | |
| format: int64 | |
| store_number: | |
| type: string | |
| aggregate_type: | |
| type: string | |
| enum: | |
| - product | |
| - variant | |
| aggregate_id: | |
| type: integer | |
| format: int64 | |
| 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_type: | |
| type: | |
| - string | |
| - "null" | |
| error_code: | |
| type: | |
| - string | |
| - "null" | |
| error_message: | |
| type: | |
| - string | |
| - "null" | |
| sku: | |
| type: | |
| - string | |
| - "null" | |
| description: Variant SKU (only for variant-level syncs) | |
| product_handle: | |
| type: | |
| - string | |
| - "null" | |
| description: Product handle (for products) or parent product handle (for variants) | |
| created_at: | |
| type: string | |
| format: date-time | |
| updated_at: | |
| type: string | |
| format: date-time | |
| processed_at: | |
| type: | |
| - string | |
| - "null" | |
| 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: | |
| request_id: | |
| type: string | |
| description: | | |
| The `X-Request-ID` of this HTTP call. Mirrors the response header. | |
| examples: | |
| - "0190a5b8-c4e2-7b34-b234-d56789abcdef" | |
| errors: | |
| type: array | |
| items: | |
| $ref: '#/components/schemas/ImportResult' | |
| total: | |
| type: integer | |
| # ========================================================================= | |
| # Variant Inventory Response Schemas | |
| # ========================================================================= | |
| VariantInventoryResponse: | |
| type: object | |
| required: | |
| - data | |
| - meta | |
| properties: | |
| data: | |
| type: array | |
| description: | | |
| One entry per Shopify inventory item matching the SKU. A SKU can map to | |
| multiple variants per store, so this may contain more than one item. | |
| items: | |
| $ref: '#/components/schemas/InventoryVariant' | |
| meta: | |
| $ref: '#/components/schemas/InventoryResponseMeta' | |
| InventoryVariant: | |
| type: object | |
| description: Live inventory for a single variant (one Shopify inventory item) matching the SKU. | |
| required: | |
| - inventory_item_id | |
| - sku | |
| - tracked | |
| - duplicate_sku_count | |
| - variant_id | |
| - variant_title | |
| - locations | |
| properties: | |
| inventory_item_id: | |
| type: string | |
| description: Shopify InventoryItem GID | |
| examples: | |
| - "gid://shopify/InventoryItem/48882985435393" | |
| sku: | |
| type: string | |
| examples: | |
| - "000844035" | |
| tracked: | |
| type: boolean | |
| description: Whether Shopify tracks inventory for this item | |
| duplicate_sku_count: | |
| type: integer | |
| description: Number of variants sharing this SKU within the store | |
| variant_id: | |
| type: string | |
| description: Shopify ProductVariant GID | |
| examples: | |
| - "gid://shopify/ProductVariant/46769643880705" | |
| variant_title: | |
| type: string | |
| examples: | |
| - "AUTO WINDSOCK 1-2C/WHITE/ - White" | |
| locations: | |
| type: array | |
| description: | | |
| Per-location inventory. Empty when the variant is not stocked at the | |
| requested location (only possible when the `location` filter is applied). | |
| items: | |
| $ref: '#/components/schemas/InventoryLocation' | |
| InventoryLocation: | |
| type: object | |
| required: | |
| - location_id | |
| - location_name | |
| - quantities | |
| - updated_at | |
| properties: | |
| location_id: | |
| type: string | |
| description: Shopify Location GID | |
| examples: | |
| - "gid://shopify/Location/85819195649" | |
| location_name: | |
| type: string | |
| examples: | |
| - "9975" | |
| quantities: | |
| $ref: '#/components/schemas/InventoryQuantities' | |
| updated_at: | |
| type: string | |
| format: date-time | |
| description: When Shopify last updated this location's inventory | |
| InventoryQuantities: | |
| type: object | |
| description: | | |
| The six Shopify inventory states. Values are signed integers — `available` | |
| can be negative when a location is oversold. Missing states default to 0. | |
| required: | |
| - available | |
| - on_hand | |
| - committed | |
| - incoming | |
| - reserved | |
| - safety_stock | |
| properties: | |
| available: | |
| type: integer | |
| examples: | |
| - 8 | |
| on_hand: | |
| type: integer | |
| examples: | |
| - 8 | |
| committed: | |
| type: integer | |
| incoming: | |
| type: integer | |
| examples: | |
| - 1 | |
| reserved: | |
| type: integer | |
| safety_stock: | |
| type: integer | |
| InventoryResponseMeta: | |
| type: object | |
| description: Describes the query that produced the response. | |
| required: | |
| - store_number | |
| - sku | |
| - item_count | |
| - duplicate_sku_count | |
| - fetched_at | |
| properties: | |
| store_number: | |
| type: string | |
| examples: | |
| - "9975" | |
| sku: | |
| type: string | |
| examples: | |
| - "000844035" | |
| location: | |
| type: | |
| - string | |
| - "null" | |
| description: The location filter that was applied, or null when unfiltered | |
| examples: | |
| - "Main Warehouse" | |
| item_count: | |
| type: integer | |
| description: Number of variants returned | |
| duplicate_sku_count: | |
| type: integer | |
| description: Highest duplicate-SKU count across the returned items | |
| fetched_at: | |
| type: string | |
| format: date-time | |
| description: When the live Shopify read occurred (UTC) | |
| # ========================================================================= | |
| # Admin Metrics Response Schemas | |
| # ========================================================================= | |
| SyncMetricsResponse: | |
| type: object | |
| description: Sync-view health snapshot (GET /api/v1/admin/metrics/sync and its deprecated /metrics alias). | |
| properties: | |
| timestamp: | |
| type: string | |
| format: date-time | |
| description: When the snapshot was computed (UTC). A served-stale cached snapshot keeps its original timestamp. | |
| pools: | |
| $ref: '#/components/schemas/PoolStats' | |
| outbox: | |
| $ref: '#/components/schemas/OutboxMetrics' | |
| shopify_capacity: | |
| type: array | |
| description: Per-store Shopify leaky-bucket capacity. Only stores consuming quota or recently active are included. | |
| items: | |
| $ref: '#/components/schemas/ShopifyCapacityEntry' | |
| IngestionMetricsResponse: | |
| type: object | |
| description: Ingestion-view health snapshot (GET /api/v1/admin/metrics/ingestion). | |
| properties: | |
| timestamp: | |
| type: string | |
| format: date-time | |
| description: When the snapshot was computed (UTC). | |
| pools: | |
| $ref: '#/components/schemas/PoolStats' | |
| ingestion: | |
| $ref: '#/components/schemas/IngestionMetrics' | |
| PoolStats: | |
| type: object | |
| description: | | |
| Connection-pool stats keyed by service. Worker roles report their own | |
| stats to Redis and appear as `worker-file`, `worker-sync`, etc.; the API | |
| process reports itself under `api`; `all` is the aggregate across every | |
| service, with `all.database.max_open_connections` overridden to the actual | |
| PostgreSQL `max_connections`. | |
| additionalProperties: | |
| $ref: '#/components/schemas/PoolStatsEntry' | |
| PoolStatsEntry: | |
| type: object | |
| properties: | |
| database: | |
| $ref: '#/components/schemas/DatabasePoolStats' | |
| redis: | |
| $ref: '#/components/schemas/RedisPoolStats' | |
| config: | |
| type: object | |
| description: Per-service pool configuration. Present on `api` and worker entries; absent on `all`. | |
| additionalProperties: true | |
| updated_at: | |
| type: string | |
| format: date-time | |
| description: When this worker last published its stats to Redis. Present on worker entries only; the live `api` and `all` entries omit it. | |
| DatabasePoolStats: | |
| type: object | |
| properties: | |
| max_open_connections: | |
| type: integer | |
| open_connections: | |
| type: integer | |
| in_use: | |
| type: integer | |
| idle: | |
| type: integer | |
| wait_count: | |
| type: integer | |
| format: int64 | |
| wait_duration_ms: | |
| type: integer | |
| format: int64 | |
| RedisPoolStats: | |
| type: object | |
| properties: | |
| total_conns: | |
| type: integer | |
| idle_conns: | |
| type: integer | |
| stale_conns: | |
| type: integer | |
| hits: | |
| type: integer | |
| misses: | |
| type: integer | |
| timeouts: | |
| type: integer | |
| OutboxMetrics: | |
| type: object | |
| description: Outbox processing metrics, global and per store. | |
| properties: | |
| global: | |
| $ref: '#/components/schemas/OutboxStoreMetrics' | |
| by_store: | |
| type: object | |
| description: Per-store metrics keyed by store number. Only active stores are included (a store with only processed_last_hour activity is omitted). | |
| additionalProperties: | |
| $ref: '#/components/schemas/OutboxStoreMetrics' | |
| OutboxStoreMetrics: | |
| type: object | |
| description: Outbox metrics for one store (or the global aggregate). Zero-valued fields are omitted from the response. | |
| properties: | |
| pending: | |
| type: object | |
| description: Pending count keyed by operation (products, prices, pos, initial_inventory, delta_inventory). | |
| additionalProperties: | |
| type: integer | |
| processing: | |
| type: object | |
| description: In-flight count keyed by operation. | |
| additionalProperties: | |
| type: integer | |
| failed_last_hour: | |
| type: integer | |
| processed_last_hour: | |
| type: integer | |
| oldest_pending_seconds: | |
| type: integer | |
| description: Age (seconds) of the oldest pending row. Global is the max across stores. | |
| throughput_per_second: | |
| type: number | |
| format: double | |
| description: Rows processed in the last 5 minutes divided by 300. | |
| ShopifyCapacityEntry: | |
| type: object | |
| description: Per-store Shopify GraphQL leaky-bucket capacity. | |
| properties: | |
| store_number: | |
| type: string | |
| available_points: | |
| type: number | |
| format: double | |
| maximum_points: | |
| type: number | |
| format: double | |
| restore_rate: | |
| type: number | |
| format: double | |
| capacity_percent: | |
| type: number | |
| format: double | |
| description: available_points / maximum_points, as a percentage. | |
| last_updated: | |
| type: string | |
| format: date-time | |
| staleness_seconds: | |
| type: integer | |
| description: Seconds since this store's capacity was last observed. | |
| IngestionMetrics: | |
| type: object | |
| properties: | |
| staging: | |
| $ref: '#/components/schemas/StagingMetrics' | |
| jobs: | |
| $ref: '#/components/schemas/JobsMetrics' | |
| StagingMetrics: | |
| type: object | |
| description: Real-time staging pipeline health, global and per store. | |
| properties: | |
| global: | |
| $ref: '#/components/schemas/StagingStoreMetrics' | |
| by_store: | |
| type: object | |
| description: Per-store metrics keyed by store number. Only active stores are included (pending, processing, throughput, or errors). | |
| additionalProperties: | |
| $ref: '#/components/schemas/StagingStoreMetrics' | |
| StagingStoreMetrics: | |
| type: object | |
| description: Staging metrics for one store (or the global aggregate). Zero-valued fields are omitted from the response. | |
| properties: | |
| pending: | |
| type: object | |
| description: Pending count keyed by file type (PRODUCT, PRICE, INVENTORY_INITIAL, INVENTORY_DELTA, POS, and the API_* variants for API-sourced jobs). | |
| additionalProperties: | |
| type: integer | |
| processing: | |
| type: object | |
| description: In-flight (processing) count keyed by file type. | |
| additionalProperties: | |
| type: integer | |
| oldest_pending_seconds: | |
| type: integer | |
| description: Age (seconds) of the oldest pending row. Global is the max across stores. | |
| throughput_per_second: | |
| type: number | |
| format: double | |
| description: Records processed in the last 5 minutes divided by 300. | |
| error_last_hour: | |
| type: integer | |
| description: Records that errored in the last hour. | |
| JobsMetrics: | |
| type: object | |
| description: | | |
| Import-job lifecycle summary. import_jobs has no store column, so these | |
| aggregate globally by status. Zero-valued fields are omitted from the | |
| response. | |
| properties: | |
| in_flight: | |
| type: object | |
| description: Job count per non-terminal status (pending, parsing, staged, processing). | |
| additionalProperties: | |
| type: integer | |
| oldest_in_flight_seconds: | |
| type: object | |
| description: Age (seconds) of the oldest job per non-terminal status. | |
| additionalProperties: | |
| type: integer | |
| completed_last_hour: | |
| type: integer | |
| completed_with_errors_last_hour: | |
| type: integer | |
| failed_last_hour: | |
| 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: 100 requests per minute" | |
| retry_after: 45 | |
| Forbidden: | |
| description: Authenticated, but the API key lacks the required scope | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ErrorResponse' | |
| examples: | |
| insufficientScope: | |
| value: | |
| error: "Insufficient permissions" | |
| required_scope: "read:inventory" | |
| BadGateway: | |
| description: Upstream Shopify request failed | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/ErrorResponse' | |
| examples: | |
| unreachable: | |
| summary: Could not reach Shopify for this store | |
| value: | |
| error: "failed to reach Shopify for this store" | |
| queryFailed: | |
| summary: Inventory query failed | |
| value: | |
| error: "failed to fetch inventory from Shopify" | |
| 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