Create or upsert a contact
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.
bearerAuthAuthorizationBearer <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.
application/json- body
email*stringemailattributes?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.
{}status?stringUnrecognized values fall back to subscribed.
"subscribed""subscribed""unsubscribed""cleaned"An existing contact was updated.
application/json- response
The intentionally minimal response from create/patch (no attributes/source/timestamps).
id?stringemail?stringstatus?string"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"}