Each store you connect gets its own webhook URL and signing secret. Your store POSTs a JSON order event to the URL, signed with HMAC-SHA256 over the timestamp and the raw body. We check the signature, store the event, answer within a moment, and decide whether to call. Any store or middleware that can do this works.
Connect a store#
- In orders.edesy.in, open Stores and add a store.
- Fill in the fields:
| Field | Required | What it is |
|---|---|---|
| Name | Yes | Your label for this store in the portal |
| Store name | No | Spoken in the call, for example "this is Acme Store calling" |
| Action URL | No | An https:// URL where we POST confirm, cancel and other results. Leave it empty to use outcomes in the portal only. See Action Requests and Placeholders |
| Webhook signing secret | Yes | Leave blank and we generate a strong one. It is shown once after you create the store, so copy it then |
- Copy the webhook URL from the store's Setup tab. It looks like:
https://voice-api.edesy.in/v1/hooks/<token>
The token is long and random. Keep the URL private; the signature is still checked on every request.
Store credentials and secrets are encrypted at rest and are never returned by the API; the portal only shows whether each one is set.
Send an order event#
POST JSON with Content-Type: application/json. Amounts are in paise.
{
"event": "order.created",
"event_id": "evt_7f3a9c",
"order": {
"id": "10452",
"name": "#10452",
"customer_name": "Priya Sharma",
"phone": "+919800000000",
"payment_method": "cod",
"total_paise": 111000,
"cod_amount_paise": 111000,
"cod_confirmed": false,
"items": [{ "title": "Cotton Kurta", "quantity": 1 }]
}
}
| Field | Notes |
|---|---|
event |
order.created, order.cancelled or order.paid. See Store Events |
event_id |
Your delivery id. A repeat with the same id is ignored. You can send it as the X-Edesy-Event-Id header instead |
order.id |
Your stable order id. Used to match later events and in the results we send back |
order.name, order.number |
What the customer hears as the order number. Either one is enough |
order.customer_name |
Spoken in the greeting |
order.phone |
The customer's Indian mobile number, ideally with +91. Orders without a valid Indian number are skipped |
order.payment_method |
cod or prepaid. The default call rule only calls cod |
order.total_paise |
Order total in paise |
order.cod_amount_paise |
Cash to collect, read out in the call. Falls back to total_paise |
order.cod_confirmed |
true if the COD order is already confirmed; the default rule skips it |
order.cancelled |
true if the order is already cancelled; it is skipped |
order.items |
[{ "title", "quantity" }], optional; used for the item count condition |
order.email, currency, status, tags, created_at |
Optional. tags can be used in conditions |
Sign the request#
Add two headers. The signature is a hex HMAC-SHA256 of the timestamp, a dot and the raw request body, keyed with your webhook signing secret, prefixed with sha256=.
X-Edesy-Timestamp: 1760256000
X-Edesy-Signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + raw_body)>
X-Edesy-Timestamp is unix seconds. Requests more than 5 minutes old are rejected.
import crypto from "node:crypto";
const body = JSON.stringify(event);
const ts = Math.floor(Date.now() / 1000).toString();
const signature = "sha256=" + crypto
.createHmac("sha256", process.env.EDESY_WEBHOOK_SECRET)
.update(ts + "." + body)
.digest("hex");
await fetch(process.env.EDESY_WEBHOOK_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Edesy-Timestamp": ts,
"X-Edesy-Signature": signature,
},
body,
});
Sign the exact bytes you send. Re-serialising the JSON after signing changes the body and breaks the signature.
Responses#
We acknowledge first and do the work afterwards, so the response is fast.
| Status | Body | Meaning |
|---|---|---|
200 |
{"ok": true, "id": "..."} |
Stored; it will be processed in a moment |
200 |
{"ok": true, "deduped": true} |
Same event received before; nothing new happens |
200 |
{"ok": true, "ignored": "paused"} |
The store is paused in the portal. Answered with 200 so your platform does not disable the webhook |
400 |
{"error": "..."} |
The body could not be read as an order event |
401 |
{"error": "invalid signature"} |
Signature missing, wrong or too old |
404 |
{"error": "unknown webhook"} |
The URL token is wrong or was rotated |
413 |
{"error": "body too large"} |
Body over 1 MB |
429 |
{"error": "rate limited"} |
Too many requests for this store at once; retry shortly |
503 |
{"error": "try again"} |
We could not store it; retry |
A rejected signature or unreadable body is also shown on the store as its last error, so you can see it in the portal.
Rotate the webhook URL#
On the store's Setup tab, Rotate issues a new URL. The old URL stops working immediately, so paste the new one into your store straight away.
To change the signing secret, enter a new one in the store's settings and update your store to sign with it.
Pause a store#
Pause on the store stops new calls from its orders. Webhooks are still answered with 200 and marked ignored. Resume turns it back on. A store cannot be deleted while a call rule uses it.
FAQ#
Do I have to send a signature?#
Yes. Every request needs a valid X-Edesy-Signature; unsigned requests are rejected with 401.
What happens if my store sends the same webhook twice?#
The second delivery is recognised by event_id (or, without one, by its type and body) and answered with deduped: true. Each order also gets only one confirmation journey per call rule.
What phone number format should I send?#
An Indian mobile number with the country code, for example +919800000000. Numbers outside India are not called.
Can I send events from Zapier, Make or n8n?#
Yes, if the tool can compute an HMAC-SHA256 signature and send custom headers.