Skip to content

Instantly share code, notes, and snippets.

@acamino
Last active January 15, 2026 22:08
Show Gist options
  • Select an option

  • Save acamino/1736ed935bb97d9404fac06690855026 to your computer and use it in GitHub Desktop.

Select an option

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