Webhook and Signing Secret

Connect a store to COD Order Calls - your webhook URL, the JSON order payload, the HMAC-SHA256 signing scheme with X-Edesy-Timestamp and X-Edesy-Signature, response codes and how to rotate the URL.

5 min read

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#

  1. In orders.edesy.in, open Stores and add a store.
  2. 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
  1. 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.

Was this page helpful?