Skip to main content

Event webhooks

Event webhooks push activity from Yellow.ai to your own HTTPS endpoint as it happens, so you react to changes instead of polling for them. You register an endpoint, subscribe it to the events you care about, and we POST each one to you — with automatic retries, per-attempt delivery logs, and health tracking that disables an endpoint that has been failing for too long.

Today the events available are Inbox events — ticket changes, the messages sent on a ticket, and SLA deadlines. The event list in the product is the source of truth for what you can subscribe to.

note

This is outbound: we call your endpoint. If you want the opposite — Yellow.ai receiving a notification from your system — see Custom Webhook.

This is the push counterpart to the Inbox Ticketing API: instead of polling for changes, you receive them within seconds.

Early access

Webhooks are turned on per agent rather than per account, so an account with several agents has it enabled for each one separately. If you don't see the Webhooks tile under Integrations, reach out to our team.

How it works

  1. Something happens to a ticket — an agent resolves it, a tag changes, a note is added.
  2. We match the event against your endpoints. An endpoint receives it only if it is enabled and subscribed to that event type.
  3. We POST the event to your URL and read your response.
  4. On failure we retry on a fixed schedule, backing off as attempts continue.
  5. Every attempt is written to Delivery logs with its status, latency and reason.
  6. An endpoint that fails continuously for 48 hours is disabled automatically, and we email you when that happens.

Events that match no endpoint are discarded — there is no queue waiting for you to subscribe later.

Set up an endpoint

The Webhooks screen lists your endpoints with their status, health, event count and when each last succeeded or failed.

Webhooks listing showing one endpoint with its URL, an enabled status toggle, health, event count and last success time, above a search box and status filter

  1. Go to Integrations and open the Webhooks tile under Tools.

  2. Select New endpoint.

  3. Fill in the form:

    FieldNotes
    NameHow the endpoint appears in the list.
    DescriptionOptional — what the endpoint is for.
    Webhook URLMust be https:// and publicly reachable — see Which URLs are accepted.
    AuthenticationSee Authentication.
    Request timeout3,000–30,000 ms. Default 10,000 ms.
    EnabledTurn off to pause delivery without deleting the endpoint.
  4. Under Events, pick the events this endpoint should receive. Search filters the list; Select all takes everything. At least one is required.

  5. Select Create endpoint.

The endpoint dialog: description, webhook URL and authentication on the left, the searchable Inbox event picker on the right with all 25 events selected

One endpoint per event source, per agent. All the events above come from one source — the Inbox — so an agent has one Inbox endpoint. Creating a second one is rejected while the first exists: edit that endpoint instead, or delete it and start again. Paused and auto-disabled endpoints still hold the slot, since both can be switched back on.

The cap is five endpoints per agent, and it exists for the event sources we add next rather than for today's list. Endpoints created before the one-per-source rule keep working and keep delivering; the rule only applies to new ones.

Changing an endpoint's event selection takes effect on the next event; it does not backfill.

Send a test event

The Send test event action on any endpoint posts a sample payload immediately and reports the HTTP status, latency and result. It's the fastest way to confirm your URL, firewall rules and response handling before real traffic arrives.

Test events are not recorded in Delivery logs.

Events

Twenty-five events are available, all under the Inbox category.

Ticket events

EventFires when
Ticket createdA new ticket is created.
Ticket assignedThe ticket is assigned to an agent.
Ticket reassignedThe ticket moves from one agent to another.
Ticket transferredThe ticket moves to a different group.
Status changedThe status changes with no more specific event — for example moving to On hold or back to the queue.
Ticket resolvedThe ticket is marked resolved.
Ticket reopenedA resolved ticket is reopened.
Ticket closedThe ticket is closed.
Ticket mergedThe ticket is merged into another ticket.
Collaborators changedA collaborator is added or removed.
Priority updatedThe ticket priority changes.
Tags updatedThe ticket tags change.
Custom field updatedA custom field on the ticket changes.
Note addedA note is added to the ticket.

Conversation events

The message events carry the message itself — its text, its author, and any files attached. The two inactivity events describe the ticket rather than a message, so they carry ticket data and no message fields.

EventFires when
Agent messageAn agent sends a message on the ticket.
Customer messageA customer sends a message on the ticket.
Agent attachmentAn agent sends a message carrying one or more files.
Customer attachmentA customer sends a message carrying one or more files.
Agent inactiveThe assigned agent has not replied within the inactivity window.
Customer inactiveThe customer has not replied within the inactivity window.

A message with files raises the attachment event rather than the message event, never both — so subscribe to both if you want every message.

The two inactivity events describe the side that has gone quiet, and the window runs from the other side's last message. So Agent inactive means the customer wrote last and no reply followed; Customer inactive means the agent wrote last. The timeouts are set per agent under Inbox settings. Each is sent once per quiet period, not repeatedly — a new message on the ticket resets it, and a reply prevents it entirely.

