# Dynamic Routing Webhook

> Let your own server decide, per call, who a masked number connects to. When a call arrives we POST a signed request to your endpoint and bridge to the number it returns.

Source: https://edesy.in/docs/number-masking/sessions/dynamic-routing

Dynamic routing lets **your** server decide, in real time, who a masked number connects to on each call — instead of pre-registering a fixed mapping. Point a number at your HTTPS endpoint; when a call arrives, we ask your endpoint for the target and bridge the call to whatever it returns (or reject it).

This is ideal for round-robin agent assignment, time- or location-based routing, and blocking abusive callers — any decision your own systems already know how to make.

## Enabling it

Set a number's routing mode to `webhook` and give it your endpoint. See [Enroll Your Own Numbers](https://edesy.in/docs/number-masking/sessions/enroll-numbers) for the `PATCH /masking/numbers/:id/routing` call, including `webhook_url`, `webhook_method`, `webhook_secret`, and `fallback_internal`.

## The request we send

When a call reaches a webhook-mode number, we POST a JSON body to your endpoint:

```json
{
  "event": "masking.route",
  "call_sid": "d28be7cd-9840-4b23-9627-199f1ef81fc6",
  "caller": "9876543210",
  "masked_number": "918071387146",
  "direction": "inbound",
  "timestamp": "2026-07-14T18:00:00Z"
}
```

| Field | Description |
|-------|-------------|
| `event` | Always `masking.route`. |
| `call_sid` | The call's id (empty for test events). |
| `caller` | The normalized number of the person calling. |
| `masked_number` | The virtual number that was dialed. |
| `direction` | `inbound`. |
| `timestamp` | RFC 3339 UTC. |

### Signature verification

If you set a `webhook_secret`, we sign the raw request body with **HMAC-SHA256** and send it in the `X-Edesy-Signature` header, formatted as `sha256=<hex>`. Recompute the HMAC over the exact body you received and compare to confirm the request genuinely came from us.

### Limits

- **Timeout:** your endpoint must respond within **5 seconds** — the caller is on the line while you decide.
- **Response size:** we read at most **8&nbsp;KB** of your response.
- **Method:** `POST` (default) or `GET`.

## The response we expect

Reply with JSON telling us what to do:

```json
{
  "action": "connect",
  "target_number": "9123456789",
  "caller_id": "918071387146"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `action` | string | `connect` (default when a `target_number` is present) or `reject`. |
| `target_number` | string | Number to bridge to. Required when `action` is `connect`. |
| `caller_id` | string | Optional caller-ID override. Defaults to the masked number. |
| `reject_reason` | string | Optional message played to the caller when `action` is `reject`. |

To turn a call away, return:

```json
{ "action": "reject", "reject_reason": "This line is closed. Please try again during business hours." }
```

## Fallback

If your endpoint errors, times out, or returns a non-2xx status, behaviour depends on the number's `fallback_internal` setting: when enabled, we fall back to the number's internal mapping; when disabled, the call is rejected. Set this based on how critical it is that calls always connect.

## Testing

Use `POST /masking/numbers/:id/routing/test` to send a synthetic `masking.route` event to your endpoint and see the HTTP status, latency, raw body, and the decision we parsed — so you can validate your integration before going live.

## Related

- [Enroll Your Own Numbers](https://edesy.in/docs/number-masking/sessions/enroll-numbers)
- [Event Webhooks](https://edesy.in/docs/number-masking/sessions/event-webhooks) — for lifecycle notifications (distinct from routing)
