Create or update a contact by e-mail
const url = 'https://api.getsitecontrol.com/v1/sites/1/contacts';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"email":"example","status":"subscribed","first_name":"example","last_name":"example","country":"example","phone":"example","timezone":"example","language":"example","fields":{"additionalProperty":"example"},"add_tags":["example"],"remove_tags":["example"]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.getsitecontrol.com/v1/sites/1/contacts \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "email": "example", "status": "subscribed", "first_name": "example", "last_name": "example", "country": "example", "phone": "example", "timezone": "example", "language": "example", "fields": { "additionalProperty": "example" }, "add_tags": [ "example" ], "remove_tags": [ "example" ] }'Creates or updates one contact by e-mail and returns it whole, as
get_contact does. Call it to push a contact from another system, such as a
CRM, without looking it up first; for many contacts at once, upsert_contacts.
The contact is found by email, without case, and updated; when the address
is free it is created. The address of a deleted contact restores that
contact with its old data and applies the request over it. result says
which: created and restored answer 201, updated and unchanged
(nothing to change, nothing written) answer 200.
A field left out is kept; null clears first_name, last_name, phone,
country, timezone and language; fields are keyed by field ids from
list_contact_fields, merged by key, and null removes a value. Tags go by
name, compared without case: add_tags adds them and creates a tag the site
does not have, remove_tags takes them off; tags not named stay.
changed names the fields that were written.
Status: subscribed is allowed from unsubscribed, and subscribes again a
contact who opted out, as the admin does; leave status out to keep what the
contact chose. unsubscribed is allowed from subscribed or from awaiting
double opt-in. A bounced contact changes status only with a new e-mail, and a
contact awaiting double opt-in is never subscribed here. A status change that
is not allowed does not stop the rest: the other fields are written and
reason says why.
A new contact is subscribed unless status says unsubscribed, which stores
it as never subscribed; it cannot be created bounced. Its source is api,
with the API key as id, or the user when the call is made with a user’s
access token. A contact created here starts “new contact” automations when
subscribed and emits contact.created; a restored one does neither. Its
e-mail is checked in the background. A found contact keeps the stored case
of its e-mail and its source.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Header Parameters
Section titled “Header Parameters”Makes a retry of a POST safe: a repeat with the same key, body and GSC-Version returns the stored response with Idempotency-Replayed: true instead of running again. The key protects against repeated execution while the key store works. Saving the change and storing the response under the key are not atomic: if the process or the key store fails between them, the change is kept, the key is released after 15 minutes and a retry runs the request again. After a timeout or a 5xx, read the resource to check the result before retrying. 5xx and 429 responses and responses over 256 KB are not stored, so a repeat runs the request again. A key is kept for 24 hours. The same key with another body or version is 422; a repeat while the first request runs is 409.
Makes a retry of a POST safe: a repeat with the same key, body and GSC-Version returns the stored response with Idempotency-Replayed: true instead of running again. The key protects against repeated execution while the key store works. Saving the change and storing the response under the key are not atomic: if the process or the key store fails between them, the change is kept, the key is released after 15 minutes and a retry runs the request again. After a timeout or a 5xx, read the resource to check the result before retrying. 5xx and 429 responses and responses over 256 KB are not stored, so a repeat runs the request again. A key is kept for 24 hours. The same key with another body or version is 422; a repeat while the first request runs is 409.
Request Bodyrequired
Section titled “Request Bodyrequired”The fields every contact write takes; an upsert finds by email.
object
Finds the contact, without case; kept as stored when found, stored in lower case when created
subscribed or unsubscribed; bounced is set only by e-mail delivery. Left out: unchanged, a new contact is subscribed; may not be null
Null clears it
Null clears it
ISO 3166-1 alpha-2 code or an English country name; stored as the code; null clears it
In the international format, or national with country in the same request; stored normalized; null clears it
IANA time zone name such as Europe/Berlin; null clears it
ISO 639-1 code such as de; stored in lower case; null clears it
Values by field id from list_contact_fields: a string for text, a whole number for integer, a number, an ISO 8601 string for a datetime (UTC without an offset), YYYY-MM-DD for a date, true or false for a boolean. Fields of the other type are read-only. Merged into the stored values: null removes a value, keys not given stay as they are
object
Tag names to add, compared without case; a tag the site does not have is created. Tags the contact has stay
Tag names to remove, compared without case; a name with no tag, or a tag the contact does not have, is no change. Never creates a tag
Responses
Section titled “Responses”object
created, restored, updated or unchanged; an item that cannot be written is refused with an error instead
Names of the fields whose stored value changed, from is_deleted, email, status, first_name, last_name, phone, country, timezone, language, fields, tags; empty when none did. A new contact names email, status and every field it was given; a restored one starts with is_deleted
contact_status_transition when the status sent was not applied; the rest was written. Null otherwise
As get_contact returns it
object
Subscribed: subscribed to e-mails. unsubscribed: opted out, never subscribed, or has not confirmed the subscription yet. bounced: e-mails to the address bounced, or it was cleaned from the list.
ISO 3166-1 alpha-2
IANA time zone name
ISO 639-1 code
Set values of custom fields by field id, typed by the field: a date as YYYY-MM-DD, a datetime in ISO 8601
object
object
Where the contact came from; widget, automation, import, shopify, mailchimp, klaviyo name the source. api: added through the public API or an AI assistant (MCP). manual: added by hand in the admin. other: a source not listed here.
Depends on the type: the widget, the automation, the admin user, the import task, the Shopify customer, the Mailchimp id; for api the API key, or the user whose access added it when there is no key
E-mails to the contact, the keys of EmailMetrics; send includes
e-mails rejected on sending, also counted in bounce.
object
When the contact was added at its source; imports keep the original date
Last change; equals created_at when none was recorded
Empty unless the contact could not be read in full; wording is not stable, do not parse
{ "result": "created", "reason": "contact_not_found", "contact": { "status": "subscribed", "source": { "type": "widget" } }}Headers
Section titled “Headers”API version the response was produced for
Request id, echoed from the request when valid
Limit of the tightest bucket
Requests left in its window
End of its window, epoch seconds
True when the response is a stored one, replayed
object
created, restored, updated or unchanged; an item that cannot be written is refused with an error instead
Names of the fields whose stored value changed, from is_deleted, email, status, first_name, last_name, phone, country, timezone, language, fields, tags; empty when none did. A new contact names email, status and every field it was given; a restored one starts with is_deleted
contact_status_transition when the status sent was not applied; the rest was written. Null otherwise
As get_contact returns it
object
Subscribed: subscribed to e-mails. unsubscribed: opted out, never subscribed, or has not confirmed the subscription yet. bounced: e-mails to the address bounced, or it was cleaned from the list.
ISO 3166-1 alpha-2
IANA time zone name
ISO 639-1 code
Set values of custom fields by field id, typed by the field: a date as YYYY-MM-DD, a datetime in ISO 8601
object
object
Where the contact came from; widget, automation, import, shopify, mailchimp, klaviyo name the source. api: added through the public API or an AI assistant (MCP). manual: added by hand in the admin. other: a source not listed here.
Depends on the type: the widget, the automation, the admin user, the import task, the Shopify customer, the Mailchimp id; for api the API key, or the user whose access added it when there is no key
E-mails to the contact, the keys of EmailMetrics; send includes
e-mails rejected on sending, also counted in bounce.
object
When the contact was added at its source; imports keep the original date
Last change; equals created_at when none was recorded
Empty unless the contact could not be read in full; wording is not stable, do not parse
{ "result": "created", "reason": "contact_not_found", "contact": { "status": "subscribed", "source": { "type": "widget" } }}Headers
Section titled “Headers”API version the response was produced for
Request id, echoed from the request when valid
Limit of the tightest bucket
Requests left in its window
End of its window, epoch seconds
True when the response is a stored one, replayed
The documented shape of every error body on the versioned surface.
object
object
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "reason": "example", "next": "example", "request_id": "example", "errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ], "metadata": {}}Headers
Section titled “Headers”API version the response was produced for
Request id, echoed from the request when valid
Limit of the tightest bucket
Requests left in its window
End of its window, epoch seconds
The documented shape of every error body on the versioned surface.
object
object
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "reason": "example", "next": "example", "request_id": "example", "errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ], "metadata": {}}Headers
Section titled “Headers”API version the response was produced for
Request id, echoed from the request when valid
Limit of the tightest bucket
Requests left in its window
End of its window, epoch seconds
Authentication scheme: Bearer
The documented shape of every error body on the versioned surface.
object
object
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "reason": "example", "next": "example", "request_id": "example", "errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ], "metadata": {}}Headers
Section titled “Headers”API version the response was produced for
Request id, echoed from the request when valid
Limit of the tightest bucket
Requests left in its window
End of its window, epoch seconds
The documented shape of every error body on the versioned surface.
object
object
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "reason": "example", "next": "example", "request_id": "example", "errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ], "metadata": {}}Headers
Section titled “Headers”API version the response was produced for
Request id, echoed from the request when valid
Limit of the tightest bucket
Requests left in its window
End of its window, epoch seconds
contact_email_taken: another request took the e-mail at the same moment; repeat the call. idempotency_in_progress: a repeat of a request still running
The documented shape of every error body on the versioned surface.
object
object
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "reason": "example", "next": "example", "request_id": "example", "errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ], "metadata": {}}Headers
Section titled “Headers”API version the response was produced for
Request id, echoed from the request when valid
Limit of the tightest bucket
Requests left in its window
End of its window, epoch seconds
The documented shape of every error body on the versioned surface.
object
object
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "reason": "example", "next": "example", "request_id": "example", "errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ], "metadata": {}}Headers
Section titled “Headers”API version the response was produced for
Request id, echoed from the request when valid
Limit of the tightest bucket
Requests left in its window
End of its window, epoch seconds
contact_field_invalid: an unknown field, a field of the other type or a value of another type. contact_unreadable: the stored contact cannot be read; it can only be deleted. validation_failed: the body is invalid: an e-mail, country, phone, time zone, language or tag name that cannot be read, a tag both added and removed. idempotency_key_reused: the Idempotency-Key was sent before with another body or version
The documented shape of every error body on the versioned surface.
object
object
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "reason": "example", "next": "example", "request_id": "example", "errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ], "metadata": {}}Headers
Section titled “Headers”API version the response was produced for
Request id, echoed from the request when valid
Limit of the tightest bucket
Requests left in its window
End of its window, epoch seconds
rate_limited: a request limit is spent. A call that creates or restores a contact also spends the contact creation limit, its own per key; an update does not
The documented shape of every error body on the versioned surface.
object
object
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "reason": "example", "next": "example", "request_id": "example", "errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ], "metadata": {}}Headers
Section titled “Headers”API version the response was produced for
Request id, echoed from the request when valid
Limit of the tightest bucket
Requests left in its window
End of its window, epoch seconds
Seconds until the bucket refills
The documented shape of every error body on the versioned surface.
object
object
{ "type": "example", "title": "example", "status": 1, "detail": "example", "instance": "example", "reason": "example", "next": "example", "request_id": "example", "errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ], "metadata": {}}Headers
Section titled “Headers”API version the response was produced for
Request id, echoed from the request when valid
Limit of the tightest bucket
Requests left in its window
End of its window, epoch seconds