Track an event
POST /api/v1/events
Records an event and attaches it to a contact, creating that contact if they don’t exist yet. Returns 201 Created.
Request
Human-readable event name, e.g.
Add to Cart. Any non-empty string is accepted — see How event names become types.Idempotency key for this occurrence — a cart ID, checkout ID, or order ID. See Idempotency; getting this wrong is the most common integration bug.
Who the event belongs to. Must contain either
contactUUID, or at least one of email / phone.Flat key-value object describing the event. Must not be empty, and must not contain nested objects.
The contact object
Field names are snake_case. Unrecognized fields are rejected with
400, so emailAddress or firstName will fail — use email and first_name.
Example request
Response
201 Created
Idempotency
uniqueIdentifier is what stops a retried HTTP call from emailing your customer twice. Uniqueness is scoped to the combination of your organization, the event type, and the identifier.
So: use a stable ID per real-world occurrence (
order_55501), and a new ID when you want the flow to fire again.
Two things still happen on a resubmit: the contact is upserted (so profile fields update), and the event schema is re-registered. Only the automation trigger is suppressed.
How event names become types
You send a human-readableeventName. Retainful normalizes it and prefixes it with your integration’s key to produce the internal event type you’ll see in the dashboard:
Normalization uppercases the name and replaces every run of non-alphanumeric characters with a single underscore. The
REST-API. prefix comes from the integration the API key belongs to.
There is no fixed list of event names. Any non-empty string works — Quiz Completed and Subscription Renewed are as valid as the commerce names above.
Keep event names under about 50 characters. The internal type is stored in a 64-character column and includes the integration prefix; longer names fail on write.
eventData rules
- Must not be empty.
- No nested objects. A nested object is rejected with
400and a message naming the offending field. Flatten it:shipping_cityrather thanshipping: { city }. - Values may be strings, numbers, or booleans. Types are inferred and preserved for use in automation filters, so send
"amount": 19.99if you want to compare it numerically. - Useful keys:
product_id,product_name,quantity,price,currency,order_id,checkout_id,total,amount. - Every key becomes available in automation trigger filters and email personalization.
Register an event schema
POST /api/v1/events/register
Registers an event type and its shape without attaching it to a contact. Use it so the event’s fields appear in the automation builder’s filter and personalization pickers before any real event has fired — letting marketing build the flow while engineering ships the integration.
Designing good events
- Name by fact, not intent:
Subscription Renewed(what happened), notSend Renewal Email(what you want done). Marketing decides the response in the automation builder. - Keep names stable. Renaming orphans the automations and segments built on the old type.
- Include the fields you’ll filter on. If a flow should only fire for high-value renewals, send
amount. - Send one event per real occurrence, with a
uniqueIdentifierthat matches that occurrence’s own ID.
Event-triggered automations end to end
POST /api/v1/events/registerwith your event shape.- In the dashboard, create an automation triggered by your event, with filters like
plan is annual. POST /api/v1/eventsfrom production whenever it happens, with a freshuniqueIdentifiereach time.- Watch contacts flow through in the automation’s analytics.
Errors
See Errors for the response shape.