API: Webhook events
Webhook subscriptions push events from Nybero to your systems the moment they happen — the mirror image of inbound webhooks. This is what powers instant triggers in tools like Zapier and Make, and what you use to keep your own backend in sync without polling.
See the API overview for authentication and errors.
Event types
| Event | Fires when | data payload |
|---|---|---|
contact.created | A new contact appears (any source: inbound message, opt-in page, webhook, API) | { contact } |
contact.opted_out | A contact opts out (STOP & friends) | { contact } |
message.received | An inbound WhatsApp message arrives | { contact, message: { wa_message_id, type, text, is_new_contact } } |
form.submitted | A WhatsApp form (flow) is completed | { contact, flow_id, answers } |
tag.added | A tag is newly attached to a contact | { contact, tag } |
message.sent | Any outbound message — from the AI agent, a teammate, an automation, a broadcast or the API | { contact, message: { id, wa_message_id, type, text, template, status, sender, user_id, automation_run_id, broadcast_id, created_at } } |
agent.handover | The AI agent hands the chat over — to the team (it can’t help, the customer wants a person, a teammate replied manually) or to another agent | { contact, agent, reason, to, detail } |
agent.stopped | The AI agent is in charge of a chat but does not answer an inbound message, or switches itself off | { contact, agent, reason } |
agent.error | An AI agent reply failed for good (after all retries) | { contact, reason, error, attempts } |
contact is the same object the Contacts API returns (incl. last_inbound_at / last_outbound_at). GET /v1/events lists the available types programmatically.
Senders and reasons
message.sent → sender is agent, team (user_id = the teammate), automation, broadcast, api, business_app (typed in the WhatsApp Business app) or system. Broadcasts send one event per recipient — subscribe only if your endpoint can take the volume.
agent.handover → reason: agent_decision (the agent handed over; detail says why), human_reply (a teammate answered manually, the agent pauses), agent_switch (another AI agent takes over the topic). The AI agent hands over automation trigger fires on agent_decision, so you can route the chat with the Assign conversation step.
agent.stopped → reason: window_closed, no_ai_plan, wallet_low, no_channel, daily_cap, reply_cap, idle_timeout, no_intent (the agent ended a chat without buying intent), empty_reply.
Subscribe — POST /v1/subscriptions
curl -s https://app.nybero.com/api/v1/subscriptions \ -H "Authorization: Bearer $NYBERO_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/nybero-hook", "events": ["contact.created", "form.submitted"]}'{ "id": "7f3a…", "url": "https://example.com/nybero-hook", "events": ["contact.created", "form.submitted"], "secret": "9c41…", "active": true, "created_at": "2026-07-09T17:02:11Z"}The url must be a public https endpoint. Up to 20 active subscriptions per workspace. Store the secret — you’ll use it to verify deliveries.
Unsubscribe with DELETE /v1/subscriptions/{id} (idempotent, returns 204). GET /v1/subscriptions lists what’s active.
What a delivery looks like
Nybero POSTs JSON to your URL, usually within a few seconds of the event:
{ "id": "e0b2…", "event": "contact.created", "created_at": "2026-07-09T17:05:40Z", "data": { "contact": { "id": "0d9f…", "phone": "+4915112345678", "name": "Ada Example", "opt_in_status": "confirmed", "source": "landingpage", "attributes": {}, "created_at": "2026-07-09T17:05:39Z" } }}Headers on every delivery:
| Header | Meaning |
|---|---|
X-Nybero-Event | The event type |
X-Nybero-Delivery | Unique delivery id (use it for idempotency) |
X-Nybero-Signature | sha256=<HMAC-SHA256 of the raw body, keyed with your secret> |
Respond with any 2xx within 10 seconds. Anything else counts as a failure and is retried with exponential backoff (about 1, 2, 4, 8 and 16 minutes); after five failed attempts the delivery is dropped. Because of retries, the same delivery id can reach you more than once — deduplicate on X-Nybero-Delivery if your handler isn’t naturally idempotent.
Verifying the signature
Always verify before trusting a payload:
import hashlib, hmac
def verify(secret: str, raw_body: bytes, header: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header)Worked example: no chat stays unanswered
- Subscribe
agent.handover,agent.stoppedandagent.error— each one means a human should look at that chat. Alert your team (Slack, e-mail, a task) withcontact.name,contact.phoneandreason. - As a safety net, call
GET /v1/contacts?awaiting_reply=true&inbound_since=…every hour and alert on anything older than your answer-time target. - Inside Nybero, an automation with the trigger AI agent hands over and the step Assign conversation puts the chat straight into the right teammate’s inbox.
Worked example: sync every new lead into your CRM
- Subscribe once:
POST /v1/subscriptionswithevents: ["contact.created", "form.submitted"]. - Your endpoint verifies the signature, then upserts the contact into your CRM; on
form.submittedit also stores theanswers. - Nothing polls, nothing is missed — retries cover short outages on your side.