Internal notes do not raise conversation events; they raise Note added instead. Automated replies, typing indicators, and messages that have been redacted or flagged as sensitive are never sent.

SLA events

Available on accounts using SLA policies. If a ticket has no SLA policy applied, none of these fire.

EventFires when
SLA assignedA policy is applied to a ticket, starting its clocks.
SLA updatedDeadlines are recalculated — for example after a priority change.
SLA due approachingA deadline is near and a reminder is raised.
SLA breachedA deadline passes without being met.
SLA escalatedA breached SLA triggers an escalation.

SLA assigned is sent once per ticket and carries every clock. The rest are sent per clock, and only when something needs attention — there is no event for a deadline being met.

note

The picker only lists events that are actually emitted today. If an event isn't in the list, nothing would ever be sent for it — so you can't subscribe to silence.

Payload

Each delivery is an HTTPS POST with Content-Type: application/json.

User-Agent: YellowRelay/1.0
X-Yellow-Event-Type: ticket.resolved
X-Yellow-Event-Id: f51197fc-b067-47ee-827a-3a5aafdbed7c
X-Yellow-Delivery-Id: 4f3ff359-80f3-4fac-8ef8-5ae620b36160
X-Yellow-Delivery-Attempt: 1
X-Yellow-Timestamp: 1786719867
X-Yellow-Bot-Id: x1693289786207

X-Yellow-Event-Type lets you route a delivery before parsing the body.

The two ids are different on purpose. X-Yellow-Event-Id identifies the event and stays the same across every retry — dedupe on it. X-Yellow-Delivery-Id identifies this individual POST and is new each time, so you can tell a retry apart from a genuinely repeated event.

{
"specVersion": "1.0",
"eventId": "f51197fc-b067-47ee-827a-3a5aafdbed7c",
"eventType": "ticket.resolved",
"eventCategory": "ticket_lifecycle",
"occurredAt": "2026-08-14T15:04:26.845Z",
"botId": "x1693289786207",
"actor": { "type": "AGENT", "id": "[email protected]", "name": "Priya R" },
"delivery": {
"id": "4f3ff359-80f3-4fac-8ef8-5ae620b36160",
"attempt": 1,
"subscriptionId": "8d7333cf-c7b1-4675-8d26-0bc5f8e0bf34"
},
"data": {
"ticket": {
"ticketId": "101519",
"status": "RESOLVED",
"categoryId": "resolved",
"priority": "URGENT",
"subject": "Refund not received",
"assignedTo": "[email protected]",
"createdAt": "2026-08-14T11:14:21.489Z",
"resolvedAt": "2026-08-14T15:04:26.000Z"
}
},
"changes": { "old": "ASSIGNED", "new": "RESOLVED" }
}

data is keyed by the entity the event is about — data.ticket for ticket and SLA events, data.message for conversation events. New event families will add their own key rather than changing the existing ones, so read the key you care about rather than assuming a fixed shape.

The sample above is abridged. data.ticket also carries the group, source, ticket type, tags, language, reopen count and the rest of the lifecycle timestamps, and a few values appear under a second name as well, for compatibility with older integrations. Read the fields you need and ignore the rest — we add fields to data without notice, and never remove one you already rely on.

SLA events carry their clocks. Alongside the ticket, an SLA event adds slaTarget (which clock the event is about), slaClockStatus, slaDueAt, slaPaused, and sla — every clock on the ticket with its target, status and deadline.

Every event carries dueAt and slaStatus for the ticket's nearest running clock: dueAt is the absolute deadline, slaStatus the seconds remaining against it — negative once the deadline has passed. Both are computed when the event is raised, so on a retry the remaining time is as of the original attempt, while dueAt does not drift. Neither is sent when no clock is running.

Fields that have no value are omitted, not sent empty. A missing resolvedAt means the ticket is not resolved — it does not mean we failed to populate it.

changes carries the before and after of what moved, when the event describes a change. status uses our ticket vocabulary as-is and is not remapped.

actor is who caused the event: an AGENT with an id and name for anything a person did, or BOT/SYSTEM for automated changes.

Size limits

Message text is capped at roughly 1,500 bytes and truncated with an ellipsis, so a long email body arrives shortened rather than whole. If an event still exceeds our internal size budget, data is reduced to the ticket id alone and the event is marked dataTruncated. Both are rare, and both mean the same thing: fetch the full record from the Inbox Ticketing API when you need every character.

Attachments

On a conversation event, data.message carries the text, the message id, and any files sent with it:

"attachments": [
{
"attachment_url": "https://…",
"fileName": "receipt.pdf",
"contentType": "application/pdf",
"size": "20481"
}
]

