The shape of an event
Every event has three parts:A type
The
key you send, like custom.pedido_confirmado. It carries the name, icon, and colour the timeline renders, and it decides which fields the payload may contain.A target
The contact it happened to. The event is stored on that contact’s open ticket — Keebai opens one if they have none.
A payload
The
data object: values for the fields the type declares. Validated on write, then written onto the ticket and the contact.Event types are configured in the portal
There is no public endpoint for creating event types — they live under CRM → Event types in the portal. That screen is also where you get everything you need to emit one: the ingest URL, a generated API key scoped tocrm:events:write, and the exact request body for that specific type.
Every company starts with 13 system types, seeded on first use. They cannot be deleted, and Keebai’s own services emit them:
Types you create yourself get a
custom. prefix automatically when the key you type has no dot in it — pedido_confirmado becomes custom.pedido_confirmado. Emit the full key, prefix included.
A type’s
key is immutable after creation. Renaming the name is free and changes nothing for integrators; the key is the contract.Fields compose the payload
An event type does not define its own private field format. It associates the CRM fields you already have — the same ticket and client custom fields you manage in the portal — and those become its payload contract. That has two consequences worth understanding before you build against it.Validation happens on write
When an event arrives, itsdata is checked against the associated fields:
- Every field marked required must be present, or the request fails with
MISSING_REQUIRED_FIELDS. - Every value present must match its field’s type, or the request fails with
INVALID_FIELD_VALUE.
error.details.fields. The event is rejected before anything is written, so a malformed payload never leaves a half-recorded event behind.
null passes for any type, and a field that is not required may simply be omitted.
Values are written through
This is the part that makes associating real fields worthwhile rather than declaring a private schema: the values reach where the field lives.
So an event carrying
{"monto": 15000}, where monto is a ticket field, records the event and updates the ticket. The ticket’s own crm.ticket.updated outbound webhook fires as a result, and the portal reflects the change in realtime.
Keys in data that do not correspond to an associated field are kept on the event and written nowhere else. They are not an error.
Targeting a contact
You identify the contact with exactly one of four fields, resolved in this order:1
ticket_id
Names the ticket directly. Nothing else is looked up. The ticket must be open.
2
chat_user_id
The contact’s
ObjectId, as returned by GET /v1/contacts.3
customer_id
A CRM customer’s
ObjectId. Resolves to the contact linked to it.4
phone
Digits only, country code included, no
+. The only identifier that requires a lookup outside the CRM, and the only one your external system usually has.NO_CONTACT — Keebai will not invent one from a phone number. Create it with POST /v1/contacts first, or send an id you already hold.
If the contact exists but has no open ticket, one is opened in the default pipeline’s first stage and the event lands on it. This is specific to the public ingest: Keebai’s internal producers skip instead, because they fire best-effort and an external caller does not.
Idempotency
Send anidempotency_key — your own unique identifier for the event, up to 200 characters. Replaying the same key returns the original event with duplicated: true and writes nothing.
It is scoped to your company, so the key only has to be unique within your own account. Use whatever id the event already carries in your system: an order number, a transaction id, a row primary key.
Without it, a network retry records the event twice, and the second one also re-runs the write-through.
Events versus outbound webhooks
Both involve HTTP and both are about things happening, so they are easy to confuse:
They compose: emitting an event that writes a ticket field causes
crm.ticket.updated to be delivered to your subscribers.
Next steps
Emit an event
The full request and response reference, including every
422 code.Find the contact
Look a contact up by phone before emitting, if you would rather not rely on the phone lookup.