Create or replace a contact

Search Documentation

Search across all developer documentation

contacts

Create or replace a contact

PUT /v1/contacts/{email}
Auth required api.contacts.manage

Full create-or-replace: if the contact exists it is fully replaced; if it does not exist it is created. Returns 201 on creation and 200 on update.

"Replace" governs the contact row, NOT the attribute map: omitted topic_preferences revert to the list default and unsubscribe_all is set to the supplied value, but attributes MERGE — a name you do not send keeps its current value, and an omitted or empty map clears nothing.

lists, when present, replaces the contact's static-List membership exactly (an empty array clears all memberships). When lists is absent, list membership is left unchanged.

Supports the Idempotency-Key header.

Path parameters

email string<email> required

Recipient address. Percent-encode @ as %40. Matched case-insensitively.

Query parameters

strict boolean optional

Opt out of attribute auto-registration. With strict=true an attribute name your org has never used is 422 attribute_validation_failed and nothing is registered — the behaviour every /v1 contact write had before this parameter existed. Absent or false registers the name as a string attribute and reports it in attributes_registered. A value that is not a boolean is 422 validation_failed, rather than being read as false: a caller who sent strict=yes believing they had opted in, and then had an attribute registered anyway, would have been told nothing.

Request body

Content type: application/json

attributes object optional

MERGED into the contact's attribute map — names you do not send are left as they are, and this field cannot remove one. Validated against the registry. An attribute name your org has never used is REGISTERED as a string attribute and named in the response's attributes_registered. Send strict=true to opt out: an unknown key is then 422 attribute_validation_failed and nothing is registered. Regardless of strict: a value that does not fit an EXISTING attribute's declared type, a name that is not a valid registry identifier, a reserved name, and a direct write to a derived (formula) attribute are all 422 attribute_validation_failed with a per-attribute errors[] array.

unsubscribe_all boolean optional

Master opt-out. Defaults to false.

topic_preferences array<object> optional

Each entry in topic_preferences:

topic_name string required

The topic's name (see /v1/topics).

subscription_status string enum required

One of: OPT_IN, OPT_OUT

lists array<string> | null optional

Exact static-List membership. Each entry is a List key (the org-unique handle, e.g. beta-cohort) or the List's id — the same pair /v1/lists/{id} resolves. A present empty array clears all memberships; an absent (null) array leaves membership untouched.

Responses

Errors follow the RFC 7807 problem format — see the error reference.

200 Contact replaced application/json
email string<email> required
unsubscribe_all boolean required

Master opt-out — when true the contact receives no topic email.

attributes object required

Custom attribute map as stored in SES. Always an object, never null.

topic_preferences array<object> required

Each entry in topic_preferences:

topic_name string required

The topic's name (see /v1/topics).

subscription_status string enum required

One of: OPT_IN, OPT_OUT

lists array<string<uuid>> required

Ids of the static Lists the contact currently belongs to. Always an array. Ids, not keys — GET /v1/lists maps one to the other, and either form is accepted on the way in.

attributes_registered array<string> required

Attribute names this call added to your org's attribute registry, each registered as a string attribute. Always present; [] when the call registered nothing, and always [] when the request set strict=true. Reported rather than silent because registering a definition changes your audience schema: the name becomes an attr.<name> a segment can filter on, and a typo becomes a permanent attribute nobody asked for. Rename an unwanted one with PUT /v1/attributes/{id}.

created_at string<date-time> required
updated_at string<date-time> required
ses_sync object optional

Present ONLY while the contact has not reached the customer's SES account yet; absent means synced. A 2xx on a contact write means SendOps has accepted and durably stored the change, not that SES has it — writes are mirrored afterwards, paced against the AWS account-wide budget of one contact request per second. Everything user-visible (segments, consent filtering, exports, sends) reads SendOps' own copy, so a pending mirror holds nothing back.

201 Contact created application/json
email string<email> required
unsubscribe_all boolean required

Master opt-out — when true the contact receives no topic email.

attributes object required

Custom attribute map as stored in SES. Always an object, never null.

topic_preferences array<object> required

Each entry in topic_preferences:

topic_name string required

The topic's name (see /v1/topics).

subscription_status string enum required

One of: OPT_IN, OPT_OUT

lists array<string<uuid>> required

Ids of the static Lists the contact currently belongs to. Always an array. Ids, not keys — GET /v1/lists maps one to the other, and either form is accepted on the way in.

attributes_registered array<string> required

Attribute names this call added to your org's attribute registry, each registered as a string attribute. Always present; [] when the call registered nothing, and always [] when the request set strict=true. Reported rather than silent because registering a definition changes your audience schema: the name becomes an attr.<name> a segment can filter on, and a typo becomes a permanent attribute nobody asked for. Rename an unwanted one with PUT /v1/attributes/{id}.

created_at string<date-time> required
updated_at string<date-time> required
ses_sync object optional

Present ONLY while the contact has not reached the customer's SES account yet; absent means synced. A 2xx on a contact write means SendOps has accepted and durably stored the change, not that SES has it — writes are mirrored afterwards, paced against the AWS account-wide budget of one contact request per second. Everything user-visible (segments, consent filtering, exports, sends) reads SendOps' own copy, so a pending mirror holds nothing back.

401 Missing, malformed, or unknown API key application/problem+json
402 The organization's plan does not allow this (code: plan_limit_exceeded). limit_type names the limit — a usage axis such as contacts, segments or active_workflows, or a feature the plan does not include (inboxes, platform_profile, audit_log, notification_presets). upgrade_url is the dashboard page that lifts it; overage_available says a Business organization can lift it by switching overage on instead. Retrying will not succeed until the plan changes or usage drops below the limit. application/problem+json
403 Key lacks the required scope or plan limit violated application/problem+json
404 Resource not found application/problem+json
422 A query parameter, path value or body field failed validation. The body is a validation_failed Problem. When the refusal is about a reference — a segment key, template slug, topic or attribute name the organization does not have — it additionally carries validation_code, field, line/column, missing, candidates and next_step, so a client can correct the call without a second round of guessing. See ValidationProblem. application/problem+json
429 Per-org rate limit exceeded application/problem+json
500 Unexpected server-side failure. The code is internal_error. The request_id field can be quoted to SendOps support to investigate. application/problem+json
503 An upstream dependency — the AWS SES account connected to this org — refused, throttled, or did not answer in time. The code is upstream_unavailable; the status is 503 when the call failed and 504 when it exceeded our deadline. Retryable. Always accompanied by Retry-After. On a 504 the outcome is genuinely unknown (the write may have reached SES), so retries of a non-idempotent call should carry an Idempotency-Key. The detail string is deliberately generic: the underlying AWS error describes the customer's own account and is recorded in SendOps logs against the request_id, not returned here. application/problem+json
504 An upstream dependency — the AWS SES account connected to this org — refused, throttled, or did not answer in time. The code is upstream_unavailable; the status is 503 when the call failed and 504 when it exceeded our deadline. Retryable. Always accompanied by Retry-After. On a 504 the outcome is genuinely unknown (the write may have reached SES), so retries of a non-idempotent call should carry an Idempotency-Key. The detail string is deliberately generic: the underlying AWS error describes the customer's own account and is recorded in SendOps logs against the request_id, not returned here. application/problem+json