Sending customer events
POST /customer-events — what happens with a customer outside a call.
A call is one thing that happens with a customer. A WhatsApp message, an email, an order, a support
ticket and a note in your CRM are others. POST /customer-events lets any system tell Callens
about one of them, so that it is stored on the same customer as their calls.
There is one endpoint and one shape for every kind of event. Callens has no connector for WhatsApp, a shop or a CRM: whatever system holds the event sends it — a few lines in n8n, Zapier, or your own backend.
{
"customer": {
"phone": "+201001234567",
"email": "ahmed@example.com",
"external": [{ "namespace": "shopify", "id": "82931" }],
"name": "Ahmed Mohamed"
},
"event": {
"type": "message_received",
"channel": "whatsapp",
"occurred_at": "2026-10-04T14:32:00Z",
"content": "ممكن أعرف السعر؟",
"source": "n8n",
"external_id": "wamid.HBgMMjAxMDAxMjM0NTY3"
}
}One event per request. There is no batch endpoint.
Before you start
Two things must be true, and each has its own refusal.
- The key has the
eventsscope. Mint one under Settings → API keys. A key that can only submit calls cannot submit events, and a key that can only submit events cannot submit calls. See authentication. - Your plan includes customer events. If it does not, every request is refused with
403andcustomer_events_not_in_plan. Retrying does not help; ask for the feature to be added.
Keep the key on a server you control. Never put a key with the events scope in a browser, a
mobile app, or any page or script a visitor can read. The response tells the caller whether a phone
number, an email address or an id already belongs to one of your customers, and returns that
customer's id. Anyone holding the key can ask that question about anyone, and can add events and
identifiers to your customers. If something in the browser must report an event — a form submitted,
a page viewed — send it to your own backend and let the backend call Callens.
The customer
Send at least one of phone, email and external, and send every one you have.
| Field | Notes |
|---|---|
phone | A phone number, at most 32 characters. See phone numbers below. |
email | An email address. Compared in lower case and otherwise exactly as written: a.b@gmail.com and ab@gmail.com are two addresses. Printable ASCII only. |
external | Up to five { "namespace", "id" } pairs: the customer's id in another system. namespace is that system's name as a lower-case slug, such as shopify or hubspot. id keeps its case. |
name | The customer's name. The name on the most recently accepted event is the one kept. It is not written when the identifiers name two different customers. |
Callens looks each identifier up, exactly as written. Nothing is matched by name, by similarity or by guesswork.
| What Callens finds | What happens |
|---|---|
| None of them is known | A customer is created with all of them. |
| They name one customer | The event is stored on that customer, and the identifiers that were new are added to it. |
| They name two different customers | The event is stored on the first, in the order phone, email, then external as you sent them. Nothing is added to anyone, nothing is merged, and the response says "conflict": true. |
Phone numbers
Callens reads a number into international form, so 01001234567, 00201001234567,
201001234567 and +201001234567 are one customer.
A number written in national form, such as 01001234567, is read in your organization's own
country. That country is a reading rule. It is not a way to find out where a customer is.
If you serve customers in more than one country, always send phone numbers in international
format (+201001234567). A national-form number from another country would be read as a number in
yours.
A string that cannot be read as a phone number — an extension, a short code — is kept as written and matches only the identical string.
The event
| Field | Notes |
|---|---|
type | Required. What happened. One of the types below; anything else is refused. |
occurred_at | Required. When it happened, by your clock. Not more than 5 minutes in the future. Any past date is accepted. |
source | Required. The name of the system sending the event, as a lower-case slug such as n8n. callens is reserved. |
external_id | Required. Your id for this event, at most 128 characters of printable ASCII. See retries. |
channel | Where it happened, as a lower-case slug: whatsapp, sms, email, web, chat, facebook, instagram, shop, crm, or any other slug. Not the same as source: n8n can be the source of a WhatsApp message. |
direction | inbound is from the customer; outbound is to the customer. Filled in for you when the type implies it. A value that contradicts the type is refused. |
content | The words a person would read, up to 8,000 characters. |
metadata | Any JSON object, at most 4 KB. Stored as submitted and never interpreted. |
content is text
content is the message text, the note, or one line describing what happened, such as
"Order 1234 placed, 5,000 EGP".
Callens stores that sentence as it is. It does not parse it. From "Order 1234 placed, 5,000
EGP" Callens does not conclude that an order with id 1234, an amount of 5,000 or a currency exists,
and an order_created event does not create an order anywhere. If you want something understood,
say it in content in words.
What content is used for
Besides appearing on the customer's timeline, the content of an event can inform the analysis of
that customer's later calls: when a call is analysed, the most recent events that came before it are
given to the model as background, beside the customer's earlier calls. Only content is used, cut
to its first 300 characters — never metadata and never the identifiers in customer. It is sent
to the AI provider as a call's transcript is. An event whose identifiers named two different
customers is kept on the timeline and is not used this way.
The same is true while a call is in progress. When a call arrives over Live Connect and says who is calling, the line Callens suggests to the agent can be written with that customer's most recent events as background, under the same limits and the same exclusion. The agent may read that line aloud.
It can also inform a summary of the customer. Where the customer summary is switched on, Callens
writes a short account of where things stand with a customer and shows it at the top of the journey
card on the customer's page. It is written from the same background — the customer's calls and most
recent events — under the same limits and the same exclusion, and content is sent to the AI
provider for it as it is for a call's analysis. The summary is derived from the record and does not
outlive it: it is written again as the record changes, and when a call or an event is deleted, what
was written from it goes with it.
So put in content what you are happy for the analysis of the next call to have read, for an agent
on the next call to be prompted by, and for a summary on the customer's page to be written from.
What type, channel and direction are used for
The customer's page also carries a journey card: who had the last word, through which channels you
deal with this customer, and how often. Those are computed when the page asks for them, from the
customer's calls and events of the last 90 days, and are stored nowhere. (The written summary at the
top of the card, where it is switched on, is the one part of it written from content, and the one
part that is kept; it is described above.) Of an event the rest of the card reads type, channel,
direction and occurred_at — never content or metadata — so what you send in those fields
decides what it says:
directiondecides who had the last word. A customer whose newest contact is aninboundevent is shown as awaiting your reply; one whose newest contact isoutbound, as awaiting theirs. A call answers whatever came before it. An event with no direction — an order, a visit — is counted, answers nothing, and is nobody's last word. So senddirectionwith a chat, form or ticket event when it is the customer speaking or being spoken to.channelis what it is counted under. Without one, an event is counted under the category of its type. An event you send on the channelcallis counted with the customer's calls.- A
note_addedevent is counted as a note, not as contact.customer_createdandcustomer_updateddescribe the record and are not counted.
Types
| Category | Types |
|---|---|
| Message | message_received, message_sent |
email_received, email_sent | |
| Chat | chat_started, chat_ended |
| Web | website_visited, product_viewed, form_submitted, search_performed |
| Commerce | order_created, order_updated, order_cancelled, payment_completed, checkout_started, checkout_abandoned |
| Support | ticket_created, ticket_updated, ticket_resolved |
| Appointment | appointment_booked, appointment_cancelled |
| Note | note_added |
| Customer | customer_created, customer_updated |
message_received and email_received are inbound; message_sent and email_sent are outbound.
Calls are not events. Send a call with POST /calls.
Retries
An event is identified by its source and external_id together. Sending the same pair again
returns the event that was stored the first time, with 200, and changes nothing — not the event,
not the customer, not the name. An event is never updated: a retry that carries different content
gets the stored event back unchanged.
external_id must be stable. Use the id the event already has where it comes from, such as the
WhatsApp message id. If the system the event comes from gives it no id, generate one, store it, and
send the same value on every retry of that event. A fresh random id on each attempt makes every
retry a new event.
first request external_id = abc123 → 201, event stored
retry external_id = abc123 → 200, the same eventResponses
{
"success": true,
"message": "Event accepted.",
"data": {
"id": "gQ7bYx",
"customer_id": "k3Vn8p",
"identity": { "matched_on": "phone", "customer_created": false, "conflict": false }
}
}201 for a new event, 200 for a retry. Both ids are opaque strings.
identity.matched_on is the kind of identifier that named the customer — phone, email or
external — or new when storing this event created the customer. phone_raw means the phone
string could not be read as a number and was matched as written.
Refusals
| Status | error | What it means |
|---|---|---|
| 401 | — | The API key is missing, malformed, unknown or revoked. |
| 403 | insufficient_scope | The key was not minted with the events scope. Never retry; fix the key. |
| 403 | customer_events_not_in_plan | Your plan does not include customer events. Never retry; ask for the feature. |
| 422 | — | A field is wrong. The body is { "message", "errors" } and names each field. Nothing was stored. |
| 429 | — | Too many requests: 600 a minute and 20 a second for each key. |
The key and its scope are checked first, then the fields, then the plan.
How long events are kept
Events are kept for as long as your organization keeps calls. An event is removed once
occurred_at is older than your retention period, so an event you send today about something that
happened before that period is removed in the next nightly run. When a customer has no call and no
event left, the customer record is removed with them.
A customer's events are erased with the customer when you file an erasure request for them.