Configure
Registering an endpoint
curl -X POST https://api.joinotify.com/webhook-endpoints \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{
"name": "production",
"url": "https://your-server.com/webhooks/joinotify",
"events": ["messages"]
}'
{
"data": {
"id": "whe_01J8ZK...",
"name": "production",
"url": "https://your-server.com/webhooks/joinotify",
"events": ["messages"],
"active": true,
"secret": "whsec_AbCdEf0123456789abcdefgh"
},
"warning": "Store the \"secret\" now — use it to validate the X-Joinotify-Signature-256 header."
}
Unlike API keys, a webhook secret can be read again later:
curl https://api.joinotify.com/webhook-endpoints/whe_01J8ZK.../secret \
-H 'Authorization: Bearer sk_live_xxx'
URL requirements
In production we reject:
http://— HTTPS only- Private hosts:
localhost,127.*,10.*,192.168.*,169.254.*,172.16-31.*,*.internal,*.local
This is SSRF protection: without it, an endpoint pointing at your internal network would turn into a scanner.
For local development, use a tunnel:
ngrok http 3000
Filters
curl -X POST https://api.joinotify.com/webhook-endpoints \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{
"name": "support-store-1",
"url": "https://your-server.com/webhooks/store-1",
"events": ["messages", "smb_message_echoes"],
"phoneNumberId": "106540352242922"
}'
| Field | Effect |
|---|---|
events: [] | Subscribes to every event |
events: ["messages"] | Only what's in the list |
wabaId | Only events from that business account |
phoneNumberId | Only events from that number |
Account events (account_update, account_alerts) and template events
(message_template_status_update) have no number attached. An endpoint filtered by
phoneNumberId will not receive them.
If you want everything, keep one endpoint unfiltered.
Multiple endpoints
Nothing stops you from having more than one — and it's usually a good idea:
# One for the product
{ "name": "app", "url": "https://app.example.com/webhooks", "events": ["messages"] }
# Another for auditing, subscribing to everything
{ "name": "audit", "url": "https://logs.example.com/webhooks", "events": [] }
Each endpoint has its own secret, its own queue and its own failure counter. A broken endpoint doesn't affect the others.
Testing
curl -X POST https://api.joinotify.com/webhook-endpoints/whe_01J8ZK.../test \
-H 'Authorization: Bearer sk_live_xxx'
Fires a synthetic event and delivers it right away, bypassing the queue. The response carries the HTTP status and body your server returned — the fastest way to confirm your signature validation is correct.
{
"data": {
"id": "whd_01J8ZM...",
"status": "delivered",
"responseStatus": 200,
"responseBody": "OK",
"durationMs": 142
}
}
The test payload is a messages event with joinotify_test: true — filter on that field
if your handler writes everything to the database.
Updating
curl -X PATCH https://api.joinotify.com/webhook-endpoints/whe_01J8ZK... \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{ "events": ["messages", "smb_message_echoes", "history"] }'
Omitted fields are left as they are. Reactivating a disabled endpoint (active: true)
resets the failure counter.
Pausing and removing
# Pause — keeps history and secret
curl -X PATCH https://api.joinotify.com/webhook-endpoints/whe_01J8ZK... \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{ "active": false }'
# Remove — discards pending deliveries
curl -X DELETE https://api.joinotify.com/webhook-endpoints/whe_01J8ZK... \
-H 'Authorization: Bearer sk_live_xxx'
For maintenance, pause. Removing is permanent.
Rotating the secret
curl -X POST https://api.joinotify.com/webhook-endpoints/whe_01J8ZK.../rotate-secret \
-H 'Authorization: Bearer sk_live_xxx'
The switch is immediate and there is no grace period where both secrets work. Two ways to do it without losing events:
No downtime — make your handler accept two secrets for a while: validate against the new one and, if that fails, against the old one. Rotate, confirm, drop the old one.
Accepting failures — rotate and publish the new secret. Deliveries that fail in between are retried automatically within 60 seconds.