A webhook tells you when the status of a number changes. CallerAPI sends a POST request to your URL. Each request contains one event.Webhooks send only changes. To get the full results for all numbers, send GET /api/did-monitoring/v1/numbers.Webhooks are not available on the Free plan. An account has one webhook URL.
Event types#
type | When |
|---|
number.flagged | A number becomes flagged. The previous status was pending, clean, or unknown. |
number.cleared | A flagged number becomes clean. |
There are no other status events. A first result of clean, a change to unknown, and a change from unknown to clean do not send an event. See the Statuses and labels page.Event fields#
| Field | Meaning |
|---|
id | The event ID, for example evt_8f3c2a1e9b7d4c60. Use it to find duplicate deliveries. |
type | number.flagged or number.cleared. |
created_at | When CallerAPI made the event. RFC 3339 in UTC. |
data.phone | The number in E.164 format. |
data.status | The new status. |
data.previous_status | The status before this check. |
data.label | The label of the new status. |
data.checked_at | When the check finished. Use it to put events in order. |
Examples#
{
"id": "evt_8f3c2a1e9b7d4c60",
"type": "number.flagged",
"created_at": "2026-10-29T08:02:11Z",
"data": {
"phone": "+13058901598",
"status": "flagged",
"previous_status": "clean",
"label": "Spam or scam risk",
"checked_at": "2026-10-29T08:02:11Z"
}
}
{
"id": "evt_1a7d5e0c3f9b2468",
"type": "number.cleared",
"created_at": "2026-11-02T08:01:47Z",
"data": {
"phone": "+13058901598",
"status": "clean",
"previous_status": "flagged",
"label": "No flags",
"checked_at": "2026-11-02T08:01:47Z"
}
}
Test event#
POST /api/did-monitoring/v1/webhook/test sends one test event. It has the same headers and the same signature as a real event. It is not a status change.{
"id": "evt_5c0e7b2a9d4f1836",
"type": "webhook.test",
"created_at": "2026-10-29T08:00:00Z",
"data": {"ok": true}
}
Reply to it with a 2xx status. Do not store it as a status change.| Header | Value |
|---|
Content-Type | application/json |
User-Agent | CallerAPI-DID-Monitoring/1 |
CallerAPI-Event-Id | The event id. |
CallerAPI-Timestamp | The time when CallerAPI signed the request, in Unix seconds. |
CallerAPI-Signature | v1=<hex>. HMAC SHA256 of <timestamp>.<raw body>. The key is your webhook secret. |
The webhook secret starts with whsec_. Use the full secret as the key, with the whsec_ part.Verify the signature#
Do these steps for each request:1.
Read the raw bytes of the body. Do not parse the JSON first.
2.
Read CallerAPI-Timestamp. Reject the request if the time is more than 5 minutes from your clock.
3.
Make the string <timestamp>.<raw body>. Use a dot between the two parts.
4.
Calculate the HMAC SHA256 of this string with your secret. Write it as lowercase hex. Put v1= in front.
5.
Compare the result with each value in CallerAPI-Signature. Use a constant time compare.
6.
Accept the request if one value is equal.
In Node.js, rawBody must be the raw body as a Buffer. For example, use express.raw({ type: "application/json" }) on the webhook route.Rotate the secret#
POST /api/did-monitoring/v1/webhook/rotate-secret returns a new secret. CallerAPI shows it one time only.For 24 hours after a rotation, CallerAPI-Signature contains two values, separated by a comma:CallerAPI-Signature: v1=<signature with the new secret>,v1=<signature with the old secret>
The code samples above accept the request when one value is equal. Put the new secret on your receiver in the 24 hours. After 24 hours, only the new secret signs.Delivery rules#
1.
Reply with a 2xx status in 10 seconds. Any other status, a timeout, or a redirect is a failed delivery.
2.
CallerAPI sends each event at least one time. The same event can arrive more than one time. Use id to find duplicates.
3.
Events can arrive out of order. Use data.checked_at to put them in order.
4.
Do not depend on a fixed source IP address. Verify the signature instead.
Retries#
After a failed delivery, CallerAPI tries again after these intervals:| Attempt | Time after the previous attempt |
|---|
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 hours |
| 7 | 12 hours |
| 8 | 24 hours |
There are 8 attempts in total, over about 45 hours. After the eighth attempt, CallerAPI stops the delivery of that event.When CallerAPI turns off your webhook#
CallerAPI turns off the webhook after 50 failed delivery attempts in a row, or after 3 days of failures. CallerAPI then sends an email to the account email address.GET /api/did-monitoring/v1/webhook shows "active": false and a note with the reason. To turn the webhook on again, fix your endpoint. Then save the URL again with PUT /api/did-monitoring/v1/webhook.While the webhook is off, CallerAPI does not deliver events. Send GET /api/did-monitoring/v1/numbers to get the current status of all numbers.URL rules#
| Rule | Value |
|---|
| Scheme | https only |
| Port | 443 or 8443 |
| Host | Must resolve to a public address. Private, loopback, link-local, and reserved addresses are not accepted. |
| Host names | localhost and names that end in .localhost, .local, or .internal are not accepted. |
| Redirects | Not followed. A redirect is a failed delivery. |
| User name or password in the URL | Not accepted. |
| Length | 2,048 characters at most |
CallerAPI checks these rules when you save the URL. CallerAPI checks them again before each delivery.