Skip to content

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

EventFires whendata payload
contact.createdA new contact appears (any source: inbound message, opt-in page, webhook, API){ contact }
contact.opted_outA contact opts out (STOP & friends){ contact }
message.receivedAn inbound WhatsApp message arrives{ contact, message: { wa_message_id, type, text, is_new_contact } }
form.submittedA WhatsApp form (flow) is completed{ contact, flow_id, answers }
tag.addedA tag is newly attached to a contact{ contact, tag }
message.sentAny 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.handoverThe 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.stoppedThe AI agent is in charge of a chat but does not answer an inbound message, or switches itself off{ contact, agent, reason }
agent.errorAn 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

Terminal window
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:

HeaderMeaning
X-Nybero-EventThe event type
X-Nybero-DeliveryUnique delivery id (use it for idempotency)
X-Nybero-Signaturesha256=<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

  1. Subscribe agent.handover, agent.stopped and agent.error — each one means a human should look at that chat. Alert your team (Slack, e-mail, a task) with contact.name, contact.phone and reason.
  2. 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.
  3. 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

  1. Subscribe once: POST /v1/subscriptions with events: ["contact.created", "form.submitted"].
  2. Your endpoint verifies the signature, then upserts the contact into your CRM; on form.submitted it also stores the answers.
  3. Nothing polls, nothing is missed — retries cover short outages on your side.