Orders & events
Everything a profile does is an event. Push orders and any activity from your own systems — they land on the contact's timeline and feed segments, aggregates and automations.
Identifying the contact
Order and event records name their contact with contactExternalId (your customer id, as registered via /ingest/contacts — must already exist) and/or email; at least one is required. An unknown email auto-creates a minimal contact with consent unknown — an order implies a profile, never marketing consent. An unknown contactExternalId without an email skips the record with a reason, and so does an email that several contacts share (ambiguous — use contactExternalId).
/ingest/orders{
"records": [
{
"externalId": "ord-1042",
"email": "ada@example.com",
"total": 129.9,
"currency": "EUR",
"completedAt": "2026-08-15T10:00:00Z"
}
]
}externalIdstring ≤200, required | Your order id, unique within your organization — the idempotency key. Replaying a batch counts as deduplicated, never double-inserts. |
contactExternalIdstring ≤200 | Your customer id; must already exist. |
emailstring ≤320 | Auto-creates an unknown contact when no match exists. |
totalnumber ≥ 0 | The order total. |
currencystring, 3 letters | Defaults to your organization's currency. |
completedAtISO 8601 | When the order completed. |
/ingest/events{
"records": [
{
"externalId": "ev-9001",
"email": "ada@example.com",
"type": "booking_completed",
"occurredAt": "2026-08-16T09:00:00Z",
"properties": { "nights": 3 }
}
]
}externalIdstring ≤200, required | Your event id, unique within your organization — the idempotency key. |
contactExternalIdstring ≤200 | Your customer id; must already exist. |
emailstring ≤320 | Auto-creates an unknown contact when no match exists. |
typelower_snake_case ≤64 | Any name from your domain's vocabulary: booking_completed, stay_ended, subscription_renewed, … |
occurredAtISO 8601 | When the event happened. |
propertiesobject | Free-form event payload, available to segments and analytics. Nest freely: a nested value is addressable everywhere as a dotted path (utm.utm_medium, order.total), up to four levels; arrays are stored and shown but not filterable per element. Four keys are reserved for OpenHouse's own annotation of conversion events — attributedCampaignId, attributedAutomationId, attributionModel, attributionAt — and are overwritten if you send them. |
Event names are yours. There is no fixed vocabulary — but the profile fields your workspace started with read specific types, set by the pack it was provisioned on, so use those names for the events they describe or the fields stay empty. Anything else you send is still on the timeline, segmentable and chartable, and your builders can aggregate it into new fields at any time.
order_completedproperties.total | Lifetime revenue, orders, last order. |
booking_completedproperties.total | Total booking value, bookings. |
stay_endedproperties.nights | Nights in the last year, last stay. |
program_enrolledproperties.total | Program revenue, enrollments, last enrollment. total is the paid seat — self-pay or employer-funded. |
program_completed— | Programs completed. |
application_submitted— | Applications. |
session_attended— | Sessions attended in the last 90 days, last session. |
Two types are the same everywhere: email_opened / email_clicked stamp the contact's engagement recency. Engagement events for email OpenHouse itself sent arrive automatically — this endpoint is for activity from other systems. Orders sent to /ingest/orders become order_completed events; on any other pack, send your revenue-bearing event through this endpoint with a total property instead.
Whichever type your organization has configured as its conversion event (default order_completed; an administrator changes it under Settings → Organization — an academy would pick program_enrolled) is attributed on arrival: OpenHouse finds the last email the contact clicked (or, under the last-touch model, opened) within the organization's attribution window before occurredAt and writes the campaign onto the event, so revenue per campaign is a query, not a guess. Send the real occurredAt — attribution reads it, not the time of the request.
When the data shows up
- Instantly — the event is on the contact's timeline, and recency fields (last order, last activity) are exact in the same request.
- Within ~2 minutes — running totals such as lifetime revenue and order count fold in the new events.
- On a schedule — averages, rolling windows and computed scores refresh on their own cadence (hours, not days).