Setup guide · Data in
Send product events with the Events API
Product events tell Sendalto what people do in your product — signed up, activated, trial ending. Journeys start on an event and stop when the goal event arrives.
Checked against Sendalto on
1. Create a server API key
In Settings → API & privacy create a key with the events:write scope. The secret starts with snd_live_ and is shown once; store it in your backend’s secret store. Owners and admins create keys after turning on two-factor sign-in.
A key acts with its own scopes and stops working when it is revoked or its creator leaves the workspace or loses the right to manage API keys.
2. Send an event
POST https://sendalto.com/api/events with Authorization: Bearer <key> and a JSON body. Unknown fields are rejected.
| Field | Rules |
|---|---|
externalId | Required. 1–200 characters. Your stable ID for this occurrence, used to drop duplicates. |
name | Required. Starts with a letter; up to 100 letters, digits and _ . : -, for example trial_started. |
email | Required. The person’s email; it links the event to a contact with that address. |
properties | Optional object, up to 32 KB as JSON. |
occurredAt | Optional ISO 8601 time with a zone, such as 2026-10-06T09:30:00Z; at most 5 minutes in the future. Defaults to the time Sendalto receives it. |
curl https://sendalto.com/api/events \
-H "Authorization: Bearer $SENDALTO_EVENTS_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "trial_started:user_8f2c1d",
"name": "trial_started",
"email": "[email protected]",
"properties": { "plan": "team", "seats": 5 },
"occurredAt": "2026-10-06T09:30:00Z"
}'// Server-side only (Node.js 18+). The key never reaches a browser.
export async function trackEvent({ externalId, name, email, properties = {}, occurredAt }) {
const response = await fetch('https://sendalto.com/api/events', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SENDALTO_EVENTS_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ externalId, name, email, properties, occurredAt }),
});
const result = await response.json();
if (response.ok) return result.data; // { id, duplicate }
// 429 and 5xx: retry later with the same externalId. Other errors need a fix.
throw Object.assign(new Error(result.error), { status: response.status, code: result.code });
}
await trackEvent({
externalId: `trial_started:${user.id}`,
name: 'trial_started',
email: user.email,
properties: { plan: user.plan },
});3. Responses, retries and limits
HTTP/1.1 201 Created
{ "data": { "id": "6c7e1d3a-…", "duplicate": false } }
HTTP/1.1 200 OK (same externalId sent again)
{ "data": { "id": "6c7e1d3a-…", "duplicate": true } }
HTTP/1.1 429 Too Many Requests
{ "error": "Event rate limit reached. Retry after one minute.", "code": "RATE_LIMIT" }- Idempotency. The same
externalIdwith the same name, email and properties returns200with"duplicate": trueand changes nothing. Reusing anexternalIdfor a different event returns409 EVENT_ID_REUSED. When a request times out or fails with429or5xx, retry with the sameexternalId. - Rate limit. 600 events per minute per workspace; above it you get
429 RATE_LIMIT. The request body may be up to 40 KB. - Errors are JSON with
error(a readable message) andcode, listed below.
| Code | HTTP | Meaning |
|---|---|---|
API_KEY_REQUIRED | 401 | No Authorization: Bearer <key> header. |
API_KEY_INVALID | 401 | The value is not a Sendalto key (snd_live_…). |
BACKEND_KEY_ONLY | 403 | The request came from a browser (it has an Origin header). |
API_SCOPE | 403 | The key was revoked, has no events:write scope, or its creator left the workspace or no longer has a verified email. |
API_ISSUER | 403 | The key’s creator no longer has a role that may manage API keys. |
KEY_REVOKED, KEY_PERMISSION_REVOKED | 403 | The key or its creator’s access changed while the request was being processed. |
BODY_REQUIRED | 400 | The request has no body. |
TOO_LARGE | 413 | The body is larger than 40 KB. |
VALIDATION | 400 / 422 | The body is not JSON (400) or does not match the fields above (422). |
EVENT_TOO_LARGE | 422 | properties are larger than 32 KB. |
EVENT_IN_FUTURE | 422 | occurredAt is more than 5 minutes ahead. |
EVENT_ID_REUSED | 409 | This externalId was already used for a different event. |
RATE_LIMIT | 429 | More than 600 events in the last minute. Retry later with the same externalId. |
What happens to an event
- It is stored with the workspace and appears in the journey builder’s list of known events.
- If a contact with that email exists, active journeys triggered by the event name start for them (within each journey’s re-entry rule), and runs whose goal is this event stop.
- Webhook subscriptions that list the event name forward it to your endpoint — see Webhooks.
- An event never creates a contact or grants permission to email. Consent is recorded on contacts.