Skip to main content

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"]
}'
201 Created
{
"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"
}'
FieldEffect
events: []Subscribes to every event
events: ["messages"]Only what's in the list
wabaIdOnly events from that business account
phoneNumberIdOnly events from that number
Filtering by number discards events without one

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.