Transactional email

Send one email to one profile, triggered by your own system: order confirmations, password resets, booking receipts. Transactional mail ignores marketing consent and carries no unsubscribe link — its brake is the suppression list.

POST/transactional/send
{
  "to": { "externalId": "cus_42", "email": "ada@example.com", "firstName": "Ada" },
  "templateKey": "order-confirmation",
  "variables": { "orderNumber": "ORD-1042", "trackingUrl": "https://..." },
  "idempotencyKey": "order-1042-confirmation"
}

# 200 →
{ "id": "...", "status": "sent", "to": "ada@example.com", "contactId": "...",
  "contactCreated": false, "templateId": "...", "templateVersion": 3,
  "deduplicated": false }

One message per request — a transactional send is triggered by one event, not a batch job. status is one of sent, failed or suppressed.

Addressing: who receives it

to carries exactly one of contactId (the OpenHouse uuid) or externalId (your customer id):

  • contactId must exist (404 otherwise). A payload email is the send-to address for this message only — it does not change the contact.
  • externalId upserts: a match is enriched (the payload email becomes the contact's address; firstName/lastName fill in, never erase), a miss creates the contact — which requires an email in the payload. Created contacts start at consent unknown: a transactional send never manufactures marketing consent.
  • The mail goes to the payload email when present, the contact's stored email otherwise.

Every transactional email lands on a contact timeline — there are no contact-less sends.

Content: a template or your own HTML

Template mode (templateKey or templateId): resolves the template's current published version at send time, so a fix made in OpenHouse ships on your very next email with no change on your side. Pass templateVersion to freeze one explicitly. subject falls back to the template's default.

HTML mode (html + subject): your already-rendered email, delivered as-is (≤300 KB; a text part is derived). Personalization tokens still render, so you can keep personalization in OpenHouse even with your own markup.

Exactly one of the two — a request carrying both, or neither, is a 400. Content containing {{unsubscribe_url}} is refused: transactional mail has no unsubscribe. If it's marketing, send it as a campaign.

Variables and personalization

Request data no contact field holds — an order number, a reset link — travels in variables and is referenced in content as {{api.orderNumber}}:

variables
object
Flat map of name → string (≤2000 chars), number or boolean. Names are letters, digits and underscores.
  • Every api.* token in the content must have a variable — a missing one is a 400 naming it, so a literal {{api.resetLink}} can never reach a recipient.
  • Variable values render verbatim (booleans as Yes/No): an order id or URL is never locale-reformatted. Format numbers yourself before sending.
  • Contact tokens — {{firstName}}, {{email}}, any field in your organization's field registry — render with your organization's locale and currency, exactly like campaign email.
  • Organization tokens — {{org.name}}, {{org.tagline}}, {{org.postalAddress}}, {{org.consentLine}} — are filled from your email brand (Settings → Email brand), in the subject and the body alike.

Suppression instead of consent

The send ignores consent_status but refuses addresses on the suppression list, which fills automatically on every permanent bounce and spam complaint. A suppressed send returns 200 with status: "suppressed" — an outcome to record, not a caller error to retry.

Idempotency and tracking

idempotencyKey (optional, ≤200 chars, unique per organization): a replayed request returns the original outcome and never mails twice. Use your event's stable id (order-1042-confirmation), not a random value per attempt.

Delivery, bounces, opens and clicks are tracked back onto the send and the contact's timeline automatically.

GET/transactional

Lists recent sends, newest first (?limit=, default 50) — the tracing surface while you wire up an integration.