Create or upsert a contact

POST
/v1/contacts

Upserts by normalized (lowercased/trimmed) email. Enforces the plan's max_contacts cap, but only when the email does not already exist — re-identifying a known contact never counts against the cap. Emits a contact.created/contact.updated webhook and, on genuine creation, enrolls the contact into any active contact_created-triggered automation.

Authorization

bearerAuth
headerAuthorizationBearer <token>

API key sent as Authorization: Bearer <key>. Looked up by SHA-256 hash (email_core::sha256_hex) against api_keys.hash; revoked keys and keys belonging to a suspended tenant are rejected. See the top-level Authentication section for permission levels.

Request Body

application/json
  1. body
email*string
Formatemail
attributes?

Flat schemaless attributes (email_core::validate_attributes): object only, at most 50 keys, ≤8 KiB serialized, and every value must be a string/number/bool/null — no nested objects or arrays.

Default{}
status?string

Unrecognized values fall back to subscribed.

Default"subscribed"
Value in"subscribed""unsubscribed""cleaned"

Response Body

An existing contact was updated.

application/json
  1. response

The intentionally minimal response from create/patch (no attributes/source/timestamps).

id?string
email?string
status?string
Value in"subscribed""unsubscribed""cleaned"
curl -X POST "https://example.com/v1/contacts" \  -H "Content-Type: application/json" \  -d '{    "email": "user@example.com"  }'
{  "id": "string",  "email": "string",  "status": "subscribed"}