ERP API Documentation

v1

Quick start — Visual FoxPro 9 + Chilkat 9.5.0

Below are ready-to-run examples using Chilkat ActiveX from Visual FoxPro 9. Replace YOUR_HOST, YOUR_CHANNEL and YOUR_API_KEY as needed.

GET /api/v1/health (no auth)
* Requires Chilkat ActiveX (e.g., v9.5.0) registered on the system
loRest = CreateObject("Chilkat_9_5_0.Rest")
loSocket = CreateObject("Chilkat_9_5_0.Socket")

lcHost = "YOUR_HOST"
lnTls = .T.
lnPort = 443
lnMaxWaitMs = 30000

llOk = loSocket.Connect(lcHost, lnPort, lnTls, lnMaxWaitMs)
IF !llOk
    ? "Connect error:", loSocket.LastErrorText
    RETURN
ENDIF

llOk = loRest.UseConnection(loSocket, .T.) && auto-close on rest object release
IF !llOk
    ? "REST error:", loRest.LastErrorText
    RETURN
ENDIF

lcResp = loRest.FullRequestNoBody("GET", "/api/v1/health")
IF loRest.LastMethodSuccess = 0
    ? "Request error:", loRest.LastErrorText
    RETURN
ENDIF
? lcResp && => {"status":"ok","time":"..."}
GET /api/v1/orders?channel=...&limit=100
loRest = CreateObject("Chilkat_9_5_0.Rest")
loSocket = CreateObject("Chilkat_9_5_0.Socket")

lcHost = "YOUR_HOST"
lnTls = .T.
lnPort = 443
lnMaxWaitMs = 30000

llOk = loSocket.Connect(lcHost, lnPort, lnTls, lnMaxWaitMs)
IF !llOk
    ? "Connect error:", loSocket.LastErrorText
    RETURN
ENDIF

llOk = loRest.UseConnection(loSocket, .T.)
IF !llOk
    ? "REST error:", loRest.LastErrorText
    RETURN
ENDIF

* Headers
loRest.AddHeader("X-Api-Key", "YOUR_API_KEY")

* Query params
lcChannel = "YOUR_CHANNEL"  && optional, e.g., shop-us
lcSince   = ""              && optional ISO8601
loRest.ClearAllQueryParams()
IF !EMPTY(lcChannel)
    loRest.AddQueryParam("channel", lcChannel)
ENDIF
IF !EMPTY(lcSince)
    loRest.AddQueryParam("since", lcSince)
ENDIF
loRest.AddQueryParam("limit", "100")

lcResp = loRest.FullRequestNoBody("GET", "/api/v1/orders")
IF loRest.LastMethodSuccess = 0
    ? "Request error:", loRest.LastErrorText
    RETURN
ENDIF

* Parse JSON array (requires Chilkat JSON ActiveX)
loJson = CreateObject("Chilkat_9_5_0.JsonArray")
llOk = loJson.Load(lcResp)
IF !llOk
    ? "Invalid JSON response"
    RETURN
ENDIF
? "Orders count:", loJson.Size

Filtering orders by status (fetch cancellations)

The orders endpoint supports filtering by status. This lets your ERP discover orders that were canceled after they were already imported.

  • status: single value, e.g., cancelled, paid, fulfilled
  • statuses: multiple values via statuses=paid,cancelled or statuses[]=paid&statuses[]=cancelled
  • since: ISO8601 timestamp; we return orders updated since this time (uses last_remote_update_at)
  • When status or statuses is used, we include orders regardless of their imported_to_erp flag.
  • By default (when no status filter is provided), cancelled orders are excluded unless you explicitly set include_cancelled=true (or include_canceled=true).
GET /api/v1/orders?status=cancelled&since=...
* Fetch orders cancelled since a timestamp (regardless of imported_to_erp)
loRest = CreateObject("Chilkat_9_5_0.Rest")
loSocket = CreateObject("Chilkat_9_5_0.Socket")

lcHost = "YOUR_HOST"
lnTls = .T.
lnPort = 443
lnMaxWaitMs = 30000

llOk = loSocket.Connect(lcHost, lnPort, lnTls, lnMaxWaitMs)
IF !llOk
    ? "Connect error:", loSocket.LastErrorText
    RETURN
ENDIF

llOk = loRest.UseConnection(loSocket, .T.)
IF !llOk
    ? "REST error:", loRest.LastErrorText
    RETURN
ENDIF

* Headers
loRest.AddHeader("X-Api-Key", "YOUR_API_KEY")

* Query params
lcChannel = "YOUR_CHANNEL"  && optional
lcSince   = "2025-12-01T00:00:00Z"  && ISO8601
loRest.ClearAllQueryParams()
IF !EMPTY(lcChannel)
    loRest.AddQueryParam("channel", lcChannel)
ENDIF
loRest.AddQueryParam("since", lcSince)
loRest.AddQueryParam("status", "cancelled")
loRest.AddQueryParam("limit", "200")

lcResp = loRest.FullRequestNoBody("GET", "/api/v1/orders")
IF loRest.LastMethodSuccess = 0
    ? "Request error:", loRest.LastErrorText
    RETURN
ENDIF

* Iterate results
loJson = CreateObject("Chilkat_9_5_0.JsonArray")
IF !loJson.Load(lcResp)
    ? "Invalid JSON response"
    RETURN
ENDIF

FOR i = 0 TO loJson.Size - 1
    loObj = loJson.ObjectAt(i)
    lcExternalId = loObj.StringOf("external_order_id")
    * cancel in ERP if present
    * ... your ERP cancel logic here ...
    RELEASE loObj
NEXT

Order lifecycle status vs payment status

