The Commerce API lets any store send its events to Kanal: orders, abandoned checkouts, customers and shipments. This data then feeds your WhatsApp automations, without going through Shopify.
Unlike the Messaging API, it does not require an active subscription. The full reference is on developers.getkanal.com.
store_id and an ingestion key limited to that store. See Connect a non-Shopify store (custom store).Every URL starts with https://api.getkanal.com/api/v1/stores/{store_id}/.
You do not need to know whether a record already exists in Kanal. You send the complete object, and Kanal creates or updates it. Each resource is identified by the (store_id, external_id) pair.
external_id is your identifier: order number, checkout token, customer ID. Send the same external_id again and Kanal updates the existing record instead of creating a duplicate.
201 Created: Kanal saw this external_id for the first time.
200 OK: an existing record was updated.
Both return the same JSON body: you can handle them the same way.
POST on an order or a checkout can trigger the automations attached to that resource again.POST /api/v1/stores/{store_id}/ordersTriggers your post-purchase automations. Required fields: external_id and total_price.
Optional fields:
placed_at (ISO 8601 format): without this field, Kanal uses the time the order was received.
customer: the customer object, with phone, external_id, first_name, last_name, email, locale, tags, lifetime_value and orders_count.
currency, financial_status, fulfillment_status, line_items, tags, metadata.
customer.phone is saved, but it does not trigger any automation. Always send the customer's phone number if the order should lead to a message.curl -X POST https://api.getkanal.com/api/v1/stores/123/orders \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "ORDER-1001",
"placed_at": "2026-05-18T10:00:00Z",
"total_price": 49.90,
"currency": "EUR",
"customer": { "phone": "+33612345678", "first_name": "Marie" }
}'An order does not create a customer record. To sync your customers' profiles (tags, lifetime value, order count), also send them through POST /customers.
To change only the status of an existing order, use PATCH /orders/{external_id}. Only financial_status, fulfillment_status, tags and metadata can be changed. An order that does not exist returns a 404 error.
curl -X PATCH https://api.getkanal.com/api/v1/stores/123/orders/ORDER-1001 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "financial_status": "paid" }'POST /api/v1/stores/{store_id}/checkoutsFeeds your checkout recovery automations. Required fields: external_id, abandoned_at, total_price and the customer object with its phone.
Optional fields: recovery_url, currency, line_items, tags, metadata, as well as the other fields of the customer object (external_id, first_name, last_name, email, locale, tags).
The recovery_url field is strongly recommended: it is the link the customer will follow to resume their order, the one you will place in your reminder template.
curl -X POST https://api.getkanal.com/api/v1/stores/123/checkouts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "CHECKOUT-7700",
"abandoned_at": "2026-05-18T09:40:00Z",
"recovery_url": "https://shop.example.com/checkout/recover/7700",
"total_price": 49.90,
"currency": "EUR",
"customer": { "phone": "+33612345678", "first_name": "Marie" }
}'abandoned status triggers an automation.PATCH /checkouts/{external_id} lets you update the checkout status, with the values active, abandoned, recovered, completed or expired. It also accepts recovery_url, tags and metadata. Remember to set a checkout to recovered as soon as it converts, to avoid unnecessary reminders.
POST /api/v1/stores/{store_id}/customersSyncs customer profiles, their tags and their lifetime value. Only phone is required. The natural key is external_id, but if you omit it, phone is used as the key. The lifetime_value and orders_count fields must be integers.
curl -X POST https://api.getkanal.com/api/v1/stores/123/customers \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "CUST-42",
"phone": "+33612345678",
"first_name": "Marie",
"tags": ["vip"],
"lifetime_value": 1299,
"orders_count": 7
}'To honor a GDPR erasure request coming from your store, use DELETE /customers/{external_id}. The response is a 204 with no body. The call is idempotent: deleting a customer that is already deleted or unknown also returns 204.
POST /api/v1/stores/{store_id}/shipmentsTriggers your delivery notifications. Required fields: external_id and order_external_id, which links the shipment to the matching order.
Optional fields: tracking_number, tracking_url, carrier, metadata, and status among pending, in_transit, delivered, failed or returned.
tracking_number and tracking_url, or if the linked order is unknown to Kanal or has no contact. In these cases, the shipment is still saved, without an error.curl -X POST https://api.getkanal.com/api/v1/stores/123/shipments \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "SHIP-9001",
"order_external_id": "ORDER-1001",
"tracking_number": "1Z999AA10123456784",
"tracking_url": "https://track.example.com/1Z999AA10123456784",
"carrier": "Colissimo",
"status": "in_transit"
}'There is no PATCH for shipments: send a POST again with the same external_id to update the tracking or the status.
The Commerce API is limited to 500 requests per minute per store. This budget is shared across all the endpoints of the same store. If you exceed it, the API returns a 429 with a Retry-After header, and the response body contains retry_after: 60. X-RateLimit-* headers are present on every response so you can track your usage.
Three principles to stay under the limit: send a complete upsert rather than several partial PATCH calls, push your data when it changes instead of polling the API, and apply exponential backoff on 429 errors. If you expect sustained volume above 500 requests per minute, for example a historical import or a flash sale, let your Kanal contact know in advance to have the limit raised.
Errors come in two forms. Problems detected before validation (key, store, limit) return a simple error string. Invalid payloads return a 422 with a message and an errors map detailed by field, in dot notation for nested objects. Example for a checkout sent without a phone number:
{
"message": "The customer.phone field is required.",
"errors": {
"customer.phone": ["The customer.phone field is required."]
}
}Code | Meaning | Retry? |
|---|---|---|
| Successful update or creation | Not applicable |
| Deletion done, no body | Not applicable |
| store_id parameter missing from the URL | No |
| Missing, invalid or unauthorized key | No |
| Unknown or paused store, or record not found | No |
| Validation failed: fix the payload | No |
| Rate limit exceeded | Yes, after Retry-After |
| Temporary server error | Yes, with exponential backoff |