Contacts

Push profiles from your system into OpenHouse. Each becomes a contact, identified by your own stable customer id (externalId) or by email, and re-sending a record safely upserts.

POST/ingest/contacts
{
  "mode": "upsert",
  "records": [
    {
      "externalId": "cus_42",
      "email": "ada@example.com",
      "firstName": "Ada",
      "lastName": "Lovelace",
      "country": "DE",
      "consentStatus": "opted_in",
      "custom": { "company": "Acme GmbH", "cohort": "2026-Q3" }
    }
  ]
}
Record fields — all optional, but a record needs an identity: externalId and/or email
externalId
string ≤200
Your system's stable customer id, unique within your organization. Immutable once set.
email
string ≤320
Lowercased and validated. An attribute, not an identity — it can change, and two contacts may share one.
firstName
string ≤200
Non-empty values enrich; omitted fields never erase.
lastName
string ≤200
Same enrichment semantics.
country
string ≤10
Country code.
consentStatus
opted_in | opted_out | unknown
Only changes when explicitly present — ingestion can never silently resurrect an opted-out contact. New contacts without it start as unknown.
custom
object
Values for your organization's own custom fields, keyed by field key. The fields must exist first (see Attributes). Each value is checked against its field's type; a value that doesn't fit is reported in fieldErrors and the record still lands. null or an omitted key never erases a stored value.

Contacts also carry a read-only randomBucket (0–999), assigned by the system at creation and returned on every contact response. It is the handle for random samples and A/B arms in segments (randomBucket less_than 100 is a stable 10%). It is not accepted on ingest.

mode (batch-level, default "upsert"): with upsert, a record matching no existing contact creates one; with enrich, it is skipped with a named reason and nothing is created — for integrations that must only ever update what already exists.

Which keys custom accepts, how to create a field, the value types, and what happens to a value that doesn't fit are on Attributes.

How records match contacts

Matching is strict and predictable — no write ever merges contacts implicitly:

  • A record with externalId matches by externalId only — never by email fallback. Re-sending {"externalId": "cus_42", "email": "new@x.com"} updates that contact's email; an unmatched externalId creates a new contact even if the email matches an existing one.
  • A record without externalId matches by email: exactly one match updates it; several contacts sharing the email skip the record with a named reason (use externalId); no match creates (upsert) or skips (enrich).
POST/contacts/identify

Attaches your externalIds to contacts that exist only by email — typically once, when your integration arrives after a CSV-imported profile base. Up to 1000 records:

{ "records": [ { "externalId": "cus_42", "email": "ada@example.com" } ] }

Each record stamps the externalId onto the contact with that email only when the email matches exactly one contact that has no externalId yet. Anything else — id already assigned, email matches nobody or several contacts, the contact already carries a different id — is a named skip, never a merge. Re-running the same pairs skips harmlessly.

Addressing a single contact

Everywhere a contact id appears in a path you can use either the OpenHouse uuid or ext: + your externalId — no id-lookup round trip needed:

GET /contacts/ext:cus_42          # the contact, by your id
GET /contacts/ext:cus_42/events   # their event timeline

An unknown externalId is a 404, exactly like an unknown uuid.

Erasure and export

Both require an administrator's key and accept either address form.

GET/contacts/ext:cus_42/export

Everything OpenHouse holds about one person as a single JSON bundle — the contact, their custom field values, their events and their email engagement — for access and portability requests.

DELETE/contacts/ext:cus_42
DELETE /contacts/ext:cus_42          # by your externalId
DELETE /contacts/3f2a…-…               # or by the OpenHouse id

# 200 → { "ok": true }     # 404 for an unknown id, 403 without an administrator's key

A hard delete: the contact, their field values, events and automation enrollments go with the row, and campaign statistics keep only anonymous counts. The action is written to your audit log with the contact's id only — never the email. Nothing remembers the erasure — a list of "people we deleted" would itself store what they asked us to remove — so re-ingesting the same person afterwards creates a brand-new contact with fresh consent. Keeping erased people out of future batches is your system's job, at the source.

A dynamic CRM. Your audiences, campaigns and reports keep up.

Customer data, analysis and campaigns usually live with three vendors, and an agent cannot run an account whose pieces do. In OpenHouse they are one product, so a question becomes a segment becomes a campaign, in one conversation.

Customer data platform

Events from the shop, the app and payments land in one profile per customer. Attributes and segments are derived from them.

CRM with a design studio

Campaigns and automations run on those segments. Emails are designed in the same place, on the brand.

Self-service BI

Anyone on the team asks a question of the customer base and gets the chart. Dashboards are described, not built.

Let’s talk.

Access is by invitation. If your team already runs a workspace, an administrator there can invite you. Otherwise, request a demo and we will set one up with you.