Skip to main content

Send Event

Send (ingest) a custom event for a contact. The event name must already be defined for your organization, and the payload is validated against that definition's schema. Accepted events can trigger automations in real time.

Send one event, or a batch of up to 50.

Endpoint

POST /events/send

Returns: 202 Accepted

Headers

HeaderValueRequired
AuthorizationBearer <your-api-key>Yes
Content-Typeapplication/jsonYes
Idempotency-KeyAny unique string, max 256 charsNo — see Idempotency

Body Parameters

ParameterTypeRequiredDescription
eventstringYesName of a defined event for your organization.
external_idstringConditionalYour own identifier for this person (max 255 chars, unique within your organization). Outranks email and phone_number.
emailstringConditionalEmail of the contact the event is about. If no contact matches, one is auto-created when the automation runs.
phone_numberstringConditionalPhone number in E.164 format (e.g. +966501234567). If no contact matches, a phone-only contact is auto-created when the automation runs.
contact_idstringConditionalContact UUID the event is about. Mutually exclusive with external_id/email/phone_number.
dataobjectYesEvent payload (max request body 5 MB). Validated against the event's declared schema — see Payload validation.
timestampstringNoISO 8601 time the event occurred. Defaults to server time. Used for rollup bucketing and echoed in the automation webhook payload — see the warning below.
Identify the contact

Provide at least one of external_id, email or phone_number (you may send several), or contact_id on its own. Combining contact_id with any of the others, or providing no identifier, returns 400.

Which identifier wins

Identifiers are ranked. The event resolves against the highest-ranked one you send that matches an existing contact:

RankIdentifierNotes
1contact_idA direct pointer. Used on its own; a UUID that doesn't exist returns 404.
2external_idYour identifier for the person. Stable across email and phone changes.
3email
4phone_numberAmong duplicates, the oldest contact wins.

Sending external_id alongside email or phone_number is the recommended pattern. If the contact is found by the weaker identifier and carries no external_id yet, it adopts the one you sent — so a later external_id-only event finds the same contact instead of creating a duplicate. An external_id already held by a different contact is left alone and the event still resolves.

If nothing matches, a contact is auto-created carrying every identifier you sent.

{
"event": "order.completed",
"external_id": "user_10294",
"email": "john.doe@example.com",
"data": {
"order_id": "A-1023",
"total": 149.99
},
"timestamp": "2026-06-01T12:00:00.000Z"
}

Payload validation

data is validated against the fields declared in the event's definition:

Field typeAcceptsRejects
stringa JSON stringnumbers, booleans, objects
numbera finite JSON numbernumeric strings ("12"), NaN, Infinity
booleantrue or false"true", 0/1
datea Date.parse-able string (e.g. "2026-03-05" or "March 5 2026")epoch numbers — send the date as a string
  • A missing required field returns 422.
  • Undeclared keys are accepted and stored, but ignored by validation. A misspelled field name (e.g. totl instead of total) does not error — it is kept in the payload and will simply never match an automation condition that expects the declared field. Check your field names against the definition.

Response

The event is accepted for asynchronous processing. id is the event log ID.

{
"id": "01957e3a-4b5c-7d8e-9f0a-1b2c3d4e5f6a",
"status": "accepted"
}
FieldTypeDescription
idstringEvent log ID (UUIDv7)
statusstringAlways accepted for a 202 response

Sending a batch

The same endpoint accepts a batch envelope instead of a single event — send { "events": [ … ] }, with up to 50 items of exactly the item shape documented above.

{
"events": [
{
"event": "product.viewed",
"external_id": "user_10294",
"data": { "sku": "A-1023" }
},
{
"event": "order.completed",
"email": "john.doe@example.com",
"data": { "order_id": "A-1023", "total": 149.99 }
}
]
}

Items are independent

A batch does not fail as a unit. Each item is validated and processed on its own, and the response carries one slot per item in the order you sent them:

{
"results": [
{ "id": "01957e3a-4b5c-7d8e-9f0a-1b2c3d4e5f6a", "status": "accepted" },
{ "error": "No event named \"order.completed\" is defined in this organization." }
]
}

A per-item validation error yields { "error": … } in that item's slot and does not affect the others. The HTTP status is still 202 — always inspect the results array rather than relying on the status code alone.

The Idempotency-Key header applies to the whole request

One key covers the entire batch, not individual items. Retrying with the same key replays the whole request; it cannot re-send just the items that errored. If you need per-item idempotency, send them individually.

Batching is what the client SDKs use to drain their queues efficiently — it is the same endpoint, so a server-side integration can use it too.


Idempotency

Sending the same event twice — for example because a network timeout made you retry — could record the event twice, double-counting your rollups and revenue. To make a retry safe, send an Idempotency-Key header whose value is unique to that event (for example order.completed/A-1023):

  • The first request with a given key is processed normally and its response is stored.
  • Any later request with the same key (within 24 hours) is not reprocessed — it returns the original response unchanged, plus an Idempotent-Replayed: true response header. The side effect happens only once.
  • After 24 hours the key expires and may be reused.

The key can be any unique string up to 256 characters. A UUID works, but a value derived from the entity the request is about — like order.completed/A-1023 — is easier to regenerate identically on a retry. Keys are scoped per endpoint, so the same value can be reused independently across the send-email, send-SMS, and send-event endpoints without colliding.

Reusing a key with a different payload

Reuse a key with a different request body and the request is rejected with 409 invalid_idempotent_request, which protects you from receiving the wrong cached response. Use a fresh key for a genuinely different request.

Idempotency error responses

StatusnameWhen
400invalid_idempotency_keyThe key is empty or longer than 256 characters.
409invalid_idempotent_requestThe key was already used with a different request body.
409concurrent_idempotent_requestsAn earlier request with the same key is still being processed — retry shortly.

Examples

curl -X POST "https://api.arsel.sa/v1/events/send" \
-H "Authorization: Bearer be_your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order.completed/A-1023" \
-d '{
"event": "order.completed",
"email": "john.doe@example.com",
"data": { "order_id": "A-1023", "total": 149.99 }
}'

Error Responses

Provide at least one of external_id/email/phone_number, or contact_id on its own:

{
"status_code": 400,
"name": "bad_request",
"message": "Provide at least one of external_id, email, phone_number, or contact_id."
}