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).

POST/ingest/orders
{
  "records": [
    {
      "externalId": "ord-1042",
      "email": "ada@example.com",
      "total": 129.9,
      "currency": "EUR",
      "completedAt": "2026-08-15T10:00:00Z"
    }
  ]
}
externalId
string ≤200, required
Your order id, unique within your organization — the idempotency key. Replaying a batch counts as deduplicated, never double-inserts.
contactExternalId
string ≤200
Your customer id; must already exist.
email
string ≤320
Auto-creates an unknown contact when no match exists.
total
number ≥ 0
The order total.
currency
string, 3 letters
Defaults to your organization's currency.
completedAt
ISO 8601
When the order completed.
POST/ingest/events
{
  "records": [
    {
      "externalId": "ev-9001",
      "email": "ada@example.com",
      "type": "booking_completed",
      "occurredAt": "2026-08-16T09:00:00Z",
      "properties": { "nights": 3 }
    }
  ]
}
externalId
string ≤200, required
Your event id, unique within your organization — the idempotency key.
contactExternalId
string ≤200
Your customer id; must already exist.
email
string ≤320
Auto-creates an unknown contact when no match exists.
type
lower_snake_case ≤64
Any name from your domain's vocabulary: booking_completed, stay_ended, subscription_renewed, …
occurredAt
ISO 8601
When the event happened.
properties
object
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.

E-commerce pack
order_completed
properties.total
Lifetime revenue, orders, last order.
Hospitality pack
booking_completed
properties.total
Total booking value, bookings.
stay_ended
properties.nights
Nights in the last year, last stay.
Education & Training pack
program_enrolled
properties.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).