order.completed. Give it a goalType and it becomes a conversion (a goal), which can also carry money.
The browser version of the same call is
POST /public/events.
Recording a purchase
201 { id, contactId, deduplicated: false, status: "recorded" }. Sending the same externalId again answers 200 with status: "duplicate".
Rules
eventmust be lowercase letters, digits,.,_,/or-, starting with a letter or digit, up to 100 characters.- Identify the contact with
emailorcontactId, and send one of the two. goalTypeis one ofpurchase,subscription,lead,signup,booking,trialorother.purchaseandsubscriptionrequire avalue. Avalueis only accepted together with agoalType, and a negative value records a refund.currencyis a 3-letter ISO code.occurredAtis an ISO date-time and defaults to now.
If the email or ID matches no contact, the event is still stored with
contactId: null. That is not an error, and no contact is created. An event with no contact starts no journey and fires no webhook. Create the contact first if you need it.What an event does
When the event matches a contact:- It starts every active journey whose API Event trigger listens for that event name, and every website-event journey that listens for it. See Journeys.
- It fires the
EVENT_RECORDEDwebhook. - It updates the contact’s last-activity time.
- It feeds goal-based segment rules, such as lifetime value, purchase count and last purchase.
Batch import
POST /v1/events/import takes { "events": [ … ] } with 1 to 50 events. The whole body is checked against the event schema first, so one malformed row fails the entire call with 400 Invalid input. After that, each row is recorded on its own and reported by index. A row that is missing email/contactId, or that breaks a goal rule, gets an error without affecting the others. The call answers 400 only when every row failed.