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

Source: https://edesy.in/docs/order-calls/connect-your-store/webhook-and-signing-secret

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](https://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](https://edesy.in/docs/order-calls/connect-your-store/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 |

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

```json
{
  "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](https://edesy.in/docs/order-calls/connect-your-store/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.

```js
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.
