Skip to main content
Send your own events into Senderz from a backend, a POS, a subscription system or anything else that knows when something happened. Every accepted event is written to the canonical event ledger, attached to a profile, added to that profile’s timeline, and handed to the automation engine.

Send an event

Returns 200 OK. Authenticated with the X-Sender-Tenant and X-API-Key header pair, not a bearer token.
string
required
Your Workspace ID. The workspace slug or the workspace UUID, both accepted.
string
required
A workspace API key (sk_evt_...).

Body

string
required
The event name, 1 to 128 characters. Normalized to lower_snake_case, so Quiz Completed, quiz-completed and quiz_completed all resolve to the same event. The first time a name arrives it is created in your workspace’s event catalogue.
string
A unique id for this occurrence, up to 255 characters. Two deliveries carrying the same eventId are counted once. Send one on every call.
string
ISO-8601 timestamp of when the event happened. Defaults to the time the request is received. Use it for any historical import.
object
What happened. Arbitrary key and value pairs describing this occurrence. Keys are normalized to lower_snake_case, so orderTotal is stored as order_total. The whole bag is capped at 32 KB.
integer
Monetary value in minor currency units (agorot, cents). 24990 is 249.90. Feeds revenue reports and attribution. Must be a whole number, a decimal is rejected.
string
Currency code for value, up to 8 characters. For example ILS.
object
Profile fields to set on the resolved contact. Validated against your workspace’s field catalogue. Deliberately separate from properties.
string
Profile first name, up to 255 characters. Written on any resolved contact, new or existing.
string
Profile last name, up to 255 characters. Written on any resolved contact, new or existing.

Identity

At least one identifier is required. See How a profile is resolved.
string
An existing Senderz profile UUID. The strongest identifier, it wins outright.
string
An id owned by your own system, up to 255 characters. Unique within externalNamespace.
string
Names the system that owns externalId, up to 64 characters. For example billing.
string
Contact email, up to 320 characters. Must be a valid address.
string
Contact phone, up to 32 characters. Normalized to E.164.
string
A device or browser id, up to 128 characters. Records activity before a visitor identifies themselves.
The request body is whitelisted. An undocumented field returns 400.

Response

string
One of ok, duplicate, anonymous or no_contact.
string | null
The canonical ledger id for this event, for correlating with your own records. This is the Senderz id, not the eventId you supplied. null when nothing was recorded.
boolean
true when the supplied eventId had already been seen. Omitted when the request carried no identifier at all.
string | null
The resolved or created profile UUID. null for anonymous and no_contact.
boolean
true if this event created the profile.
integer
How many automation enrolments were dispatched while handling this request. Downstream effects are drained per workspace, so this can include a small number of enrolments left pending by an earlier call.

Example

How a profile is resolved

Identifiers are tried in a fixed order. The first one that matches an existing profile wins.
1

contactId

An internal profile UUID wins outright, before anything else is consulted.
2

externalId within externalNamespace

An id set deliberately by the system that owns the customer record.
3

email

Lowercased and trimmed before matching.
4

phone

Normalized to E.164 before matching.
5

anonymousId

A device or browser, matched last.
Every identifier on the payload is linked to the resolved profile, so the next event carrying any one of them finds the same person. If two identifiers on a single event point at two different profiles and the two profiles do not contradict each other, they are folded into one. An ambiguous conflict is left alone rather than merged.

When a profile is created

If nothing matches, a profile is created only when the event carries an email or a phone. It is created with source api, and any contact_created automation fires for it.
An externalId or an anonymousId alone cannot create a profile. An id naming a billing record or a browser is not a contactable person, and creating profiles from them would fill your audience with rows that can never be messaged.Sending only an externalId for a customer Senderz has never seen returns status: "no_contact" and records nothing. Pair the externalId with an email or phone the first time you see a customer, then the external id alone resolves on every call after that.

Anonymous activity

An event carrying an anonymousId and nothing else is recorded against that id and returns status: "anonymous" with contactId: null. Send the same anonymousId together with an email or phone once the visitor identifies themselves, and the earlier anonymous events are attached to the profile at that moment. Their history is preserved rather than starting from the moment of signup.