contentType is derived from the file name, and size is only recorded on channels that report it. A message carries at most one file per kind — file, image, video, audio — so an event lists up to four.

Responding

Return a 2xx as soon as you have accepted the event. Don't do slow downstream work before responding, or you risk hitting the request timeout and being sent a retry for an event you already processed.

If your response body contains a resultInfo object we read it. resultStatus of "S" is success. "F" is a failure we treat as temporary and retry, unless it carries a resultCode we know to be permanent — INVALID_TICKET_STATUS today — which is given up on immediately. A plain 2xx with no resultInfo is treated as success.

Delivery guarantees

At-least-once, so build for duplicates. eventId — also sent as the X-Yellow-Event-Id header — is the same on every retry of an event. Use it as your idempotency key. X-Yellow-Delivery-Id changes on each attempt, so don't deduplicate on that.

Order is best-effort, not guaranteed. A retried event can arrive after a newer one. If sequence matters, order on the event's own timestamp rather than on arrival.

No replay or backfill. Once an event is dropped — because nothing was subscribed, the endpoint was disabled, or every attempt failed — it is not re-delivered, and there is no way to request it again. For workflows where a missed event is costly, reconcile periodically against the Inbox Ticketing API.

Retries

Every event gets up to 12 attempts — the first, plus 11 retries:

RetryDelay after the previous attempt
115 seconds
25 minutes
330 minutes
42 hours
54 hours
6–118 hours each

Each delay carries up to ±20% of randomness so a backlog to one endpoint doesn't arrive as a single burst. After the twelfth failure the event is given up on.

Retry-After wins. If your response carries a Retry-After header — on a 429 or 503, for example — we schedule the next attempt then instead of using the table, with a floor of 5 seconds. A Retry-After above 24 hours is refused and the event is dropped, on the grounds that a day-late ticket event is no longer useful.

What gets retried:

RetriedGiven up immediately
Timeouts and connection errors400, 410, 422 and other malformed-request responses
429 rate limits501, 505
All 5xx except 501/505Redirects — 301, 302 and any other 3xx
401, 403, 404, 408, 425

401, 403 and 404 are retried because in practice they're temporary — a rotated credential, or a route that's missing for the length of a deploy — rather than a permanent refusal.

Retries are visible per attempt in Delivery logs. Here one ticket.status_changed event is retried through five 500s and accepted on the last attempt — the attempt number climbs, the event keeps its resource id, and the final row records the 200:

Delivery logs filtered to one event, showing six rows for the same resource id: five Retried rows with HTTP 500 and a server error reason, then a Success row with HTTP 200

Endpoint health

StateMeaning
UnknownNothing delivered yet to judge on.
HealthyThe last delivery succeeded.
DegradedRecent failures, still delivering.
FailingFive consecutive failures. Delivery pauses for a 5-minute cooldown, then resumes — we stop hammering an endpoint that is clearly down.
Deregistered48 hours of continuous failure. The endpoint is disabled and stops receiving events.

The 48-hour clock starts at the first failure and resets on any success. Rate limiting (429) never counts against endpoint health, so an endpoint that only ever rate-limits us is never disabled for it.

While an endpoint is deregistered its events are discarded, not held.

You are emailed when an endpoint is disabled

Deregistration is the one thing we notify you about, because it is the only state where events stop reaching you and nothing recovers on its own. The email goes to whoever created the endpoint and names it, the reason it failed, and when it last succeeded.

Nothing is sent for the states above it — an endpoint that is degraded or failing is still being retried, and most recover without anyone doing anything.

note

The email is sent to the address of the user who created the endpoint. If that person has left your organisation, nobody is notified — so check the Endpoint health column periodically rather than relying only on the email.

Re-enable a deregistered endpoint

Fix the endpoint, then select Re-enable on its row. This sends a test event and reactivates the endpoint only if that delivery succeeds — an endpoint that is still broken cannot be switched back on. Health reads Unknown until the first real delivery lands.

Delivery logs and metrics

Delivery logs record every attempt: timestamp, event type, resource id, status, result, attempt number, HTTP status, latency, payload size and failure reason. Filter by status or event type, search the failure reason, open a row for detail, or export the current view to CSV — see Exporting delivery logs.

Delivery logs table listing ticket events by name — ticket created, SLA assigned, priority updated — each with its status, attempt number, HTTP code, latency and payload size

Health metrics shows delivered, succeeded and failed counts, retries, success rate, average latency, last success and last failure, plus a delivered-vs-failed trend.

Health and metrics screen led by the success rate, with delivered, succeeded, failed, retries and average latency beside it, last success and last failure below, above a delivered-versus-failed bar chart

note

Logs are retained for 30 days. We do not store the response body your endpoint returns — only its status code and, where present, resultInfo.

