Skip to content
Docs

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.

  1. 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.

  2. 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.

  3. 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/json
Keys are scoped to one shop. Rotating a key takes effect immediately, so mint the replacement before you revoke the old one.

Server-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_id

    string · required

    Your own reference. Sent back with the result untouched.

  • phone

    string · required

    Bangladeshi mobile in any common format — 01723306206, +8801723306206, and 8801723306206 all work.

  • callback_url

    string · required

    HTTPS endpoint where we POST the result when the call ends.

  • customer_name

    string

    Spoken in the script as @customer_name.

  • amount

    number

    Order total in BDT. Becomes @total_price.

  • items

    array

    [{ name, qty }] — becomes @products and @quantity.

  • variables

    object

    Any key here becomes an @variable your script can speak, e.g. { "courier": "Pathao" } → @courier.

  • meta

    object

    Opaque to us. Returned verbatim on the webhook.

Response

202
202 Accepted{  "call_id":  "b35e7c84-2f19-4a6d-9b0e-5c1d7a3e8f42",  "order_id": "ORD-10231",  "status":   "queued",  "will_dial": "+8801723306206"}
Keep call_id — it is how the result identifies itself when it comes back, and how you de-duplicate a retry.

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.

order_confirmation_result
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_id

    string · required

    The reference you sent. Match on this to find the order.

  • call_id

    string · required

    Our id for the call. De-duplicate on it — a result can arrive twice.

  • result

    string · required

    The outcome. One of the values below — act on this, not on the digit.

  • dtmf_digit

    string

    The key the customer actually pressed, or null if they pressed none.

  • billsec

    number

    Billed seconds of connected audio. 0 when nobody answered.

  • cost

    number

    What the call cost you, in BDT.

  • answered_at

    string

    ISO-8601 UTC timestamp of pickup, or null when unanswered.

  • meta

    object

    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);
Hash the raw request body, not a re-serialised object — key order and whitespace both change the digest.

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.

Welcome script

আসসালামু আলাইকুম। আপনি @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_name

    customer_name

  • @quantity

    items — total qty

  • @products

    items — spoken names

  • @total_price

    amount

  • @order_id

    order_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.

Error body
{ "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

    Store

    New orders call themselves; status writes back on the press.

  • Shopify

    Store

    Confirmed and cancelled orders tagged automatically.

  • Custom website

    Store

    Built your own storefront? Point it at one URL and new orders call themselves.

  • Facebook page

    Social

    Selling from comments and Messenger? Those orders get called too, same as any other channel.

  • REST API & Webhooks

    Any platform

    Not 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.