Each order includes two related but distinct fields:

  • status — the order lifecycle state from the sales channel (Shopify). Possible values include: open, cancelled, fulfilled, partially_fulfilled, closed. This is what you should use for shipping and cancellation logic.
  • payment_status — mapped from Shopify financial_status (e.g., paid, pending, refunded, partially_refunded). This reflects payment collection and refunds, and is independent from lifecycle status.

Example: an order can be status=cancelled while still having payment_status=paid if it was paid and then cancelled; your ERP should treat it as non‑shippable due to the lifecycle status.

POST /api/v1/orders/ack
loRest = CreateObject("Chilkat_9_5_0.Rest")
loSocket = CreateObject("Chilkat_9_5_0.Socket")

lcHost = "YOUR_HOST"
llOk = loSocket.Connect(lcHost, 443, .T., 30000)
IF !llOk
    ? "Connect error:", loSocket.LastErrorText
    RETURN
ENDIF

llOk = loRest.UseConnection(loSocket, .T.)
IF !llOk
    ? "REST error:", loRest.LastErrorText
    RETURN
ENDIF

* Headers
loRest.AddHeader("X-Api-Key", "YOUR_API_KEY")
loRest.AddHeader("Content-Type", "application/json")

* Build body: { "ids": [123,124] }
loJson = CreateObject("Chilkat_9_5_0.JsonObject")
loJson.UpdateInt("ids[0]", 123)
loJson.UpdateInt("ids[1]", 124)

lcResp = loRest.FullRequestString("POST", "/api/v1/orders/ack", loJson.Emit())
IF loRest.LastMethodSuccess = 0
    ? "Request error:", loRest.LastErrorText
    RETURN
ENDIF
? lcResp && => {"updated":2}
POST /api/v1/inventory_updates
loRest = CreateObject("Chilkat_9_5_0.Rest")
loSocket = CreateObject("Chilkat_9_5_0.Socket")

lcHost = "YOUR_HOST"
llOk = loSocket.Connect(lcHost, 443, .T., 30000)
IF !llOk
    ? "Connect error:", loSocket.LastErrorText
    RETURN
ENDIF

llOk = loRest.UseConnection(loSocket, .T.)
IF !llOk
    ? "REST error:", loRest.LastErrorText
    RETURN
ENDIF

* Headers
loRest.AddHeader("X-Api-Key", "YOUR_API_KEY")
loRest.AddHeader("Content-Type", "application/json")

* Body
loJson = CreateObject("Chilkat_9_5_0.JsonObject")
loJson.UpdateString("channel", "YOUR_CHANNEL")
loJson.UpdateString("items[0].sku", "ABC123")
loJson.UpdateInt("items[0].quantity", 10)

lcResp = loRest.FullRequestString("POST", "/api/v1/inventory_updates", loJson.Emit())
IF loRest.LastMethodSuccess = 0
    ? "Request error:", loRest.LastErrorText
    RETURN
ENDIF
? lcResp
GET /api/v1/carrier_codes?provider=walmart
loRest = CreateObject("Chilkat_9_5_0.Rest")
loSocket = CreateObject("Chilkat_9_5_0.Socket")

lcHost = "YOUR_HOST"
lnTls = .T.
lnPort = 443
lnMaxWaitMs = 30000

llOk = loSocket.Connect(lcHost, lnPort, lnTls, lnMaxWaitMs)
IF !llOk
    ? "Connect error:", loSocket.LastErrorText
    RETURN
ENDIF

llOk = loRest.UseConnection(loSocket, .T.)
IF !llOk
    ? "REST error:", loRest.LastErrorText
    RETURN
ENDIF

* Headers
loRest.AddHeader("X-Api-Key", "YOUR_API_KEY")

* Query params — filter by provider or channel
loRest.ClearAllQueryParams()
loRest.AddQueryParam("provider", "walmart")  && or use "channel", "wm-us"

lcResp = loRest.FullRequestNoBody("GET", "/api/v1/carrier_codes")
IF loRest.LastMethodSuccess = 0
    ? "Request error:", loRest.LastErrorText
    RETURN
ENDIF

* Parse JSON response
loJson = CreateObject("Chilkat_9_5_0.JsonObject")
llOk = loJson.Load(lcResp)
IF !llOk
    ? "Invalid JSON response"
    RETURN
ENDIF

lnCount = loJson.SizeOfArray("carrier_codes")
? "Carrier codes:", lnCount
FOR i = 0 TO lnCount - 1
    loJson.I = i
    lcCode = loJson.StringOf("carrier_codes[i].code")
    lcName = loJson.StringOf("carrier_codes[i].display_name")
    ? "  ", lcCode, "-", lcName
NEXT

Orders — Pull orders changed since cursor (includes canceled by default)

  • GET /api/v1/orders
  • Auth: required

Headers:

Header Type Required
X-Api-Key string yes

Query parameters:

Param Type Required Default Notes
channel string no Channel code filter; when omitted, returns across all channels for the account
since string no 24h ago ISO8601 last update time (orders updated at or after). We recommend storing the largest last_remote_update_at you have processed and using that for next calls.
limit int no 100 Max 500
status string no Optional single status filter (e.g., paid, cancelled, canceled). When provided, results are filtered to matching orders only.
statuses string[]/string no Optional multiple statuses. Accepts repeated params statuses[]=paid&statuses[]=cancelled or comma-separated statuses=paid,cancelled.

Behavior changes as of 2025-12-12:

  • Default response now includes all orders changed since since (based on last_remote_update_at, with fallback), regardless of import/ack status, and includes canceled orders by default.
  • Previously, only “unimported and not canceled” orders were returned by default. If you still need that legacy behavior, set statuses explicitly to the statuses your ERP wants, or filter on your side by imported_to_erp == false.
  • The include_cancelled/include_canceled parameter is now unnecessary and effectively ignored by default; canceled orders are included unless you apply a status/statuses filter that excludes them. The parameter remains accepted for backward compatibility but is deprecated.