Exporting delivery logs

Export produces a CSV of the rows your current filters select — not just the page on screen. There is one row per delivery attempt, so an event that was retried four times appears five times, sharing an eventId.

Exports are prepared in the background, however large or small. Nothing downloads in the browser — we email you a download link when the file is ready, so you can leave the page. There is no limit on how many rows an export can contain.

Filtering by status before you export is worth doing: failures alone are usually both quicker to produce and closer to the question being asked than every attempt for every event.

Because logs are kept for 30 days, an export cannot reach further back than that however wide a date range you choose.

Exports run in a service that isn't deployed in every region. Where it's unavailable, Export reports that rather than queueing a file — the on-screen logs and filters are unaffected.

Security

  • HTTPS only. http:// URLs are rejected.
  • Your endpoint must be publicly reachable — see Which URLs are accepted.
  • Credentials are never returned by the API. Where you supply one, it is shown only as a masked last-4 hint after saving.
  • Response bodies from your endpoint are not persisted.

Which URLs are accepted

We deliver from inside our network, so a webhook URL that points at a private address would let a webhook reach services that aren't yours. Every URL is therefore checked when you save it, and again on each delivery.

RejectedExamples
Anything other than httpshttp://, ftp://
Private, loopback and link-local addresses127.0.0.1, 10.0.0.5, 172.16.0.1, 192.168.1.10, 169.254.169.254
Carrier-NAT, multicast and reserved ranges100.64.0.1, 224.0.0.1
The IPv6 equivalents::1, fe80::1, fd00::1, and IPv4-mapped forms like ::ffff:10.0.0.1
Credentials in the URLhttps://user:[email protected]/hook

Two things worth knowing:

  • Writing the address a different way doesn't get around the check. Decimal, hexadecimal and octal spellings of the same address — https://2130706433/, https://0x7f000001/, https://0177.0.0.1/ — are all read as 127.0.0.1 and rejected.
  • Hostnames are judged on what they resolve to, not on how they look. A public name pointing at an internal address is refused when we try to deliver. If the name resolves to several addresses and any of them is internal, the delivery is refused rather than sent to one of the others.

Redirects are not followed. If your endpoint answers 301 or 302, that response is the delivery outcome — point the endpoint at its final URL instead.

Saving a URL that fails these checks returns RELAY_WEBHOOK_URL_NOT_ALLOWED, so you find out immediately rather than after events start failing.

note

Delivering to an endpoint inside a private network — over PrivateLink or VPC peering, say — is possible but not self-service. Reach out to our team; it needs an explicit allow-list entry for your host.

Authentication

TypeWhat we send
Signature (RSA)Nothing yet — see below.
NoneNo authentication header.
Bearer / Basic / API keyYour credential, as Authorization or X-Api-Key.
Authenticate us by allow-list for now

Neither mechanism above is active yet. Signing is not implemented — no signature is sent, and there is no public key to verify against. Transport credentials you enter are held as a masked last-4 hint and are not yet attached to deliveries, so requests arrive without an Authorization or X-Api-Key header.

Until both ship, identify us by allow-listing our egress IPs or by putting an unguessable path segment in your webhook URL. Choosing a credential type now is still worth doing — your endpoints start sending it the moment the mechanism lands, with no change on your side.

Troubleshooting

Nothing is arriving. Check, in order: the endpoint is Enabled; it hasn't been Deregistered; the event you expect is actually ticked in its event list; and Delivery logs for that endpoint. An event that matched no endpoint leaves no trace, so an empty log usually means the subscription didn't match — not that delivery failed.

Deliveries show as failed with 2xx. Your response body contains a resultInfo with resultStatus: "F". We classify on that, not on the status code.

Duplicates. Expected under at-least-once delivery — deduplicate on eventId.

Timeouts. Raise Request timeout (up to 30,000 ms), or acknowledge immediately and move the work off the request path. Timeouts count against endpoint health and will eventually deregister the endpoint.

The endpoint was disabled on its own. That's the 48-hour deregistration, and the email you received about it names the reason and the last successful delivery. Fix the cause, then Re-enable — it will only come back if a test event succeeds.

Two emails about the same endpoint. Each deregistration sends one email, so a second means the endpoint was re-enabled and then failed for another full window, not that the first was duplicated.

Creating an endpoint is rejected because another one already covers these events. An agent gets one endpoint per event source, and the message names the endpoint holding it — including a paused or auto-disabled one. Edit that endpoint to change its URL or its event selection, or delete it and create the new one.

Saving fails with RELAY_WEBHOOK_URL_NOT_ALLOWED. The URL isn't publicly reachable — most often a private or loopback address, or http:// instead of https://. See Which URLs are accepted. If the host is genuinely inside a private network reachable from us, contact our team.