Commerce Connector API
How a store platform (TotalShop, an ERP, anything that sells) connects to the support desk so agents see a customer’s orders and can refund them from the conversation. The contract is platform-neutral: TotalShop is just the first implementation.
A connection has two directions:
- Events in. The platform posts order and refund events to the desk.
- Actions out. The platform exposes a small HTTP API the desk calls to check refund eligibility, execute refunds, deliver verification codes and look up orders.
Connecting
Each workspace has at most one connection. An owner creates it in the inbox (Settings → Store connection), or a platform that already provisions workspaces creates it through the internal API:
PUT /internal/v1/workspaces/{store id}/commerce (signed with DESK_INTERNAL_SECRET, like other internal calls)
{"platform": "totalshop", "base_url": "https://shop.example.com/desk-connector"}
→ {"connection_id": "…", "secret": "…"} (the secret is shown only when created or rotated)
base_url is where the platform serves the actions below. The secret signs requests in both directions.
Signing
Every request in either direction carries:
| Header | Value |
|---|---|
X-Desk-Connection |
the connection id |
X-Desk-Timestamp |
Unix seconds |
X-Desk-Signature |
hex(HMAC-SHA256(secret, "<timestamp>.<raw body>")) (empty body for GET) |
Reject requests whose timestamp is more than 5 minutes off, and compare signatures in constant time. The desk does both.
Money, ids and time
- Amounts are integers in minor units (
145000= ৳1,450.00) with an ISO 4217currency. - Order and customer ids are the platform’s own, as strings.
- Times are RFC 3339 with an offset.
Events in
POST {desk}/integrations/v1/events
{"events": [ {event}, … ]} (1 to 100 events; a single event object is also accepted)
{
"id": "evt_01HZX…", // unique per connection; repeats are ignored (safe to retry)
"type": "order.updated",
"occurred_at": "2026-11-07T10:15:00+06:00",
"data": { … }
}
Response: 200 {"accepted": n, "duplicates": n}. A 4xx means the batch is malformed (fix it, don’t retry); retry 5xx and timeouts with backoff.
order.created, order.updated
data is the full order. The desk keeps the latest snapshot, judged by updated_at (older updates arriving late are ignored), so send the whole order every time.
{
"id": "1042",
"number": "#1042",
"status": "shipped", // pending | confirmed | paid | processing | shipped | delivered | cancelled | returned | refunded | partially_refunded (others are shown as-is)
"currency": "BDT",
"total_minor": 145000,
"refunded_minor": 0,
"payment": {"method": "bkash", "status": "paid"},
"customer": {"id": "cust-1001", "name": "Farhana Akter", "phone": "+8801812345678", "email": "farhana@example.com"},
"items": [{"sku": "KRT-BL-L", "name": "Blue cotton kurta (L)", "quantity": 1, "unit_price_minor": 145000, "image_url": "https://…"}],
"shipping": {"carrier": "Pathao", "tracking_number": "PTH123", "tracking_url": "https://…", "status": "in_transit", "address": "Mirpur 10, Dhaka"},
"placed_at": "2026-11-05T19:02:00+06:00",
"updated_at": "2026-11-07T10:15:00+06:00",
"admin_url": "https://shop.example.com/admin/orders/1042"
}
Required: id, status, currency, total_minor, updated_at, and at least one of customer.id, customer.phone, customer.email (that’s how the desk matches the order to a conversation). Phones should be E.164.
Cash on delivery and ad tracking. Send payment.method: "cod" for cash-on-delivery orders, and keep them pending until the store has confirmed them (a call, or the customer’s WhatsApp reply), then confirmed. processing does not count as confirmed. The desk sends a COD order to Meta as a Purchase only once it is confirmed (or shipped/delivered, depending on the merchant’s setting); cancelled, returned and refunded orders are never sent. Prepaid orders count once payment.status is paid. Send placed_at: Meta accepts purchases up to 7 days after it.
refund.updated
Reports the outcome of a refund the desk asked for, when it finished asynchronously.
{"refund_id": "<desk refund id>", "status": "succeeded", "external_ref": "bkash-trx-8F3K2", "failure_reason": null}
status is processing, succeeded or failed.
Actions out
The desk calls these on base_url, signed as above, with a 15-second timeout. Answer 2xx with JSON on success. For errors answer {"error": {"code": "…", "message": "…"}}: a 4xx is final, while a 5xx or timeout is retried where it’s safe.
GET /ping
{"ok": true, "platform": "totalshop", "capabilities": {"refunds": true, "verifications": true, "order_search": true, "order_confirmation": true}}
The desk calls this when the connection is saved and on “Test connection”.
POST /refunds/eligibility
→ {"order_id": "1042", "amount_minor": 145000, "reason": "Damaged item"}
← {"eligible": true, "max_refundable_minor": 145000, "methods": [{"id": "bkash", "label": "bKash (original payment)"}, {"id": "manual", "label": "Manual"}], "reason": null}
When eligible is false, reason explains why, and the agent sees it. Your refund rules live here; the desk never decides eligibility itself.
POST /refunds
Executes an approved refund. Must be idempotent on refund_id (also sent as the Idempotency-Key header): the desk retries on timeouts, and a repeated call must return the original result without paying twice.
→ {"refund_id": "…", "order_id": "1042", "amount_minor": 145000, "currency": "BDT", "method": "bkash",
"reason": "Damaged item", "requested_by": "Karim", "approved_by": "Rahim"}
← {"status": "succeeded", "external_ref": "bkash-trx-8F3K2", "message": null}
status is succeeded, failed (with message) or processing. For processing, report the outcome later with a refund.updated event.
POST /verifications
Before an agent acts on an order for a customer the desk can’t already tie to it (a guest, or a phone number that doesn’t match the order), the desk generates a one-time code and asks the platform to deliver it to the contact details on the order. The customer reads the code back in the chat.
→ {"order_id": "1042", "code": "482913", "expires_at": "…", "store_name": "Demo Store"}
← {"delivered": true, "destination": "SMS to +88018••••678"}
The platform chooses SMS or email from the order and sends a message like “Your Demo Store verification code is 482913. It expires in 10 minutes.”. The desk never sees the order’s raw contact details through this call, only the masked destination.
POST /orders/search (optional)
Lets the desk backfill orders the events missed, when an agent opens a customer.
→ {"customer_id": "cust-1001", "phone": "+8801812345678", "email": null, "limit": 10}
← {"orders": [ {order}, … ]}
POST /orders/confirmation (optional, capability order_confirmation)
The customer confirmed or cancelled a cash-on-delivery order by replying to the desk’s WhatsApp confirmation. Update the order and report it with an order.updated event (confirmed or cancelled). Idempotent on Idempotency-Key.
→ {"order_id": "1046", "decision": "confirmed", "channel": "whatsapp", "at": "2026-11-11T10:20:00Z"}
← {"ok": true}
Refund lifecycle in the desk
requested ─► checking ─► awaiting_approval ─► approved ─► processing ─► completed
│ │ │
└─► rejected ◄───┘ └─► failed (an owner can retry)
- checking: the desk calls
/refunds/eligibility. An ineligible order goes to rejected with the platform’s reason. - awaiting_approval: an owner or admin approves or rejects. Refunds created by owners or admins, or under the workspace’s auto-approve amount, skip this.
- processing: a background job calls
/refundswith retries. The platform’s answer, or a laterrefund.updatedevent, decides completed or failed.
Every change is written into the conversation as an internal event and into the audit log.
Order actions need the customer to be tied to the order: a signed-in shopper whose platform customer id matches, a WhatsApp number that matches the order’s phone, or a verification code from this conversation (valid 24 hours). Workspaces can turn verification off.