Response body (array of orders):

Field Type Notes  
id integer Internal id  
external_order_id string Channel order id  
status string Order status from channel  
ordered_at string ISO8601 create/purchase time  
last_remote_update_at string ISO8601 last updated time from channel  
order_number_hint string Human-facing number where available  
fulfillment_channel string Channel/provider hint (e.g., shopify, walmart)  
channel_code string The hub Channel code configured in MAGI Channel Hub (e.g., shop-us)  
provider string The channel provider (e.g., shopify, walmart, amazon, ebay)  
taxes_included boolean null Whether the shop’s prices include tax (Shopify only; from shop.taxesIncluded). true = prices include tax, false = tax added on top, null = not applicable/unknown
buyer_name string Normalized buyer/contact name  
buyer_email string Normalized buyer email  
currency string ISO currency (3-letter)  
subtotal_amount number Items subtotal before taxes/shipping/discounts  
shipping_amount number Shipping total  
tax_amount number Tax total  
discount_amount number Discounts total  
total_amount number Order grand total  
bill_name string Billing name  
bill_address1 string    
bill_address2 string    
bill_city string    
bill_region string State/Province/Region  
bill_postal string Postal/ZIP  
bill_country string ISO country code where available  
bill_phone string    
ship_name string Ship-to name  
ship_address1 string    
ship_address2 string    
ship_city string    
ship_region string State/Province/Region  
ship_postal string Postal/ZIP  
ship_country string ISO country code where available  
ship_phone string    
payment_status string Channel-specific (e.g., paid, pending, canceled)  
payment_method string Gateway/method hint if provided  
line_items_count integer Number of line items  
items_total_quantity integer Sum of all line quantities  
is_gift boolean Gift flag if provided  
notes string Free-form notes if provided  
line_items array See “Line items schema” below  
raw_payload object Full channel-native payload for reference  
imported_to_erp boolean Whether already acknowledged by ERP  
imported_at string null ISO8601 when acknowledged, else null

Example response: [ { "id": 123, "external_order_id": "abc-1001", "status": "paid", "ordered_at": "2025-11-25T12:34:56Z", "last_remote_update_at": "2025-11-26T10:11:12Z", "order_number_hint": "#1001", "fulfillment_channel": "shopify", "channel_code": "shop-us", "provider": "shopify", "taxes_included": false, "buyer_name": "Jane Doe", "buyer_email": "jane@example.com", "currency": "USD", "subtotal_amount": 49.98, "shipping_amount": 5.00, "tax_amount": 3.75, "discount_amount": 0.00, "total_amount": 58.73, "ship_name": "Jane Doe", "ship_address1": "123 Main St", "ship_city": "Springfield", "ship_region": "IL", "ship_postal": "62701", "ship_country": "US", "payment_status": "paid", "payment_method": "shopify_payments", "line_items_count": 2, "items_total_quantity": 2, "is_gift": false, "notes": null, "line_items": [ { "external_line_id": "l_1", "sku": "TSHIRT-RED-S", "title": "Tee Shirt", "variant_sku": "Red / S", "item_master_id": 456, "quantity": 1, "currency": "USD", "unit_price": 24.99, "selling_unit_price": 24.99, "total_price": 24.99, "tax_amount": 1.87, "discount_amount": 0.0, "shipping_amount": 0.0, "fulfillment_status": null, "is_gift": false, "notes": null } ], "raw_payload": { }, "imported_to_erp": false, "imported_at": null } ]

Example without channel (all channels for the account): curl -s -H "X-Api-Key: $API_KEY" \ "https://your-host/api/v1/orders?since=2025-11-25T00:00:00Z&limit=100"

Example: curl -s -H "X-Api-Key: $API_KEY" \ "https://your-host/api/v1/orders?channel=shop-us&since=2025-11-25T00:00:00Z&limit=100"

Acknowledge imported orders

  • POST /api/v1/orders/ack
  • Auth: required

Headers:

Header Type Required Notes
X-Api-Key string yes  

Request body:

Field Type Required Notes
ids array yes Array of RemoteOrder.id to acknowledge

Example request body: { "ids": [123, 124, 125] }

Response body:

Field Type Notes
updated integer Number of orders marked as imported

Example response: { "updated": 3 }


Refunds — Pull unimported refunds

  • GET /api/v1/refunds
  • Auth: required

Headers:

Header Type Required Notes
X-Api-Key string yes  

Query parameters:

Param Type Required Default Notes
channel string yes Channel code
since string no 24h ago ISO8601 occurrence time
limit int no 100 Max 500

Response body (array of refunds):

Field Type Notes  
id integer Internal id  
provider string The channel provider (e.g., shopify, walmart, amazon, ebay)  
external_order_id string Channel order id  
external_refund_id string Channel refund/return id  
occurred_at string ISO8601 when refund occurred  
amount number Aggregate amount from provider (legacy; total refund amount)  
currency string ISO currency code  
subtotal_amount number Items subtotal refunded (best‑effort)  
shipping_amount number Shipping refunded  
tax_amount number Tax refunded  
discount_amount number Discounts deducted from refund  
total_amount number Total refunded amount (may equal amount)  
line_items_count integer Number of refund line items (if available)  
items_total_quantity integer Sum of refunded quantities across lines  
payment_status string Channel-specific payment status if applicable  
payment_method string Gateway/method hint if provided  
reason_code string High-level refund/return reason code if available  
notes string Free-form notes  
line_items array Per-line refund details (schema below)  
raw_payload object Channel-native payload  
imported_to_erp boolean Whether already acknowledged by ERP  
imported_at string null ISO8601 when acknowledged, else null

Line items schema (refund_event_lines):

