Skip to main content
A CRM event is something that happened to a contact, recorded on their ticket’s timeline. Keebai already emits its own — an appointment was confirmed, a payment link went out — and this surface lets your systems emit theirs: an order closed in your ERP, a visit registered at the front desk, a delivery completed. Events matter for two reasons. They give whoever picks up the conversation the full history of the customer in one place, and they give the AI agent the same context, so it stops asking about things your systems already know.

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.
An event is append-only. It records what was true at that moment and is never edited afterwards, which is what makes the timeline trustworthy as a history.

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 to crm: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, its data 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.
Both errors carry the offending keys in 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.
The write overwrites. A field that already had a value is replaced, not merged and not skipped. If two systems emit events touching the same field, the last one to arrive wins.The event’s own payload is untouched by this — it keeps the exact values it carried, so the history stays intact even after the ticket has moved on.
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.
If the contact does not exist, the request fails with 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 an idempotency_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.