Deduplication

Send an eventId on every call. It is the only way two genuine repeats of the same action can be told apart from one delivery arriving twice.
With an eventId, deduplication is exact. Two deliveries carrying the same id are one event: the second returns status: "duplicate", writes nothing, and fires no automation. Two different ids are two events even when every other field matches, so a customer who buys the same item twice is counted twice. Without an eventId, the event falls back to a per contact, per event name, per minute window. A burst of identical calls collapses into one, which also means two genuine repeats inside the same minute collapse into one and are silently lost. This fallback exists so integrations built before eventId keep working, not as a recommended mode. Use something your own system already treats as unique. An order id, an invoice id, or a composite like sub_8842:renewal:2026-08-26 all work.

Event names and property keys

Both are normalized on the way in. Normalizing means orderTotal and order-total never become three separate properties. It also means the key you reference later, in a segment filter or a merge tag, is the normalized one.

Property types are fixed on first sight

The first value a property carries decides its type: boolean, number, string, datetime, list or object. A string that looks like an ISO date is recognized as datetime. A later value of an incompatible type is rejected with 400 and code metric_property_type_conflict rather than being written where no filter would match it. Widening from datetime to string is allowed; narrowing is not. A number sent as the string "49.90" stays a string, it is never coerced, so send numbers as JSON numbers.

Revenue

value is a whole number of minor currency units, paired with currency. Send 24990 and ILS for 249.90 shekels. Sending 249.90 is rejected as a non-integer. Events carrying a value feed revenue reports and attribution, so an event like subscription_renewed or booking_paid should carry one.

Backdating

occurredAt places the event at the moment it really happened. Any historical import needs it, otherwise every backfilled row lands with today’s timestamp and every time-windowed report and segment reads them as activity from today.
Backdating does not suppress automations. An event with an occurredAt two years ago still enrols any matching automation the moment it is accepted. Pause the relevant automations before running a historical import, or a two-year-old order will send someone a message today.

What happens downstream

An accepted event does four things.

Ledger entry

Written to the canonical event ledger with its properties, value, currency and timestamp. This is what segments and reports read.

Timeline entry

Added to the profile’s activity feed as a custom event carrying the event name and its properties.

Automation enrolment

Enrols any automation whose trigger is a custom event matching this name.

Event catalogue

A new event name appears in your workspace’s event catalogue and in GET /public/events.
The timeline entry carries the property bag only when it is under 8 KB. A larger bag is omitted from the timeline row for readability; the full payload is still recorded in the ledger, and only a bag over 32 KB is rejected outright.

Matching an automation trigger

The automation’s configured custom event name is normalized the same way as the incoming one, so an automation built on Quiz Completed in the builder is triggered by quiz_completed sent through this endpoint.
Custom event names appear in the event catalogue returned by GET /public/events, but they cannot be subscribed to as outbound webhook events. Outbound webhooks use a fixed dotted vocabulary such as email.opened.

Using event properties in automations

Properties become flat merge tags in automation email and SMS templates. A property stored as order_total is referenced as {{order_total}}. value is available as {{amount}} and currency as {{currency}} when the properties do not already define those keys. Only primitive values become merge tags. Nested objects and arrays are skipped, and the identity and compliance tags {{firstName}}, {{lastName}}, {{email}}, {{phone}} and {{unsubscribeUrl}} are reserved and cannot be shadowed by a property of the same name. See Automations for how to build the trigger.

properties vs profileProperties

The two are separate on purpose. An event property never becomes a contact field. If you want a value to persist on the person, put it in profileProperties. profileProperties, firstName and lastName are written to any resolved profile, new or existing. Every key is checked against your workspace’s field catalogue: an unknown key or a computed field is skipped. If every key in the request is rejected, the call returns 400 with code profile_field_invalid.

Errors

Errors use the standard envelope. See Errors and rate limits. An event that carried no identifier at all is not an error. It returns 200 with status: "no_contact", so an older integration keeps working rather than starting to fail.