Field Type Notes
external_line_id string Channel refund line id if available
sku string SKU associated with the refunded item
title string Product title
variant_sku string Variant descriptor if available
quantity integer Refunded quantity
currency string ISO currency
unit_refund number Unit amount refunded (best‑effort)
total_refund number Line total refund amount
tax_amount number Tax portion refunded
discount_amount number Discounts applied to the line (affecting refund)
shipping_amount number Per-line shipping refunded, if provided
reason_code string Reason for this line’s refund if available
notes string Additional notes

Example response: [ { "id": 987, "provider": "shopify", "external_order_id": "abc-1001", "external_refund_id": "r-555", "occurred_at": "2025-11-26T08:00:00Z", "amount": 10.50, "currency": "USD", "subtotal_amount": 9.99, "shipping_amount": 0.51, "tax_amount": 0.00, "discount_amount": 0.00, "total_amount": 10.50, "line_items_count": 1, "items_total_quantity": 1, "reason_code": null, "notes": null, "line_items": [ { "external_line_id": "rl_1", "sku": "TSHIRT-RED-S", "title": "Tee Shirt", "variant_sku": "Red / S", "quantity": 1, "currency": "USD", "unit_refund": 9.99, "total_refund": 9.99, "tax_amount": 0.00, "discount_amount": 0.00, "shipping_amount": 0.51, "reason_code": null, "notes": null } ], "raw_payload": { }, "imported_to_erp": false, "imported_at": null } ]

Example: curl -s -H "X-Api-Key: $API_KEY" \ "https://your-host/api/v1/refunds?channel=shop-us&since=2025-11-25T00:00:00Z&limit=100"

Provider notes: - Shopify: per-line refund details are included; shipping adjustments captured at refund level when present. Totals are computed best‑effort from refund payload. - Walmart: per-line refund charges aggregated from return lines; amounts broken down into product/shipping/tax/discount components. - eBay: Finances API yields aggregate refund transactions without per-line details; only total_amount is populated. - Amazon: Finances API yields aggregate refund events; only total_amount is populated.

Acknowledge imported refunds

  • POST /api/v1/refunds/ack
  • Auth: required

Headers:

Header Type Required
X-Api-Key string yes

Request body:

Field Type Required Notes
ids array yes Array of RefundEvent.id to acknowledge

Example request body: { "ids": [987, 988] }

Response body:

Field Type Notes
updated integer Number of refunds marked as imported

Example response: { "updated": 2 }


Inventory Updates — Enqueue quantity pushes

  • POST /api/v1/inventory_updates
  • Auth: required

Headers:

Header Type Required
X-Api-Key string yes

Request body schema:

Field Type Required Notes
channel string yes Channel code
items array yes Array of stock updates
items[].item_master_id integer preferred Internal ItemMaster.id for the channel/tenant
items[].sku string deprecated SKU code (legacy; will be removed in a future version)
items[].quantity integer yes New available quantity

Example request body (preferred ItemMaster IDs): { "channel": "shop-us", "items": [ { "item_master_id": 4421, "quantity": 10 }, { "item_master_id": 4422, "quantity": 0 } ] }

Legacy example (SKU-based; deprecated): { "channel": "shop-us", "items": [ { "sku": "ABC123", "quantity": 10 } ] }

Response body (201 Created):

Field Type Notes
created_ids array IDs of InventoryUpdate records enqueued

Example response: { "created_ids": [1001, 1002] }

Validation errors (422): { "error": ["either item_master_id or sku must be present", "Quantity is not a number"] }

Notes: - Prefer item_master_id and store this ID in your ERP for future updates. The hub will handle channel-specific identifier mappings (e.g., Shopify inventory_item_id). - When you send SKU only, the response will include Deprecation header. SKU-based inventory updates will be removed in a future release. - items must be an array; otherwise 422 with { "error": "items must be an array" }.


Shipments — Enqueue shipment pushes

  • POST /api/v1/shipments
  • Auth: required

Headers:

Header Type Required
X-Api-Key string yes

Request body schema:

Field Type Required Notes
shipments array yes List of shipment payloads
shipments[].remote_order_id integer no Preferred. MAGI Channel Hub remote_orders.id; implies channel/order.
shipments[].channel string conditionally Required if remote_order_id is not provided. Channel code.
shipments[].external_order_id string conditionally Required if remote_order_id is not provided. Channel order id.
shipments[].carrier string yes Must be a valid carrier code for the channel’s provider. Use GET /api/v1/carrier_codes to fetch the allowed list. Casing is auto-corrected to the canonical form (e.g., upsUPS).
shipments[].service string no e.g., Ground, 2Day
shipments[].tracking_number string yes Tracking number
shipments[].ship_date string no ISO8601 ship date/time

Example request body (preferred: remote_order_id): { "shipments": [ { "remote_order_id": 43210, "carrier": "UPS", "service": "Ground", "tracking_number": "1Z999AA10123456784", "ship_date": "2025-11-26T15:00:00Z" } ] }

Example request body (backward compatible: channel + external_order_id): { "shipments": [ { "channel": "shop-us", "external_order_id": "abc-1001", "carrier": "UPS", "service": "Ground", "tracking_number": "1Z999AA10123456784", "ship_date": "2025-11-26T15:00:00Z" } ] }

Response body (201 Created):

Field Type Notes
created_ids array IDs of ShipmentPush records enqueued

Example response: { "created_ids": [2001] }

Validation errors (422): { "error": ["Tracking number can't be blank"] }

Notes: - Prefer remote_order_id and store this id in your ERP; the hub will resolve the correct channel and native order id automatically. - If both remote_order_id and channel/external_order_id are provided and they conflict, the request will be rejected with 422 and a descriptive error. - shipments must be an array; otherwise 422 with { "error": "shipments must be an array" }. - The carrier field is validated against the list of allowed carrier codes for the channel’s provider. If the carrier is not recognized, the entire request is rejected with 422 and a descriptive error (e.g., "Carrier is not a valid carrier code for walmart"). Use GET /api/v1/carrier_codes to fetch valid codes. Carrier casing is automatically corrected to the canonical form on save.


SKUs — List item masters

  • GET /api/v1/skus
  • Auth: required

Headers:

Header Type Required
X-Api-Key string yes

Query parameters:

Param Type Required Default Notes
channel string no Channel code filter; when omitted, returns across all channels for the account
since string no ISO8601 filter: items updated at or after this time
status string no (active only) Filter by status. Omit to return only active SKUs. Pass all to include every status, or a specific canonical status (e.g., inactive, archived, draft, retired, unpublished) to filter to that status only.
limit int no 100 Max 500
offset int no 0 Pagination offset

Response body:

Field Type Notes
total integer Total items for the selected scope (channel or all account channels)
limit integer Echo of applied limit
offset integer Echo of applied offset
items array Paged items

Each item (normalized fields + raw):

Field Type Notes
channel string Channel code for this item (useful when querying across all channels)
provider string The channel provider (e.g., shopify, walmart, amazon, ebay)
id integer Internal id
sku string Seller SKU code
title string Human title/name
channel_product_id string Native product identifier in the sales channel
channel_variant_id string Native variant identifier when applicable (nullable)
currency string ISO currency (3-letter) for listed price if provided
price number Listed price (best‑effort)
barcode string Barcode/GTIN/UPC if provided
status string Canonical normalized status: active, inactive, archived, retired, unpublished, or draft
active boolean true when status is active, false otherwise
updated_at_remote string Last updated time from the channel for this product (if available)
external_ids object Provider-specific ids map (legacy; still included)
attrs object Additional attributes captured per provider
raw_payload object Channel-native payload snippet used to populate fields

Example response: { "total": 1234, "limit": 100, "offset": 0, "items": [ { "channel": "shop-us", "provider": "shopify", "id": 55, "sku": "ABC123", "title": "Widget A", "channel_product_id": "gid://shopify/Product/1234567890", "channel_variant_id": "gid://shopify/ProductVariant/111", "currency": "USD", "price": 24.99, "barcode": "012345678905", "status": "active", "active": true, "updated_at_remote": "2025-12-01T12:00:00Z", "external_ids": { "product_id": "1234567890", "variant_id": "111" }, "attrs": { "option1": "Red", "inventory_item_id": 999999 }, "raw_payload": { /* truncated provider payload */ } } ] }

Example: curl -s -H "X-Api-Key: $API_KEY" \ "https://your-host/api/v1/skus?channel=shop-us&since=2025-11-25T00:00:00Z&limit=100&offset=0"

Example without channel (all channels for the account): curl -s -H "X-Api-Key: $API_KEY" \ "https://your-host/api/v1/skus?since=2025-11-25T00:00:00Z&limit=100&offset=0"

Channel Codes

Channel codes are short identifiers you define per tenant (e.g., shop-us, wm-us, amz-na). Many endpoints require channel to scope data.

Notes on channel vs provider fields: - fulfillment_channel indicates the upstream provider of the order (e.g., shopify, walmart, amazon, ebay). - channel_code is the stable, system-defined code of your Channel record in the hub (e.g., the specific Shopify store like acme-us-shop). Use this to map orders to your ERP channels when you have multiple stores on the same provider. - provider is the channel provider type (e.g., shopify, walmart, amazon, ebay). Use this to branch logic in your ERP for provider-specific handling.

Tips & Conventions

  • Always include X-Api-Key except for health checks.
  • Use UTC ISO8601 timestamps.
  • Use reasonable limit values; start with defaults (100) and increase only as needed (max 500).
  • After you import orders/refunds into your ERP, call the respective ack endpoints to prevent re-fetching the same records.

Catalog Sync → Inventory Push requirements

To push inventory reliably using item_master_id, the catalog (SKU) sync must persist certain identifiers into item_masters per channel. The hub’s catalog syncs handle this for you.

  • Shopify
    • Required for inventory pushes: ItemMaster.attrs["inventory_item_id"] per variant.
    • How it’s populated: ShopifyClient.sync_skus! reads Products/Variants and upserts one ItemMaster row per (SKU/variant, location) using GraphQL inventoryItem.inventoryLevels.location. Each row stores attrs.inventory_item_id.
    • Inventory push behavior: push_inventory! uses inventorySetOnHandQuantities and will pick the location from (in order): InventoryUpdate.location_id, ItemMaster.location_id, then channel.settings.location_id.
    • Temporary fallback: channel.settings.inventory_item_id_map[sku] may be used until your catalog is synced.
  • Walmart
    • Inventory push uses SKU only; ensure SKUs exist in item_masters.
    • WalmartClient.sync_skus! upserts ItemMaster with normalized fields (SKU, title, channel_product_id, GTIN/UPC when present).
    • Required channel setting: channel.settings.ship_node (Ship Node ID).
  • Amazon (FBM)
    • Inventory push uses SKU only; ensure SKUs exist in item_masters.
    • A minimal catalog sync should upsert ItemMaster with SKU/title so you can manage by item_master_id consistently.
    • Optional channel setting: channel.settings.marketplace_ids (defaults by region if omitted).
  • eBay
    • Inventory push uses SKU only; ensure SKUs exist in item_masters.
    • EbayClient.sync_skus! upserts ItemMaster with SKU/title and useful attributes (condition, GTIN list).
    • Required channel setting: channel.settings.ebay_inventory_location_key.

Notes: - ERP integrations should prefer storing the hub’s item_masters.id and send item_master_id with inventory updates. SKU-only requests remain temporarily supported but are deprecated. - You can query current catalog via GET /api/v1/skus to inspect stored identifiers and attributes per item.


