Order payloads, webhook events, and the call API
Two calls to build against: you POST an order, we ring the customer in Bangla and POST the result back. Everything else on this page is detail you can skip until you need it.
- Base URL
- https://api.echo.bd/v1
- Auth header
- X-API-Key
- Encoding
- JSON over HTTPS, UTF-8
- Webhook retries
- 4, backing off
Quickstart
Three steps, one afternoon. If your store already fires a webhook on new orders, step two is a single HTTP call away.
Mint an API key
Dashboard → Developers → Create key. The plaintext key is shown once and stored only as a hash, so copy it into your server's environment before you close the dialog.
POST the order
One request per new order, from your server. We answer immediately and place the call in the background — nothing about your checkout has to wait on a phone ringing.
Handle the webhook
When the call ends we POST the outcome to your callback_url. Verify the signature, act on result, and reply 2xx. That is the whole integration.
Authentication
Every request carries an API key in an X-API-Key header. Keys are minted in Dashboard → Developers and shown exactly once — we store only a hash, so a lost key is replaced rather than recovered.
X-API-Key: ek_live_9f2c... # server-side onlyContent-Type: application/jsonServer-side only. A key in browser or mobile-app code can place calls on your balance. Orders should reach us from your backend, never from a checkout page.
Send an order
POST /v1/orders queues the call and answers immediately — your checkout never waits on a phone ringing. One request per order.
curl -X POST https://api.echo.bd/v1/orders \ -H "X-API-Key: $ECHO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "order_id": "ORD-10231", "phone": "01723306206", "callback_url": "https://yourshop.com/echo/result", "customer_name": "Rahim Uddin", "amount": 1250, "items": [ { "name": "Honey 500g", "qty": 2 }, { "name": "Cotton Pants", "qty": 1 } ], "meta": { "warehouse": "dhaka-1" } }'Body
order_idstring · required
Your own reference. Sent back with the result untouched.
phonestring · required
Bangladeshi mobile in any common format — 01723306206, +8801723306206, and 8801723306206 all work.
callback_urlstring · required
HTTPS endpoint where we POST the result when the call ends.
customer_namestring
Spoken in the script as @customer_name.
amountnumber
Order total in BDT. Becomes @total_price.
itemsarray
[{ name, qty }] — becomes @products and @quantity.
variablesobject
Any key here becomes an @variable your script can speak, e.g. { "courier": "Pathao" } → @courier.
metaobject
Opaque to us. Returned verbatim on the webhook.
Response
202 Accepted{ "call_id": "b35e7c84-2f19-4a6d-9b0e-5c1d7a3e8f42", "order_id": "ORD-10231", "status": "queued", "will_dial": "+8801723306206"}Receive the result
When the call ends we POST the outcome to your callback_url, signed. This is the half of the integration that decides whether a parcel gets packed, so it is worth building carefully.
POST https://yourshop.com/echo/resultX-EchoBD-Event: order_confirmation_resultX-EchoBD-Signature: hmac-sha256 of the raw body{ "order_id": "ORD-10231", "call_id": "b35e7c84…", "result": "confirmed", "dtmf_digit": "1", "billsec": 18, "cost": 1.90, "answered_at": "2026-07-16T10:15:50Z", "meta": { "warehouse": "dhaka-1" }}order_idstring · required
The reference you sent. Match on this to find the order.
call_idstring · required
Our id for the call. De-duplicate on it — a result can arrive twice.
resultstring · required
The outcome. One of the values below — act on this, not on the digit.
dtmf_digitstring
The key the customer actually pressed, or null if they pressed none.
billsecnumber
Billed seconds of connected audio. 0 when nobody answered.
costnumber
What the call cost you, in BDT.
answered_atstring
ISO-8601 UTC timestamp of pickup, or null when unanswered.
metaobject
Whatever you sent on the order, returned unchanged.
Reply 2xx fast
Acknowledge first, do your work in a queue. A slow handler is indistinguishable from a failed one, and we will retry a call you already shipped.
De-duplicate on call_id
Retries mean the same result can land twice. Treat the handler as idempotent and a duplicate becomes a no-op instead of a second parcel.
Trust result, not the digit
You can remap the keypad in Dashboard → Call flow at any time. Branch on result and today's 1 becoming tomorrow's 3 costs you nothing.
Verify the signature
// Laravel — verify before you act on the body.$expected = hash_hmac('sha256', $request->getContent(), config('services.echo.secret'));abort_unless( hash_equals($expected, $request->header('X-EchoBD-Signature')), 401);Call results
Six outcomes cover every call. Branch on these rather than on the keypad digit — you can remap keys whenever you like, and result is what stays stable.
- confirmed
The customer confirmed the order on the keypad. Ship it.
- cancelled
The customer cancelled. Do not pack the parcel.
- forwarded
The customer asked for a person and was bridged to your team's number. The order is undecided until someone on your side says otherwise.
- no_input
The call was answered but no key was pressed before the script ran out.
- no_answer
Nobody picked up. Retries follow the schedule in your call flow.
- failed
The call could never be placed — an unreachable number or a carrier failure. Nothing was charged for the attempt.
Keypad options you add yourself report their own status names alongside these — a “call me back” key returns callback, not one of the six.
Keypad & script
What the customer actually hears, and what each key does. Every line is editable in Dashboard → Call flow; this is the flow a new shop starts with.
আসসালামু আলাইকুম। আপনি @quantity টি @products অর্ডার করেছিলেন, ডেলিভারি চার্জ সহ মোট @total_price টাকা। অর্ডারটি কনফার্ম করতে ১ চাপুন, কোনো প্রশ্ন থাকলে ২ চাপুন, আর ক্যান্সেল করতে ৩ চাপুন। ধন্যবাদ।
confirmedEnds the call and reports confirmed.ধন্যবাদ। আপনার অর্ডারটি কনফার্ম করা হয়েছে।
forwardedBridges the live call to the number you set on the menu.একটু অপেক্ষা করুন, আমাদের প্রতিনিধির সাথে সংযোগ করে দিচ্ছি।
cancelledEnds the call and reports cancelled.অর্ডারটি বাতিল করা হয়েছে। আমাদের সাথে থাকার জন্য ধন্যবাদ।
Up to ten menus, on digits 0–9.
Variables
Anything you send on the order can be spoken back. These five are derived for you; every key you put in variables becomes one too.
@customer_namecustomer_name
@quantityitems — total qty
@productsitems — spoken names
@total_priceamount
@order_idorder_id
Errors & retries
Failures arrive as an HTTP status and a single human-readable detail string — there is no second error-code vocabulary to learn.
{ "detail": "phone is not a valid Bangladeshi mobile number" }- 202
Queued. The call is placed after your configured delay.
Store call_id and wait for the webhook.
- 400
The payload did not validate.
Read detail in the body — it names the field.
- 401
Missing, revoked, or malformed X-API-Key.
Mint a fresh key in Dashboard → Developers.
- 402
Not enough balance to place the call.
Top up in Dashboard → Billing, then retry.
- 429
Too many orders too quickly.
Back off and retry — the queue is not lost.
- 5xx
Our problem, not your payload.
Retry with backoff. Placing the same order_id twice is safe.
Webhooks we cannot deliver are retried four times with backoff. A handler that answers 2xx and queues its work will never see them.
No-code options
If your store runs on a platform we already speak, none of the above is work you have to do — you paste a URL and orders start calling themselves.
WooCommerce
StoreNew orders call themselves; status writes back on the press.
Shopify
StoreConfirmed and cancelled orders tagged automatically.
Custom website
StoreBuilt your own storefront? Point it at one URL and new orders call themselves.
Facebook page
SocialSelling from comments and Messenger? Those orders get called too, same as any other channel.
REST API & Webhooks
Any platformNot on a listed platform? One POST places the call, one webhook brings the result back.
Something here not answering your question?
Tell us what your store runs on and we will send back the exact payload it should be sending — or the plugin that means it does not have to.