Early accessSome features, including AI drafting, are still being switched on.See what works today

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.

Request body
FieldRules
externalIdRequired. 1–200 characters. Your stable ID for this occurrence, used to drop duplicates.
nameRequired. Starts with a letter; up to 100 letters, digits and _ . : -, for example trial_started.
emailRequired. The person’s email; it links the event to a contact with that address.
propertiesOptional object, up to 32 KB as JSON.
occurredAtOptional 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
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"
  }'
Node.js
// 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

Responses
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 externalId with the same name, email and properties returns 200 with "duplicate": true and changes nothing. Reusing an externalId for a different event returns 409 EVENT_ID_REUSED. When a request times out or fails with 429 or 5xx, retry with the same externalId.
  • 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) and code, listed below.
Error codes
CodeHTTPMeaning
API_KEY_REQUIRED401No Authorization: Bearer <key> header.
API_KEY_INVALID401The value is not a Sendalto key (snd_live_…).
BACKEND_KEY_ONLY403The request came from a browser (it has an Origin header).
API_SCOPE403The key was revoked, has no events:write scope, or its creator left the workspace or no longer has a verified email.
API_ISSUER403The key’s creator no longer has a role that may manage API keys.
KEY_REVOKED, KEY_PERMISSION_REVOKED403The key or its creator’s access changed while the request was being processed.
BODY_REQUIRED400The request has no body.
TOO_LARGE413The body is larger than 40 KB.
VALIDATION400 / 422The body is not JSON (400) or does not match the fields above (422).
EVENT_TOO_LARGE422properties are larger than 32 KB.
EVENT_IN_FUTURE422occurredAt is more than 5 minutes ahead.
EVENT_ID_REUSED409This externalId was already used for a different event.
RATE_LIMIT429More 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.