FBA Inbound Shipments — Sync and query Amazon FBA inbound shipments

List shipments

  • GET /api/v1/fba_inbound_shipments?channel=...
  • Auth: required

Headers:

Header Type Required
X-Api-Key string yes

Query parameters:

Param Type Required Default Notes
channel string no Channel code filter; when omitted, returns across all FBA channels for the account
status string no Filter by shipment status (e.g., WORKING, SHIPPED, IN_TRANSIT, DELIVERED, CHECKED_IN, RECEIVING)
include_pending string no Set to true to include shipments whose line items haven’t been fetched yet

Response body:

Field Type Notes
shipments array Array of shipment records

Each shipment:

Field Type Notes
id uuid Internal id
shipment_id string Amazon FBA shipment id (e.g., FBA16...)
shipment_name string Shipment name from Amazon
destination_fulfillment_center_id string Amazon FC code (e.g., PHX6)
shipment_status string Current status (WORKING, SHIPPED, etc.)
label_prep_type string Label prep type from Amazon
are_cases_required boolean Whether cases are required
items array Line items with SKUs, quantities, and prep details (from SP-API GetShipmentItems)
channel_id integer Channel id (internal)
channel_code string Channel code (e.g., amz-na)
account_id uuid Account id
created_at string ISO8601
updated_at string ISO8601

Each item in items:

Field Type Notes
ShipmentId string Amazon FBA shipment id
SellerSKU string Seller SKU
FulfillmentNetworkSKU string Amazon fulfillment network SKU (FNSKU)
QuantityShipped integer Quantity shipped in this shipment
QuantityReceived integer Quantity received at Amazon FC
QuantityInCase integer Quantity per case (if cases required)
PrepDetailsList array Prep instructions (prep owner, prep type)
ReleaseDate string Release date (if applicable)

Example: curl -s -H "X-Api-Key: $API_KEY" \ "https://your-host/api/v1/fba_inbound_shipments?channel=amz-na"

Example response: { "shipments": [ { "id": "a1b2c3d4-...", "shipment_id": "FBA16ABC123", "shipment_name": "My Shipment", "destination_fulfillment_center_id": "PHX6", "shipment_status": "WORKING", "label_prep_type": "SELLER_LABEL", "are_cases_required": false, "items": [ { "ShipmentId": "FBA16ABC123", "SellerSKU": "MY-SKU-001", "FulfillmentNetworkSKU": "X0012AB1CD", "QuantityShipped": 24, "QuantityReceived": 0, "QuantityInCase": 0 } ], "channel_id": 1, "channel_code": "amz-na", "account_id": "uuid-...", "created_at": "2026-03-27T12:00:00Z", "updated_at": "2026-03-27T12:00:00Z" } ] }

Show shipment (includes raw_data and items)

  • GET /api/v1/fba_inbound_shipments/:id
  • Auth: required

Returns the full shipment record including items (line items with SKUs and quantities) and raw_data (the complete Amazon SP-API payload).

Example: curl -s -H "X-Api-Key: $API_KEY" \ "https://your-host/api/v1/fba_inbound_shipments/a1b2c3d4-..."

Sync shipments from Amazon

  • POST /api/v1/fba_inbound_shipments/sync
  • Auth: required

Triggers a sync from Amazon SP-API. The sync uses a batched job architecture for reliability and rate-limit safety: 1. SyncFbaShipmentsJob (high priority) — fetches only one shipment-list page per run, saves/updates those headers immediately, queues item-fetch batches of 5 shipments, and re-enqueues itself 60 seconds later when Amazon returns a NextToken. If that page itself hits a 429, the job re-enqueues the same page instead of failing permanently. 2. RefreshFbaShipmentStatusesJob (default queue) — after the final header page finishes, refreshes older active shipments in batches of 50 via ShipmentIdList so records can still transition to terminal statuses like CLOSED without bloating the main sync job. It also re-enqueues itself on remaining batches or 429s. 3. FetchFbaShipmentItemsBatchJob (default queue) — processes up to 5 shipments per job with a 2-second delay between each API call, plus a 1-second inter-page delay for multi-page item lists. Has a 60-second job timeout so it never hogs a worker. If a 429 rate limit is hit, the remaining shipments are re-enqueued with a 60-second delay instead of blocking the worker. Non-rate-limit errors are logged (at level warn for rate limits, error for others) and the batch continues with the remaining shipments. SP-API calls retry up to 3 times on 429 with exponential backoff (2s, 4s, 8s). 4. BackfillFbaShipmentItemsJob (recurring, every 15 minutes) — automatically finds non-terminal shipments still missing their line items and re-queues FetchFbaShipmentItemsBatchJob batches (staggered 60s apart). Skips entirely if there are already pending batch jobs to prevent pile-up. This ensures items are eventually obtained even if the original fetch failed due to rate limits or transient errors.

Known issue: FBA Inbound v0 pagination misbehavior

The item-fetch call (GET /fba/inbound/v0/shipments/{id}/items) targets the deprecated v0 endpoint. In production we have observed this endpoint return a non-empty NextToken that never advances — each follow-up call yields a freshly-signed token but the same items. Left unguarded this caused a 60k-job re-enqueue storm (every batch raised, every recurring sync re-tried the same 71 shipments).

Channels::AmazonClient#fetch_shipment_items defends against this with four layers: - Always sends MarketplaceId on every page (some v0 endpoints require it). - De-dupes items by content key (SellerSKU, QuantityShipped, QuantityReceived, FulfillmentNetworkSKU) so a re-served page never inflates totals. - Breaks (gracefully, with a Rails.logger.warn) when the same NextToken is returned twice — cursor not advancing. - Breaks (gracefully) when a page yields zero new items but Amazon still returned a token — definitely spinning. - Last-resort raise at max_pages = 10. Should be unreachable after the above; kept so a brand-new failure mode can’t silently accumulate partial data.

Follow-up — v2024-03-20 migration: Amazon has deprecated FBA Inbound v0 in favor of v2024-03-20, which uses a different model (InboundPlanPlacementOptionShipment, with items accessed via getShipment / listShipmentBoxes rather than getShipmentItemsByShipmentId). There is no 1:1 endpoint replacement; migrating requires schema changes to FbaInboundShipment and a multi-operation orchestration. Tracked as a separate piece of work — the v0 endpoint still functions and the defenses above keep it operationally safe until migration.

The API index endpoint excludes shipments whose items haven’t loaded yet (pass include_pending=true to see them).

The sync has two phases: - Active page sync (DATE_RANGE): Fetches shipments in active statuses (WORKING, SHIPPED, IN_TRANSIT, DELIVERED, CHECKED_IN, RECEIVING) updated within the sync window (default 7 days). Amazon pagination is handled across multiple SyncFbaShipmentsJob runs instead of a single long-running worker. - Status-refresh phase (SHIPMENT): After the active pages are done, RefreshFbaShipmentStatusesJob checks older active-status records in the database that were not touched during this sync and refreshes them by ShipmentIdList in batches of 50. This keeps closure updates targeted and worker-friendly.

Headers:

Header Type Required
X-Api-Key string yes
Content-Type string yes

Request body:

Field Type Required Notes
channel string yes Channel code
days integer no How many days back to sync (default: 7). Use for backfilling old records.

Example: curl -s -X POST -H "X-Api-Key: $API_KEY" -H "Content-Type: application/json" \ -d '{"channel":"amz-na", "days": 60}' \ "https://your-host/api/v1/fba_inbound_shipments/sync"

Response body (202 Accepted):

Field Type Notes
message string Confirmation that the sync was enqueued.

FBA Carton Pushes — Push carton/box contents for FBA inbound shipments

List carton pushes

  • GET /api/v1/fba_carton_pushes?channel=...
  • Auth: required

Headers:

Header Type Required
X-Api-Key string yes

Query parameters:

Param Type Required Default Notes
channel string no Channel code filter; when omitted, returns across all FBA channels for the account
status string no Filter by status (pending, processing, succeeded, failed)

Response body:

Field Type Notes
fba_carton_pushes array Array of carton push records

Each carton push:

Field Type Notes
id uuid Internal id
fba_inbound_shipment_id uuid Linked shipment id
status string pending, processing, succeeded, failed
items array Items in the carton (see below)
error_message string|null Error details if failed
channel_id integer Channel id
account_id uuid Account id
created_at string ISO8601
updated_at string ISO8601

Items array elements:

Field Type Notes
sku string Seller SKU
quantity_shipped integer Quantity shipped
quantity_in_case integer Quantity per case

Example: curl -s -H "X-Api-Key: $API_KEY" \ "https://your-host/api/v1/fba_carton_pushes?channel=amz-na"

Show carton push

  • GET /api/v1/fba_carton_pushes/:id
  • Auth: required

Example: curl -s -H "X-Api-Key: $API_KEY" \ "https://your-host/api/v1/fba_carton_pushes/a1b2c3d4-..."

Create carton push

  • POST /api/v1/fba_carton_pushes
  • Auth: required

Creates a carton push record and enqueues a background job to push the carton contents to Amazon SP-API.

Headers:

Header Type Required
X-Api-Key string yes
Content-Type string yes

Request body:

Field Type Required Notes
channel string yes Channel code
fba_inbound_shipment_id uuid yes ID of the local FBA inbound shipment
items array yes Non-empty array of items
items[].sku string yes Seller SKU
items[].quantity_shipped integer yes Quantity shipped
items[].quantity_in_case integer yes Quantity per case

Example request: curl -s -X POST -H "X-Api-Key: $API_KEY" -H "Content-Type: application/json" \ -d '{ "channel": "amz-na", "fba_inbound_shipment_id": "a1b2c3d4-...", "items": [ { "sku": "ABC123", "quantity_shipped": 24, "quantity_in_case": 6 } ] }' \ "https://your-host/api/v1/fba_carton_pushes"

Response body (201 Created):

Field Type Notes
fba_carton_push object The created carton push record

Example response: { "fba_carton_push": { "id": "b2c3d4e5-...", "fba_inbound_shipment_id": "a1b2c3d4-...", "status": "pending", "items": [ { "sku": "ABC123", "quantity_shipped": 24, "quantity_in_case": 6 } ], "error_message": null, "created_at": "2026-03-27T12:15:00Z", "updated_at": "2026-03-27T12:15:00Z" } }

Notes: - The carton push is processed asynchronously via a background job. Poll the show endpoint or list endpoint to check status. - Status transitions: pendingprocessingsucceeded or failed. - If the push fails, error_message will contain the error details from Amazon.


Carrier Codes — List valid carrier codes per provider

  • GET /api/v1/carrier_codes
  • Auth: required

Returns the list of valid carrier codes that can be used in shipment pushes. Carrier codes are maintained per provider (walmart, ebay, shopify, amazon) and synced periodically from each channel’s API.

Headers:

Header Type Required
X-Api-Key string yes

Query parameters:

Param Type Required Default Notes
provider string no Filter by provider (e.g., walmart, ebay, shopify, amazon)
channel string no Filter by channel code; resolves to the channel’s provider

If neither provider nor channel is specified, returns carrier codes for all providers.

Response body:

Field Type Notes
carrier_codes array List of valid carrier codes

Each carrier code:

Field Type Notes
provider string Provider name (e.g., walmart)
code string Canonical carrier code to use in shipment pushes (e.g., UPS, FedEx)
display_name string Human-friendly label (e.g., United Parcel Service)

Example response: { "carrier_codes": [ { "provider": "walmart", "code": "UPS", "display_name": "UPS" }, { "provider": "walmart", "code": "USPS", "display_name": "USPS" }, { "provider": "walmart", "code": "FedEx", "display_name": "FedEx" } ] }

Example: curl -s -H "X-Api-Key: $API_KEY" \ "https://your-host/api/v1/carrier_codes?provider=walmart"

Example filtered by channel: curl -s -H "X-Api-Key: $API_KEY" \ "https://your-host/api/v1/carrier_codes?channel=wm-us"

Notes: - Use this endpoint to build carrier dropdowns or validate carrier codes in your ERP before submitting shipments. - Carrier codes are synced from each channel’s API on a weekly schedule. An admin can also trigger a manual sync from the hub’s admin interface. - When submitting shipments via POST /api/v1/shipments, the carrier value is validated against this list. Invalid carriers are rejected with 422. - Carrier code lookup is case-insensitive; the hub auto-corrects casing to the canonical form stored here.


Changelog

– 2026-05-20: FBA Inbound Shipments & Carton Pushes: channel parameter is now optional for the list endpoints; when omitted, results span all relevant records for the account. – 2026-05-12: Added taxes_included boolean to order-level response. For Shopify channels, queries shop { taxesIncluded } during sync and caches in channel settings. When true, selling_unit_price subtracts line-level tax before dividing to ensure a consistent tax-exclusive per-unit price. – 2026-05-12: Added selling_unit_price to order line items. This is the actual per-unit selling price after all discounts (line-level and order-level), rounded to 2 decimal places. For Shopify, calculated as (discountedTotalSet - lineTax) / quantity when tax-inclusive, or discountedTotalSet / quantity otherwise. For Walmart, calculated as (productChargeAmount - productDiscounts) / quantity. For eBay/Amazon/ShipStation, equals unit_price (those providers already return the selling price). – 2026-04-21: Added GET /api/v1/carrier_codes endpoint. Shipment pushes now validate the carrier field against allowed carrier codes per provider; invalid carriers return 422. – 2026-04-06: SKUs API now defaults to returning only active SKUs. Pass status=all to include all statuses, or status=<value> to filter by a specific canonical status (active, inactive, archived, retired, unpublished, draft). Status values are now normalized to lowercase canonical values across all channels. – 2026-03-27: Added FBA Inbound Shipments (list, show, sync) and FBA Carton Pushes (list, show, create) API endpoints. – 2026-02-25: Added provider field to orders, skus, and refunds API responses to facilitate provider-specific branching in ERP integrations. – 2025-12-10: Orders API now includes channel_code (the hub Channel/store code) in each order payload. – 2025-12-03: Orders API now documents normalized order fields and embedded line_items; added examples and field tables. – 2025-12-03: SKUs API now includes normalized item master fields including channel_product_id and channel_variant_id; added since filter and examples. – 2025-12-09: Orders and SKUs APIs: channel parameter is now optional; when omitted, results span all channels for the account. – 2025-12-03: Refunds API now includes normalized refund fields and embedded line_items (when available) with examples and provider notes. – 2025-11-26: Initial publication of v1 integration guide.

Line items schema

Each order includes line_items with normalized per‑line details:

Field Type Notes
external_line_id string Channel’s line id if available
sku string SKU from the channel
title string Product title/name
variant_sku string Variant option text or SKU if available
item_master_id integer Hub’s ItemMaster.id for the SKU/variant; null if unresolved
assigned_location_id integer (Shopify) Legacy location id assigned for fulfillment; null if unknown/not applicable
assigned_location_name string (Shopify) Convenience name for assigned_location_id when known
quantity integer Ordered quantity
currency string ISO currency (3-letter)
unit_price number Original list price per unit (before discounts)
selling_unit_price number Actual selling price per unit after all discounts, rounded to 2 decimal places. For Shopify: if the shop is tax-inclusive (taxes_included == true), tax is subtracted before dividing: (discountedTotalSet - lineTax) / quantity; otherwise discountedTotalSet / quantity. For Walmart: (productCharge - productDiscounts) / quantity. For eBay/Amazon/ShipStation: same as unit_price (already the selling price). Always tax-exclusive
total_price number Line total (approximate where channels only give aggregates)
tax_amount number Tax for the line (if provided by the channel)
discount_amount number Discounts applied to the line (if provided)
shipping_amount number Per-line shipping where channels report it
fulfillment_status string Line fulfillment status (channel-specific)
is_gift boolean Gift flag for the line if provided
notes string Free-form notes if provided

Example (truncated):

[ { "id": 123, "external_order_id": "1001", "currency": "USD", "total_amount": 59.98, "line_items": [ { "external_line_id": "l_1", "sku": "SKU-RED-S", "title": "Tee Shirt", "quantity": 2, "currency": "USD", "unit_price": 24.99, "selling_unit_price": 24.99, "total_price": 49.98, "tax_amount": 4.50 } ] } ]

Notes: - Amazon line items are fetched via the OrderItems API to provide accurate SKU/qty/pricing details. - Some providers do not expose all components per-line; amounts are best-effort and may be computed from available fields. - item_master_id is populated during import using per-channel identifiers (e.g., Shopify variant_id) or by SKU. Historical rows can be backfilled; if the hub could not match a line, this field may be null. - Shopify assigned_location_* fields are derived from GraphQL fulfillmentOrders.assignedLocation and are best-effort: - They may be null for unassigned/unsupported orders (e.g., digital), or when Shopify splits an order across multiple locations and a line cannot be mapped confidently. - Depending on Shopify API version and schema, the location legacy id may not be present directly; the hub will fall back to extracting it from the location GID when needed.