Below are ready-to-run examples using Chilkat ActiveX from Visual FoxPro 9. Replace YOUR_HOST, YOUR_CHANNEL and YOUR_API_KEY as needed.
* 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":"..."}
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
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, fulfilledstatuses: multiple values via statuses=paid,cancelled or statuses[]=paid&statuses[]=cancelledsince: ISO8601 timestamp; we return orders updated since this time (uses last_remote_update_at)status or statuses is used, we include orders regardless of their imported_to_erp flag.cancelled orders are excluded unless you explicitly set include_cancelled=true (or include_canceled=true).* 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
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.
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}
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
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
/api/v1/ordersHeaders:
| 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:
since (based on last_remote_update_at, with fallback), regardless of import/ack status, and includes canceled orders by default.statuses explicitly to the statuses your ERP wants, or filter on your side by imported_to_erp == false.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"
/api/v1/orders/ackHeaders:
| 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 }
/api/v1/refundsHeaders:
| 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.
/api/v1/refunds/ackHeaders:
| 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 }
/api/v1/inventory_updatesHeaders:
| 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" }.
/api/v1/shipmentsHeaders:
| 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., ups → UPS). |
| 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.
/api/v1/skusHeaders:
| 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 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.
X-Api-Key except for health checks.limit values; start with defaults (100) and increase only as needed (max 500).ack endpoints to prevent re-fetching the same records.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.
ItemMaster.attrs["inventory_item_id"] per variant.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.push_inventory! uses inventorySetOnHandQuantities and will pick the location from (in order): InventoryUpdate.location_id, ItemMaster.location_id, then channel.settings.location_id.channel.settings.inventory_item_id_map[sku] may be used until your catalog is synced.item_masters.WalmartClient.sync_skus! upserts ItemMaster with normalized fields (SKU, title, channel_product_id, GTIN/UPC when present).channel.settings.ship_node (Ship Node ID).item_masters.ItemMaster with SKU/title so you can manage by item_master_id consistently.channel.settings.marketplace_ids (defaults by region if omitted).item_masters.EbayClient.sync_skus! upserts ItemMaster with SKU/title and useful attributes (condition, GTIN list).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.
/api/v1/fba_inbound_shipments?channel=...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"
}
]
}
/api/v1/fba_inbound_shipments/:idReturns 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-..."
/api/v1/fba_inbound_shipments/syncTriggers 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.
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 (InboundPlan → PlacementOption → Shipment, 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. |
/api/v1/fba_carton_pushes?channel=...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"
/api/v1/fba_carton_pushes/:idExample:
curl -s -H "X-Api-Key: $API_KEY" \
"https://your-host/api/v1/fba_carton_pushes/a1b2c3d4-..."
/api/v1/fba_carton_pushesCreates 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: pending → processing → succeeded or failed.
- If the push fails, error_message will contain the error details from Amazon.
/api/v1/carrier_codesReturns 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.
– 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.
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.