Skip to content

Instantly share code, notes, and snippets.

@acamino
Last active January 30, 2026 20:20
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.6
description: |
REST API for batch ingestion of product, POS, inventory, and price data into the Shopify Bridge system.
This API provides an alternative to file-based ingestion, enabling programmatic data submission
with per-entry error tracking and job status monitoring.
## Key Features
- **Batch Processing**: Submit up to 10,000 entries per request
- **Per-Entry Tracking**: Client-provided identifiers for precise error correlation
- **Flexible Processing**: Synchronous for small batches, asynchronous for large ones
- **Idempotent Operations**: Optional idempotency keys for safe retries
## Processing Modes
| Batch Size | Mode | Response |
|------------|------|----------|
| ≤ 100 entries | Synchronous | `200`/`207` with full results |
| > 100 entries | Asynchronous | `202` with job_id for polling |
## Rate Limits
| Limit | Value |
|-------|-------|
| Requests per minute | 60 |
| Entries per minute | 10,000 |
| Payload size | 10 MB |
| Entries per request | 10,000 |
contact:
name: API Support
email: ag@pthreemedia.com
license:
name: Proprietary
identifier: LicenseRef-Proprietary
servers:
- url: https://shopify-bridge-api.up.railway.app
description: QA Environment (Railway)
- url: http://localhost:8080
description: Local Development
tags:
- name: Products
description: Product and variant ingestion
- name: POS
description: Point of Sale product updates
- name: Inventory
description: Inventory level management
- name: Prices
description: Price updates and promotions
- name: Jobs
description: Asynchronous job monitoring
- name: Admin
description: Administrative endpoints for job and outbox management
security:
- BearerAuth: []
paths:
/api/v1/ingest/products/{store_number}:
post:
operationId: ingestProducts
summary: Ingest products with variants
parameters:
- $ref: '#/components/parameters/StoreNumber'
description: |
Submit product data with one or more variants per entry.
Each entry represents a complete product with its variants. SKUs are automatically
padded to 9 digits for numeric values.
## Processing Rules
| Rule | Details |
|------|---------|
| Product matching | By handle (auto-generated from title if missing) |
| Variant matching | By SKU within product (9-digit padding for numeric SKUs) |
| Multi-variant support | Up to 3 option dimensions (option1, option2, option3) |
| Handle format | Lowercase alphanumeric with hyphens only |
| Initial load validation | `vertex.product_class` column required for initial load files |
| Metafield format | Descriptor format: `metafield.namespace.key.type` |
| Boolean normalization | Y/N, true/false → "True"/"False" |
| Taxonomy padding | Zero-padded to 3 digits (dept, sub_department, class, sub_class) |
| Store scoping | Products are store-scoped (not location-scoped) |
## Safeguards
| Safeguard | Description |
|-----------|-------------|
| Optimistic concurrency | Version field prevents conflicting updates |
| Duplicate detection | Prevents re-creation of existing products |
| Batch processing | 100 records per batch with transactional boundaries |
| Change detection | Skips unchanged products (compares title, data, metafields) |
| Sync preservation | ShopifyProductID, SyncStatus, SyncAttempts preserved during updates |
| Product options | Auto-populated from variant values post-processing |
| Outbox pattern | Ensures reliable Shopify sync with retry capability |
## Limitations
| Limit | Value |
|-------|-------|
| Max entries per request | 10,000 |
| Max payload size | 10 MB |
| Batch size (file processing) | 100 |
| SKU uniqueness | Per product+store (not globally) |
| Handle uniqueness | Per store |
| Initial load requirement | `vertex.product_class` column required |
tags:
- Products
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProductIngestRequest'
examples:
multiVariantProduct:
summary: Multi-variant product with full metafields
value:
idempotency_key: "qa-test-products-001"
options:
force_sync: true
entries:
- entry_id: "product-1"
data:
handle: "1-4z-pwrbl-sm-e-wd-scarl-xs---925658-1"
title: "University of New Mexico 1/4 Zip Powerblend Jacket"
description: "This versatile, eco-friendly Powerblend 1/4 zip campus sweatshirt gives you a great casual look."
vendor: "Champion"
product_type: "Sweatshirts"
tags: "collection:men,collection:clothing-and-accessories,collection:sweatshirts"
published: true
metafields:
custom.product_number: "030631 CS2083/E1177237/2299"
custom.manufacturer: "Champion Products"
custom.fabric: "50% Cotton/50% Polyester"
custom.embellishment: "Embroidered/Sewn"
custom.merchandisehierarchy: "500004001998"
custom.dept: "500"
custom.deptname: "Mens Apparel"
custom.vendornumber: "30631"
custom.vendorstyle: "CS2083/E1177237"
custom.seasoncode: "BNS"
custom.vendor_partnumber: "CS2083"
custom.san_number: "804-1814"
custom.aap_flag: "False"
custom.esd_flag: "False"
custom.graphic_type: "Sustainable"
taxonomy.department: "500"
taxonomy.sub_department: "004"
taxonomy.class: "001"
taxonomy.sub_class: "998"
merchandising.parent_store: "9975"
merchandising.child_store: "2299"
fulfillment.dsv_flag: "False"
fulfillment.vendor: "CHAMPION PRODUCTS"
fulfillment.vendor_number: "30631"
variants:
- sku: "18886985"
price: 75
compare_at_price: 75
cost: 28.92
barcode: "18886985"
taxable: true
requires_shipping: true
tax_code: "61000"
inventory_policy: "deny"
option1_name: "COLOR"
option1_value: "Scarlet Red"
option2_name: "SIZE"
option2_value: "XSmall"
variant_image: "https://bkstr.scene7.com/is/image/Bkstr/2299-CS2083-E1177237-Scarlet-Red"
variant_metafields:
vertex.product_class: "61000"
custom.clearance_flag: "False"
custom.sale_flag: "False"
- sku: "18886986"
price: 75
compare_at_price: 75
cost: 28.92
barcode: "18886986"
taxable: true
requires_shipping: true
tax_code: "61000"
inventory_policy: "deny"
option1_name: "COLOR"
option1_value: "Scarlet Red"
option2_name: "SIZE"
option2_value: "Small"
variant_image: "https://bkstr.scene7.com/is/image/Bkstr/2299-CS2083-E1177237-Scarlet-Red"
variant_metafields:
vertex.product_class: "61000"
custom.clearance_flag: "False"
custom.sale_flag: "False"
- entry_id: "product-2"
data:
handle: "12x40-cooling-towe-team1-------432088-1"
title: "University of New Mexico 12x40 Cooling Towel"
product_type: "Tailgate & Spirit"
tags: "collection:tailgate-and-spirit,collection:gifts-and-collectibles"
published: true
metafields:
custom.product_number: "066255 52300/58/2299"
custom.manufacturer: "The Northwest Company"
custom.embellishment: "Embroidered/Sewn"
custom.merchandisehierarchy: "600011001001"
custom.dept: "600"
custom.deptname: "Gifts"
custom.vendornumber: "66255"
custom.vendorstyle: "52300/58"
custom.seasoncode: "BNS"
custom.vendor_partnumber: "52300"
custom.san_number: "804-1814"
custom.aap_flag: "False"
custom.esd_flag: "False"
custom.graphic_type: "Wordmark"
taxonomy.department: "600"
taxonomy.sub_department: "011"
taxonomy.class: "001"
taxonomy.sub_class: "001"
merchandising.parent_store: "9975"
merchandising.child_store: "2299"
fulfillment.dsv_flag: "False"
fulfillment.vendor: "The Northwest Group, LLC"
fulfillment.vendor_number: "66255"
variants:
- sku: "27490402"
price: 20
compare_at_price: 20
cost: 6.17
barcode: "190604737423"
taxable: true
requires_shipping: true
tax_code: "61360"
inventory_policy: "deny"
option1_name: "COLOR"
option1_value: "Team Color"
variant_image: "https://bkstr.scene7.com/is/image/Bkstr/2299-52300-58-Team-Color"
variant_metafields:
vertex.product_class: "61360"
custom.clearance_flag: "False"
custom.sale_flag: "False"
minimalProduct:
summary: Minimal product (required fields only)
value:
entries:
- entry_id: "prod-minimal"
data:
title: "Simple Product"
variants:
- sku: "SIMPLE-001"
price: 19.99
responses:
'200':
description: All entries processed successfully
content:
application/json:
schema:
$ref: '#/components/schemas/SyncIngestResponse'
'202':
description: Batch accepted for asynchronous processing
content:
application/json:
schema:
$ref: '#/components/schemas/AsyncIngestResponse'
'207':
description: Partial success - some entries failed
content:
application/json:
schema:
$ref: '#/components/schemas/SyncIngestResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'413':
$ref: '#/components/responses/PayloadTooLarge'
'422':
$ref: '#/components/responses/UnprocessableEntity'
'429':
$ref: '#/components/responses/RateLimitExceeded'
'500':
$ref: '#/components/responses/InternalServerError'
/api/v1/ingest/pos/{store_number}:
post:
operationId: ingestPOS
summary: Ingest POS product updates
parameters:
- $ref: '#/components/parameters/StoreNumber'
description: |
Submit Point of Sale product updates. Each entry creates or updates
one product with a single variant.
## Processing Rules
| Rule | Details |
|------|---------|
| Product-variant mapping | One product = one variant (1:1 enforced by handle generation) |
| Handle format | `{normalized-title}-{store_number}-{formatted_sku}` |
| Status handling | Status field ignored (always treated as ACTIVE in Shopify) |
| Store number default | "9975" if missing |
| Vendor default | "Unknown" if empty |
| Product type | Always "POS" |
| Tax code storage | Both `variants.tax_code` AND `vertex.product_class` metafield |
| Barcode formatting | Removes `.0` suffix, validates digits only |
| POSOnly flag | Always set to `true` |
## Default Values
| Field | Default | Condition |
|-------|---------|-----------|
| Store Number | "9975" | If empty |
| Title | "POS Product {row}" | If empty |
| Vendor | "Unknown" | If empty |
| Product Type | "POS" | Always |
| Price | 0.0 | If empty or parse error |
## Safeguards
| Safeguard | Description |
|-----------|-------------|
| Store validation | Ensures only active stores processed |
| Duplicate SKU handling | Unique handle generation includes store+SKU |
| Price change tracking | Price history stored in JSONB |
| Change detection | Product-level and variant-level comparison |
| Metafield type overrides | From Shopify definitions cache |
| Atomic updates | Variant changes trigger product sync |
## Limitations
| Limitation | Details |
|------------|---------|
| No multi-variant | POS design constraint (1 product = 1 variant) |
| Store validation | Store number must exist in database |
| Barcode format | Must be numeric (invalid values → empty string) |
| No images | Files array always empty for POS products |
| No tags | Tags array always empty for POS products |
tags:
- POS
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/POSIngestRequest'
examples:
posUpdate:
summary: POS product batch with metafields
value:
idempotency_key: "qa-test-pos-001"
entries:
- entry_id: "row-1"
data:
sku: "000000004"
title: "SUPPLIES"
vendor: "CATEGORY&DUMP SKU-NO SCAN CREDIT"
description: "School Supplies"
barcode: "000000000004"
price: 0
compare_at_price: 0
cost: 0
tax_code: "76900"
metafields:
custom.dept: "400"
taxonomy.department: "400"
custom.deptname: "School Supplies"
custom.vendornumber: "222222"
custom.merchandisehierarchy: "500999000000"
taxonomy.sub_department: "998"
taxonomy.class: "998"
taxonomy.sub_class: "998"
merchandising.child_store: "2299"
- entry_id: "row-2"
data:
sku: "000000005"
title: "Emblematic Men's Apparel"
vendor: "CATEGORY&DUMP SKU-NO SCAN CREDIT"
description: "Mens Apparel"
barcode: "000000000005"
price: 0.01
compare_at_price: 0.01
cost: 0
tax_code: "61000"
metafields:
custom.dept: "500"
taxonomy.department: "500"
custom.deptname: "Mens Apparel"
custom.vendornumber: "222222"
custom.merchandisehierarchy: "500999000000"
taxonomy.sub_department: "998"
taxonomy.class: "998"
taxonomy.sub_class: "998"
merchandising.child_store: "2299"
responses:
'200':
description: All entries processed successfully
content:
application/json:
schema:
$ref: '#/components/schemas/SyncIngestResponse'
'202':
description: Batch accepted for asynchronous processing
content:
application/json:
schema:
$ref: '#/components/schemas/AsyncIngestResponse'
'207':
description: Partial success - some entries failed
content:
application/json:
schema:
$ref: '#/components/schemas/SyncIngestResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'413':
$ref: '#/components/responses/PayloadTooLarge'
'422':
$ref: '#/components/responses/UnprocessableEntity'
'429':
$ref: '#/components/responses/RateLimitExceeded'
'500':
$ref: '#/components/responses/InternalServerError'
/api/v1/ingest/inventory/{store_number}:
post:
operationId: ingestInventory
summary: Ingest inventory level updates
parameters:
- $ref: '#/components/parameters/StoreNumber'
description: |
Submit inventory level updates for existing variants.
**Processing Modes** (via `options.mode`):
| Mode | Behavior |
|------|----------|
| `initial` | Sets absolute quantity values |
| `delta` | Adjusts quantities by the specified amounts (default) |
---
## Initial Inventory Load (`mode: initial`)
### Processing Rules
| Rule | Details |
|------|---------|
| on_hand mutation | `inventorySetQuantities` (absolute SET) |
| on_order mutation | `inventoryAdjustQuantities` (delta ADJUST) |
| Quantity mapping | on_hand → "available", on_order → "incoming" |
| Location resolution | Uses store_number + location_name |
| Location default | If location_name empty/"0": uses location where name = store_number |
| child_store routing | Defaults to location_name (or store_number if empty) |
| Duplicate SKU support | Updates ALL matching variants |
### Safeguards (Initial)
| Safeguard | Description |
|-----------|-------------|
| Location validation | Location must be active |
| Store validation | Store must be active |
| Transaction wrapping | Outbox + audit events atomic |
| Placeholder handling | Location/inventory item ID placeholders if Shopify IDs missing |
### Limitations (Initial)
| Limitation | Details |
|------------|---------|
| Location requirement | Location must exist in database |
| Location status | Location must be active |
| No dynamic creation | Cannot create locations dynamically |
| Zero handling | on_hand defaults to 0 if empty (not skipped) |
| Quantity parsing | Float truncated to int (e.g., 105.99 → 105) |
---
## Delta Inventory (`mode: delta`)
### Processing Rules
| Rule | Details |
|------|---------|
| Mutation | `inventoryAdjustQuantities` (delta ADJUST) for both types |
| Delta type: ADJUSTMENT | Uses quantity_name "available" |
| Delta type: ON_ORDER | Uses quantity_name "incoming" |
| Quantity direction | Positive (increase) or negative (decrease) |
| No CAS checks | Trusts file delta values directly |
| No local tracking | No local quantity state maintained |
### Delta Type Mapping
| File Value | Normalized | Shopify quantity_name |
|------------|------------|----------------------|
| "Adjustment" | ADJUSTMENT | available |
| "On-Order" / "ON_ORDER" | ON_ORDER | incoming |
### 3-Tier Variant Lookup (Delta)
| Tier | Action |
|------|--------|
| Tier 1 | Local database lookup by SKU+store+child_store |
| Tier 2 | Sync missing variants from Shopify if product exists locally |
| Tier 3 | Full Shopify product fetch + local creation |
### Safeguards (Delta)
| Safeguard | Description |
|-----------|-------------|
| Delta type validation | Rejects invalid types |
| Location validation | Location must be active |
| Store validation | Store must be active |
| Transaction isolation | Per-update atomicity |
### Limitations (Delta)
| Limitation | Details |
|------------|---------|
| Delta type values | Must be "On-Order" or "Adjustment" (case-insensitive) |
| No drift detection | No local inventory state tracking |
| File accuracy | Relies entirely on file delta accuracy |
| No validation | Cannot validate final quantity result |
| Location status | Location must exist and be active |
tags:
- Inventory
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/InventoryIngestRequest'
examples:
deltaUpdate:
summary: Delta inventory adjustment
value:
options:
mode: "delta"
entries:
- entry_id: "inv-001"
data:
sku: "CBS-S-BLU"
store: "STORE-101"
on_hand: 5
- entry_id: "inv-002"
data:
sku: "CBS-M-BLU"
store: "STORE-101"
on_hand: -2
initialSet:
summary: Set absolute inventory levels
value:
options:
mode: "initial"
entries:
- entry_id: "inv-003"
data:
sku: "CBS-S-BLU"
store: "STORE-101"
location_name: "Main Warehouse"
on_hand: 100
on_order: 50
responses:
'200':
description: All entries processed successfully
content:
application/json:
schema:
$ref: '#/components/schemas/SyncIngestResponse'
'202':
description: Batch accepted for asynchronous processing
content:
application/json:
schema:
$ref: '#/components/schemas/AsyncIngestResponse'
'207':
description: Partial success - some entries failed
content:
application/json:
schema:
$ref: '#/components/schemas/SyncIngestResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'413':
$ref: '#/components/responses/PayloadTooLarge'
'422':
$ref: '#/components/responses/UnprocessableEntity'
'429':
$ref: '#/components/responses/RateLimitExceeded'
'500':
$ref: '#/components/responses/InternalServerError'
/api/v1/ingest/prices:
post:
operationId: ingestPrices
summary: Ingest price updates
description: |
Submit price updates for existing variants.
## Processing Rules
| Rule | Details |
|------|---------|
| Price types | R (Regular), S (Sale), C (Clearance), CS (Clearance Sale) |
| Type R | Sets price, clears compare_at_price |
| Type S/C | Sets price, preserves/sets compare_at_price from old price |
| Type CS | Combines Sale + Clearance flags |
| Change tolerance | $0.01 (float precision protection) |
| Metafield flags | `custom.sale_flag`, `custom.clearance_flag` auto-set based on type |
| child_store routing | Defaults to parent store if empty or "0" |
| Duplicate SKU support | Updates ALL matching variants |
## Price Type Behavior
| Type | Price | compare_at_price | sale_flag | clearance_flag |
|------|-------|------------------|-----------|----------------|
| R (Regular) | new_price | cleared (nil) | false | false |
| S (Sale) | new_price | old_price (if transitioning from R) | true | false |
| C (Clearance) | new_price | old_price (if transitioning from R) | false | true |
| CS (Clearance Sale) | new_price | old_price (if transitioning from R) | true | true |
## 2-Tier Variant Lookup
| Tier | Action |
|------|--------|
| Tier 1 | Local database lookup by SKU+store+child_store (includes duplicate SKU resolution via child_store metafield) |
| Tier 2 | Shopify API fallback (fetches product, creates locally, returns variants) |
## Safeguards
| Safeguard | Description |
|-----------|-------------|
| Price tolerance | $0.01 (skips sub-penny changes) |
| Skip unchanged | Skips update if price AND price_type unchanged |
| Store validation | Validates store exists and is active |
| Transaction isolation | Each SKU update in separate transaction |
| Price history | Tracked in `variant.Data["price_history"]` JSONB array |
| Max sync attempts | 3 attempts via outbox pattern |
## Limitations
| Limitation | Details |
|------------|---------|
| Variant requirement | Variants must exist (creates via Tier 2 if needed) |
| Negative prices | Rejected during validation |
| Store status | Store must be active |
| Price type default | Defaults to "R" if invalid |
| Max sync attempts | 3 via outbox pattern |
tags:
- Prices
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PriceIngestRequest'
examples:
regularPrice:
summary: Regular price update
value:
entries:
- entry_id: "price-001"
data:
sku: "CBS-S-BLU"
store_id: "STORE-101"
price: 44.99
price_type: "R"
salePrice:
summary: Sale price with compare_at
value:
entries:
- entry_id: "price-002"
data:
sku: "CBS-S-BLU"
store_id: "STORE-101"
price: 34.99
price_type: "S"
responses:
'200':
description: All entries processed successfully
content:
application/json:
schema:
$ref: '#/components/schemas/SyncIngestResponse'
'202':
description: Batch accepted for asynchronous processing
content:
application/json:
schema:
$ref: '#/components/schemas/AsyncIngestResponse'
'207':
description: Partial success - some entries failed
content:
application/json:
schema:
$ref: '#/components/schemas/SyncIngestResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'413':
$ref: '#/components/responses/PayloadTooLarge'
'422':
$ref: '#/components/responses/UnprocessableEntity'
'429':
$ref: '#/components/responses/RateLimitExceeded'
'500':
$ref: '#/components/responses/InternalServerError'
/api/v1/jobs/{job_id}:
get:
operationId: getJobStatus
summary: Get job status and summary
description: |
Retrieve the current status and summary statistics for an asynchronous job.
Poll this endpoint to track progress of large batch submissions.
Once status is `completed`, `completed_with_errors`, or `failed`,
use the results endpoint to retrieve detailed per-entry outcomes.
tags:
- Jobs
parameters:
- $ref: '#/components/parameters/JobId'
responses:
'200':
description: Job status retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/JobStatus'
examples:
processing:
summary: Job in progress
value:
job_id: "27"
status: "processing"
created_at: "2025-01-06T10:30:00Z"
updated_at: "2025-01-06T10:30:45Z"
progress_percent: 65
summary:
total: 1000
processed: 650
created: 400
updated: 250
errors: 0
completed:
summary: Job completed with errors
value:
job_id: "27"
status: "completed_with_errors"
created_at: "2025-01-06T10:30:00Z"
updated_at: "2025-01-06T10:32:15Z"
completed_at: "2025-01-06T10:32:15Z"
progress_percent: 100
summary:
total: 1000
processed: 985
created: 600
updated: 385
errors: 15
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/api/v1/jobs/{job_id}/results:
get:
operationId: getJobResults
summary: Get paginated job results
description: |
Retrieve detailed per-entry results for a job.
Results are returned in the same order as the original request entries.
Use pagination parameters to retrieve large result sets efficiently.
tags:
- Jobs
parameters:
- $ref: '#/components/parameters/JobId'
- name: status
in: query
description: Filter results by status
schema:
type: string
enum:
- success
- error
- skipped
- name: limit
in: query
description: Maximum results per page
schema:
type: integer
minimum: 1
maximum: 1000
default: 100
- name: offset
in: query
description: Number of results to skip
schema:
type: integer
minimum: 0
default: 0
responses:
'200':
description: Results retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/JobResults'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/api/v1/jobs/{job_id}/errors:
get:
operationId: getJobErrors
summary: Get job errors
description: |
Convenience endpoint to retrieve only failed entries from a job.
Returns the original entry data alongside error details to facilitate
debugging and resubmission of failed entries.
tags:
- Jobs
parameters:
- $ref: '#/components/parameters/JobId'
responses:
'200':
description: Errors retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/JobErrors'
examples:
withErrors:
summary: Job with validation errors
value:
job_id: "27"
total_errors: 2
errors:
- entry_id: "prod-005"
error:
type: "validation"
message: "At least one variant is required"
field: "variants"
data:
handle: "empty-product"
title: "Product Without Variants"
variants: []
- entry_id: "prod-008"
error:
type: "resolution"
message: "Store not found: INVALID-STORE"
field: "store_id"
data:
sku: "12345"
store_id: "INVALID-STORE"
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
# =========================================================================
# Admin Endpoints
# =========================================================================
/api/v1/admin/import-jobs/{id}:
get:
operationId: getImportJob
summary: Get import job details
description: |
Retrieve detailed information about an import job including:
- Job status and progress
- Processing statistics (total, processed, errors)
- File metadata (if file-based import)
- Timestamps (created, updated, completed)
This endpoint provides enriched data including sync error counts from the outbox.
tags:
- Admin
parameters:
- name: id
in: path
required: true
description: Import job ID
schema:
type: integer
format: int64
examples:
default:
value: 27
responses:
'200':
description: Import job retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/ImportJobResponse'
examples:
completed:
summary: Completed import job
value:
import_job:
id: 27
file_type: "api_product"
source: "api"
status: "completed"
total_rows: 2
processed_rows: 2
error_rows: 0
sync_errors: 0
created_at: "2025-01-30T10:30:00Z"
updated_at: "2025-01-30T10:30:05Z"
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/api/v1/admin/import-jobs/{id}/results:
get:
operationId: getImportJobResults
summary: Get import job results
description: |
Retrieve all results for an import job with pagination support.
Results include both successful entries and errors, with details about:
- Entry ID (client-provided identifier)
- Status (success, error, skipped)
- Action taken (created, updated, unchanged)
- Product and variant IDs (on success)
- Error details (on failure)
tags:
- Admin
parameters:
- name: id
in: path
required: true
description: Import job ID
schema:
type: integer
format: int64
examples:
default:
value: 27
- name: limit
in: query
description: Maximum results per page
schema:
type: integer
minimum: 1
maximum: 1000
default: 100
- name: offset
in: query
description: Number of results to skip
schema:
type: integer
minimum: 0
default: 0
responses:
'200':
description: Results retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/ImportJobResultsResponse'
examples:
withResults:
summary: Job results with mixed outcomes
value:
results:
- entry_id: "product-1"
status: "success"
action: "created"
product_id: 649597
variant_ids: [1029834, 1029835]
- entry_id: "product-2"
status: "success"
action: "updated"
product_id: 649598
variant_ids: [1029836]
pagination:
limit: 100
offset: 0
total: 2
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/api/v1/admin/import-jobs/{id}/outbox:
get:
operationId: getImportJobOutbox
summary: Get outbox events for import job
description: |
Retrieve outbox events linked to an import job.
Outbox events represent pending Shopify sync operations. Use this endpoint to:
- Check if sync is complete (all events processed)
- Identify failed syncs (events with status=failed)
- Debug sync issues (view payloads and error messages)
tags:
- Admin
parameters:
- name: id
in: path
required: true
description: Import job ID
schema:
type: integer
format: int64
- name: status
in: query
description: Filter by outbox status (comma-separated)
schema:
type: string
examples:
pending:
value: "pending"
multiple:
value: "pending,processing,failed"
- name: limit
in: query
schema:
type: integer
default: 100
- name: offset
in: query
schema:
type: integer
default: 0
responses:
'200':
description: Outbox events retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/ImportJobOutboxResponse'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/api/v1/admin/import-jobs/{id}/errors:
get:
operationId: getImportJobErrors
summary: Get import job errors
description: |
Retrieve only error entries for an import job.
Convenience endpoint that filters results to show only failures,
making it easier to identify and debug processing issues.
tags:
- Admin
parameters:
- name: id
in: path
required: true
description: Import job ID
schema:
type: integer
format: int64
responses:
'200':
description: Errors retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/ImportJobErrorsResponse'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
description: |
API key authentication via Bearer token.
**Usage:**
```
Authorization: Bearer <your_api_key>
```
**Example curl request:**
```bash
curl -X POST "https://shopify-bridge-api.up.railway.app/api/v1/ingest/products/9975" \
-H "Authorization: Bearer your-api-key-here" \
-H "Content-Type: application/json" \
-d '{"entries": [...]}'
```
Contact ag@pthreemedia.com to request an API key.
parameters:
JobId:
name: job_id
in: path
required: true
description: System-generated job identifier (integer as string)
schema:
type: string
pattern: '^\d+$'
examples:
default:
value: "27"
StoreNumber:
name: store_number
in: path
required: true
description: Store number for routing the request
schema:
type: string
examples:
default:
value: "9975"
schemas:
# =========================================================================
# Request Schemas
# =========================================================================
ProductIngestRequest:
type: object
required:
- entries
properties:
idempotency_key:
type: string
description: Client-provided key for idempotent retries
maxLength: 255
examples:
- "import-2025-01-06-batch-001"
entries:
type: array
description: Array of product entries to process
minItems: 1
maxItems: 10000
items:
$ref: '#/components/schemas/ProductEntry'
options:
$ref: '#/components/schemas/IngestOptions'
ProductEntry:
type: object
required:
- entry_id
- data
properties:
entry_id:
type: string
description: Client-provided identifier for error correlation
maxLength: 255
examples:
- "prod-001"
data:
$ref: '#/components/schemas/ProductData'
ProductData:
type: object
required:
- variants
properties:
handle:
type: string
description: URL-safe product identifier. Generated from title if not provided.
pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
maxLength: 255
examples:
- "classic-blue-shirt"
title:
type: string
description: Product title
maxLength: 255
examples:
- "Classic Blue Shirt"
description:
type: string
description: Product description (HTML allowed)
examples:
- "<p>A timeless classic for any occasion.</p>"
vendor:
type: string
description: Product vendor/manufacturer
maxLength: 255
examples:
- "Acme Clothing"
product_type:
type: string
description: Product type or category
maxLength: 255
examples:
- "Shirts"
tags:
type: string
description: Comma-separated list of tags
examples:
- "clothing,shirts,blue,summer"
published:
type: boolean
description: Whether the product is visible in the storefront
default: true
metafields:
type: object
description: Custom metafields as namespace.key to value mapping
additionalProperties:
type: string
examples:
- custom.material: "100% Cotton"
custom.care_instructions: "Machine wash cold"
variants:
type: array
description: Product variants (at least one required)
minItems: 1
items:
$ref: '#/components/schemas/VariantData'
VariantData:
type: object
required:
- sku
properties:
sku:
type: string
description: Stock keeping unit. Numeric values are padded to 9 digits.
maxLength: 255
examples:
- "CBS-S-BLU"
price:
type: number
format: double
description: Selling price
minimum: 0
default: 0
examples:
- 49.99
compare_at_price:
type:
- number
- "null"
format: double
description: Original price (displayed as strikethrough for sale items)
minimum: 0
examples:
- 59.99
cost:
type:
- number
- "null"
format: double
description: Cost of goods (not visible to customers)
minimum: 0
examples:
- 22.50
barcode:
type:
- string
- "null"
description: Barcode (UPC, EAN, ISBN, etc.)
maxLength: 255
examples:
- "123456789012"
weight:
type:
- number
- "null"
format: double
description: Weight value
minimum: 0
examples:
- 200
weight_unit:
type: string
description: Weight unit
enum:
- g
- kg
- lb
- oz
default: "g"
inventory_policy:
type: string
description: Behavior when inventory reaches zero
enum:
- deny
- continue
default: "deny"
taxable:
type: boolean
description: Whether the variant is subject to taxes
default: true
requires_shipping:
type: boolean
description: Whether the variant requires shipping
default: true
tax_code:
type:
- string
- "null"
description: Tax code for tax calculation services
maxLength: 255
examples:
- "P0000000"
option1_name:
type:
- string
- "null"
description: First option name (e.g., "Size", "Color")
maxLength: 255
examples:
- "Size"
option1_value:
type:
- string
- "null"
description: First option value
maxLength: 255
examples:
- "Small"
option2_name:
type:
- string
- "null"
description: Second option name
maxLength: 255
option2_value:
type:
- string
- "null"
description: Second option value
maxLength: 255
option3_name:
type:
- string
- "null"
description: Third option name
maxLength: 255
option3_value:
type:
- string
- "null"
description: Third option value
maxLength: 255
variant_image:
type:
- string
- "null"
format: uri
description: URL to variant-specific image
examples:
- "https://cdn.example.com/images/cbs-small-blue.jpg"
variant_image_alt:
type:
- string
- "null"
description: Alt text for variant image
maxLength: 512
examples:
- "Classic Blue Shirt - Small"
variant_metafields:
type:
- object
- "null"
description: Variant-specific metafields
additionalProperties:
type: string
POSIngestRequest:
type: object
required:
- entries
properties:
idempotency_key:
type: string
maxLength: 255
entries:
type: array
minItems: 1
maxItems: 10000
items:
$ref: '#/components/schemas/POSEntry'
options:
$ref: '#/components/schemas/IngestOptions'
POSEntry:
type: object
required:
- entry_id
- data
properties:
entry_id:
type: string
maxLength: 255
data:
$ref: '#/components/schemas/POSData'
POSData:
type: object
description: |
POS item data. Store number is provided via URL path parameter.
Each POS entry creates one product with one variant (1:1 relationship).
required:
- sku
properties:
sku:
type: string
description: Stock keeping unit (padded to 9 digits for numeric values)
maxLength: 255
examples:
- "000000004"
title:
type: string
description: Product title. Defaults to "POS Product {row}" if empty.
maxLength: 255
examples:
- "SUPPLIES"
vendor:
type: string
description: Product vendor
maxLength: 255
default: "Unknown"
examples:
- "CATEGORY&DUMP SKU-NO SCAN CREDIT"
description:
type:
- string
- "null"
description: Product description
examples:
- "School Supplies"
barcode:
type:
- string
- "null"
description: Barcode (UPC). Must be numeric; invalid values are converted to empty string.
maxLength: 255
examples:
- "000000000004"
price:
type: number
format: double
description: Selling price (POS_PRICE)
minimum: 0
default: 0
examples:
- 0
- 10.01
compare_at_price:
type:
- number
- "null"
format: double
description: Original/regular price (REG_PRICE)
minimum: 0
examples:
- 0
- 0.01
cost:
type:
- number
- "null"
format: double
description: Cost of goods
minimum: 0
tax_code:
type:
- string
- "null"
description: |
Tax code for tax calculation.
Sets both `variants.tax_code` field AND `vertex.product_class` variant metafield.
maxLength: 255
examples:
- "76900"
- "61000"
metafields:
type:
- object
- "null"
description: |
Product-level metafields as namespace.key to value mapping.
Common POS metafields include:
- `custom.dept` - Department number (padded to 3 digits)
- `taxonomy.department` - Taxonomy department (padded to 3 digits)
- `custom.deptname` - Department name
- `custom.vendornumber` - Vendor number
- `custom.merchandisehierarchy` - Merchandise hierarchy code
- `taxonomy.sub_department` - Sub-department (padded to 3 digits)
- `taxonomy.class` - Class (padded to 3 digits)
- `taxonomy.sub_class` - Sub-class (padded to 3 digits)
- `merchandising.child_store` - Child store number
additionalProperties:
type: string
InventoryIngestRequest:
type: object
required:
- entries
properties:
idempotency_key:
type: string
maxLength: 255
entries:
type: array
minItems: 1
maxItems: 10000
items:
$ref: '#/components/schemas/InventoryEntry'
options:
allOf:
- $ref: '#/components/schemas/IngestOptions'
- type: object
properties:
mode:
type: string
description: |
Processing mode:
- `initial`: Sets absolute quantity values
- `delta`: Adjusts quantities by specified amounts
enum:
- initial
- delta
default: "delta"
InventoryEntry:
type: object
required:
- entry_id
- data
properties:
entry_id:
type: string
maxLength: 255
data:
$ref: '#/components/schemas/InventoryData'
InventoryData:
type: object
required:
- sku
- store
properties:
sku:
type: string
description: Stock keeping unit
maxLength: 255
store:
type: string
description: Store number
maxLength: 50
location_name:
type:
- string
- "null"
description: Location name for multi-location inventory. Defaults to store value.
maxLength: 255
on_hand:
type: integer
description: Quantity on hand (absolute value or delta depending on mode)
default: 0
on_order:
type: integer
description: Quantity on order
default: 0
PriceIngestRequest:
type: object
required:
- entries
properties:
idempotency_key:
type: string
maxLength: 255
entries:
type: array
minItems: 1
maxItems: 10000
items:
$ref: '#/components/schemas/PriceEntry'
options:
$ref: '#/components/schemas/IngestOptions'
PriceEntry:
type: object
required:
- entry_id
- data
properties:
entry_id:
type: string
maxLength: 255
data:
$ref: '#/components/schemas/PriceData'
PriceData:
type: object
required:
- sku
- store_id
- price
properties:
sku:
type: string
description: Stock keeping unit
maxLength: 255
store_id:
type: string
description: Store number
maxLength: 50
child_store:
type:
- string
- "null"
description: Child store for variant routing. Defaults to store_id.
maxLength: 50
price:
type: number
format: double
description: New price value
minimum: 0
price_type:
type: string
description: |
Price type affecting compare_at_price behavior:
- `R` (Regular): Sets price, clears compare_at_price
- `S` (Sale): Sets price, preserves/sets compare_at_price
- `C` (Clearance): Sets price, preserves/sets compare_at_price
- `CS` (Clearance Sale): Combines C + S flags
enum:
- R
- S
- C
- CS
default: "R"
IngestOptions:
type: object
properties:
force_sync:
type: boolean
description: Create outbox entries even for unchanged data
default: false
validate_only:
type: boolean
description: Validate entries without persisting. Useful for dry-run testing.
default: false
# =========================================================================
# Response Schemas
# =========================================================================
SyncIngestResponse:
type: object
required:
- job_id
- status
- summary
- results
properties:
job_id:
type: string
format: int64
description: System-generated job identifier
examples:
- "27"
status:
type: string
description: Overall job status
enum:
- completed
- completed_with_errors
- failed
summary:
$ref: '#/components/schemas/JobSummary'
results:
type: array
description: Per-entry results in submission order
items:
$ref: '#/components/schemas/EntryResult'
AsyncIngestResponse:
type: object
required:
- job_id
- status
- message
- links
properties:
job_id:
type: string
format: int64
description: System-generated job identifier
examples:
- "27"
status:
type: string
enum:
- pending
examples:
- "pending"
message:
type: string
description: Human-readable status message
examples:
- "Batch of 500 entries accepted for processing"
links:
type: object
required:
- status
- results
properties:
status:
type: string
format: uri
description: URL to poll for job status
examples:
- "/api/v1/jobs/27"
results:
type: string
format: uri
description: URL to retrieve results when complete
examples:
- "/api/v1/jobs/27/results"
JobStatus:
type: object
required:
- job_id
- status
- created_at
- updated_at
- summary
- progress_percent
properties:
job_id:
type: string
format: int64
status:
type: string
enum:
- pending
- processing
- completed
- completed_with_errors
- failed
created_at:
type: string
format: date-time
description: Job creation timestamp (ISO 8601)
updated_at:
type: string
format: date-time
description: Last update timestamp (ISO 8601)
completed_at:
type:
- string
- "null"
format: date-time
description: Completion timestamp (present when finished)
summary:
$ref: '#/components/schemas/JobSummary'
progress_percent:
type: integer
description: Processing progress (0-100)
minimum: 0
maximum: 100
JobSummary:
type: object
required:
- total
- processed
- created
- updated
- errors
properties:
total:
type: integer
description: Total entries submitted
minimum: 0
processed:
type: integer
description: Entries successfully processed
minimum: 0
created:
type: integer
description: New records created
minimum: 0
updated:
type: integer
description: Existing records updated
minimum: 0
errors:
type: integer
description: Entries that failed
minimum: 0
JobResults:
type: object
required:
- job_id
- total_results
- results
- pagination
properties:
job_id:
type: string
format: int64
total_results:
type: integer
description: Total matching results
minimum: 0
results:
type: array
items:
$ref: '#/components/schemas/EntryResult'
pagination:
$ref: '#/components/schemas/Pagination'
JobErrors:
type: object
required:
- job_id
- total_errors
- errors
properties:
job_id:
type: string
format: int64
total_errors:
type: integer
description: Total failed entries
minimum: 0
errors:
type: array
items:
$ref: '#/components/schemas/ErrorEntry'
EntryResult:
type: object
required:
- entry_id
- status
properties:
entry_id:
type: string
description: Echo of client-provided entry_id
status:
type: string
enum:
- success
- error
- skipped
action:
type:
- string
- "null"
description: Action taken (null if error)
enum:
- created
- updated
- unchanged
product_id:
type:
- integer
- "null"
description: Created/updated product ID (on success)
variant_ids:
type:
- array
- "null"
description: Created/updated variant IDs (on success)
items:
type: integer
error:
$ref: '#/components/schemas/EntryError'
ErrorEntry:
type: object
required:
- entry_id
- error
- data
properties:
entry_id:
type: string
error:
$ref: '#/components/schemas/EntryError'
data:
type: object
description: Original entry data for debugging
additionalProperties: true
EntryError:
type: object
required:
- type
- message
properties:
type:
type: string
description: |
Error category:
- `validation`: Business rule violation
- `conversion`: Data type conversion failure
- `database`: Database constraint violation
- `resolution`: Reference lookup failure
enum:
- validation
- conversion
- database
- resolution
message:
type: string
description: Human-readable error description
examples:
- "SKU is required"
field:
type:
- string
- "null"
description: Field that caused the error
examples:
- "sku"
Pagination:
type: object
required:
- limit
- offset
- has_more
properties:
limit:
type: integer
description: Current page size
offset:
type: integer
description: Current offset
has_more:
type: boolean
description: Whether more results are available
# =========================================================================
# Admin Response Schemas
# =========================================================================
ImportJobResponse:
type: object
required:
- import_job
properties:
import_job:
$ref: '#/components/schemas/ImportJob'
ImportJob:
type: object
properties:
id:
type: integer
format: int64
description: Import job ID
file_id:
type:
- string
- "null"
description: File ID (for file-based imports)
file_path:
type:
- string
- "null"
description: File path (for file-based imports)
file_type:
type: string
description: Type of import (product, pos, inventory, api_product, api_pos, etc.)
examples:
- "api_product"
- "pos"
- "product"
source:
type: string
description: Import source
enum:
- file
- api
status:
type: string
description: Job status
enum:
- pending
- processing
- completed
- completed_with_errors
- failed
total_rows:
type: integer
description: Total entries in the job
processed_rows:
type: integer
description: Successfully processed entries
error_rows:
type: integer
description: Failed entries
sync_errors:
type: integer
description: Number of failed outbox sync operations
error_message:
type:
- string
- "null"
description: Error message (if failed)
idempotency_key:
type:
- string
- "null"
description: Client-provided idempotency key
store_id:
type:
- integer
- "null"
format: int64
description: Store ID (for API imports)
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
ImportJobResultsResponse:
type: object
required:
- results
- pagination
properties:
results:
type: array
items:
$ref: '#/components/schemas/ImportResult'
pagination:
type: object
properties:
limit:
type: integer
offset:
type: integer
total:
type: integer
ImportResult:
type: object
properties:
id:
type: integer
format: int64
import_job_id:
type: integer
format: int64
row_number:
type: integer
entry_id:
type:
- string
- "null"
description: Client-provided entry identifier
status:
type: string
enum:
- success
- error
- skipped
action:
type:
- string
- "null"
description: Action taken (created, updated, unchanged)
enum:
- created
- updated
- unchanged
product_id:
type:
- integer
- "null"
format: int64
variant_ids:
type:
- array
- "null"
items:
type: integer
format: int64
error_type:
type:
- string
- "null"
description: Error category (validation, database, conversion, resolution)
error_message:
type:
- string
- "null"
error_column:
type:
- string
- "null"
created_at:
type: string
format: date-time
ImportJobOutboxResponse:
type: object
required:
- outbox_events
- pagination
properties:
outbox_events:
type: array
items:
$ref: '#/components/schemas/OutboxEvent'
pagination:
type: object
properties:
limit:
type: integer
offset:
type: integer
total:
type: integer
OutboxEvent:
type: object
properties:
id:
type: integer
format: int64
aggregate_id:
type: integer
format: int64
description: Product or variant ID
aggregate_type:
type: string
description: Entity type
enum:
- product
- variant
event_type:
type: string
description: Operation type
enum:
- create
- update
store_number:
type: string
sync_operation:
type:
- string
- "null"
description: Sync operation type (product.sync, pos.sync, inventory, price)
status:
type: string
enum:
- pending
- processing
- processed
- failed
attempts:
type: integer
max_attempts:
type: integer
error_message:
type:
- string
- "null"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
processed_at:
type:
- string
- "null"
format: date-time
ImportJobErrorsResponse:
type: object
required:
- errors
properties:
errors:
type: array
items:
$ref: '#/components/schemas/ImportResult'
total:
type: integer
# =========================================================================
# Error Response Schemas
# =========================================================================
ErrorResponse:
type: object
required:
- error
properties:
error:
type: string
description: Human-readable error message
ValidationErrorResponse:
type: object
required:
- error
- validation_errors
properties:
error:
type: string
examples:
- "Request validation failed"
validation_errors:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
RateLimitErrorResponse:
type: object
required:
- error
- retry_after
properties:
error:
type: string
examples:
- "Rate limit exceeded"
retry_after:
type: integer
description: Seconds until the rate limit resets
examples:
- 30
responses:
BadRequest:
description: All entries failed validation
content:
application/json:
schema:
type: object
properties:
error:
type: string
details:
type: array
items:
type: object
examples:
default:
value:
error: "All entries failed validation"
details:
- entry_id: "prod-001"
message: "SKU is required"
Unauthorized:
description: Missing or invalid Authorization header
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
missingHeader:
summary: Missing Authorization header
value:
error: "Missing Authorization header"
invalidFormat:
summary: Invalid header format (must be "Bearer <token>")
value:
error: "Invalid Authorization header format"
invalidKey:
summary: Invalid or inactive API key
value:
error: "Invalid or inactive API key"
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
default:
value:
error: "Job not found"
PayloadTooLarge:
description: Request payload exceeds size limit
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
default:
value:
error: "Payload exceeds 10MB limit"
UnprocessableEntity:
description: Invalid request schema
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationErrorResponse'
examples:
default:
value:
error: "Request validation failed"
validation_errors:
- field: "entries"
message: "must contain at least 1 item"
RateLimitExceeded:
description: Rate limit exceeded
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimitErrorResponse'
examples:
default:
value:
error: "Rate limit exceeded: 60 requests per minute"
retry_after: 45
InternalServerError:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
default:
value:
error: "An unexpected error occurred"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment