Skip to content

Create or update a contact by e-mail

POST
https://api.getsitecontrol.com/v1/sites/{site_id}/contacts
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.

FieldDescription
site_id
required
Site Id
integer
FieldDescription
Idempotency-Key
Idempotency-Key

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.

string
>= 1 characters <= 128 characters /^[A-Za-z0-9_-]+$/

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.

Media typeapplication/json
ContactUpsert

The fields every contact write takes; an upsert finds by email.

object
FieldDescription
email
required
Email

Finds the contact, without case; kept as stored when found, stored in lower case when created

string
<= 250 characters
status
Status

subscribed or unsubscribed; bounced is set only by e-mail delivery. Left out: unchanged, a new contact is subscribed; may not be null

string
Allowed values: subscribed unsubscribed
first_name
First Name

Null clears it

string
nullable <= 250 characters
last_name
Last Name

Null clears it

string
nullable <= 250 characters
country
Country

ISO 3166-1 alpha-2 code or an English country name; stored as the code; null clears it

string
nullable <= 250 characters
phone
Phone

In the international format, or national with country in the same request; stored normalized; null clears it

string
nullable <= 250 characters
timezone
Timezone

IANA time zone name such as Europe/Berlin; null clears it

string
nullable <= 250 characters
language
Language

ISO 639-1 code such as de; stored in lower case; null clears it

string
nullable <= 250 characters
fields
Fields

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
FieldDescription
key
additional properties
Allowed types:
stringintegernumberboolean
string:
<= 1000 characters
add_tags
Add Tags

Tag names to add, compared without case; a tag the site does not have is created. Tags the contact has stay

Array<string>
<= 100 items
remove_tags
Remove Tags

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

Array<string>
<= 100 items

The contact existed: updated, or nothing to change

Media typeapplication/json
ContactWritten
object
FieldDescription
result
required

created, restored, updated or unchanged; an item that cannot be written is refused with an error instead

string
Allowed values: created updated restored unchanged failed
changed
required
Changed

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

Array<string>
reason

contact_status_transition when the status sent was not applied; the rest was written. Null otherwise

string
Allowed values: contact_not_found contact_email_taken contact_tag_invalid contact_field_invalid contact_unreadable contact_status_transition contact_value_invalid
detail
Detail
string
nullable
contact
required
Contact

As get_contact returns it

object
FieldDescription
id
required
Id
integer
email
required
Email
string
status
required
Status

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.

string
Allowed values: subscribed unsubscribed bounced
first_name
First Name
string
nullable
last_name
Last Name
string
nullable
phone
Phone
string
nullable
country
Country

ISO 3166-1 alpha-2

string
nullable
timezone
Timezone

IANA time zone name

string
nullable
language
Language

ISO 639-1 code

string
nullable
fields
required
Fields

Set values of custom fields by field id, typed by the field: a date as YYYY-MM-DD, a datetime in ISO 8601

object
tags
required
Tags
Array<object>
ContactTag
object
FieldDescription
id
required
Id
integer
name
required
Name
string
source
required
ContactSource
object
FieldDescription
type
required
Type

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.

string
Allowed values: widget automation manual import shopify mailchimp klaviyo api other
id
Id

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

integer
nullable
email_stats
required
ContactEmailStats

E-mails to the contact, the keys of EmailMetrics; send includes e-mails rejected on sending, also counted in bounce.

object
FieldDescription
send
required
Send
integer
deliver
required
Deliver
integer
bounce
required
Bounce
integer
open
required
Open
integer
click
required
Click
integer
unsubscribe
required
Unsubscribe
integer
complaint
required
Complaint
integer
created_at
required
Created At

When the contact was added at its source; imports keep the original date

string format: date-time
updated_at
required
Updated At

Last change; equals created_at when none was recorded

string format: date-time
warnings
required
Warnings

Empty unless the contact could not be read in full; wording is not stable, do not parse

Array<string>

Example

{
"result": "created",
"reason": "contact_not_found",
"contact": {
"status": "subscribed",
"source": {
"type": "widget"
}
}
}
FieldDescription
GSC-Version
string

API version the response was produced for

X-Request-Id
string

Request id, echoed from the request when valid

X-RateLimit-Limit
integer

Limit of the tightest bucket

X-RateLimit-Remaining
integer

Requests left in its window

X-RateLimit-Reset
integer

End of its window, epoch seconds

Idempotency-Replayed
string

True when the response is a stored one, replayed

Successful Response

Media typeapplication/json
ContactWritten
object
FieldDescription
result
required

created, restored, updated or unchanged; an item that cannot be written is refused with an error instead

string
Allowed values: created updated restored unchanged failed
changed
required
Changed

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

Array<string>
reason

contact_status_transition when the status sent was not applied; the rest was written. Null otherwise

string
Allowed values: contact_not_found contact_email_taken contact_tag_invalid contact_field_invalid contact_unreadable contact_status_transition contact_value_invalid
detail
Detail
string
nullable
contact
required
Contact

As get_contact returns it

object
FieldDescription
id
required
Id
integer
email
required
Email
string
status
required
Status

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.

string
Allowed values: subscribed unsubscribed bounced
first_name
First Name
string
nullable
last_name
Last Name
string
nullable
phone
Phone
string
nullable
country
Country

ISO 3166-1 alpha-2

string
nullable
timezone
Timezone

IANA time zone name

string
nullable
language
Language

ISO 639-1 code

string
nullable
fields
required
Fields

Set values of custom fields by field id, typed by the field: a date as YYYY-MM-DD, a datetime in ISO 8601

object
tags
required
Tags
Array<object>
ContactTag
object
FieldDescription
id
required
Id
integer
name
required
Name
string
source
required
ContactSource
object
FieldDescription
type
required
Type

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.

string
Allowed values: widget automation manual import shopify mailchimp klaviyo api other
id
Id

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

integer
nullable
email_stats
required
ContactEmailStats

E-mails to the contact, the keys of EmailMetrics; send includes e-mails rejected on sending, also counted in bounce.

object
FieldDescription
send
required
Send
integer
deliver
required
Deliver
integer
bounce
required
Bounce
integer
open
required
Open
integer
click
required
Click
integer
unsubscribe
required
Unsubscribe
integer
complaint
required
Complaint
integer
created_at
required
Created At

When the contact was added at its source; imports keep the original date

string format: date-time
updated_at
required
Updated At

Last change; equals created_at when none was recorded

string format: date-time
warnings
required
Warnings

Empty unless the contact could not be read in full; wording is not stable, do not parse

Array<string>

Example

{
"result": "created",
"reason": "contact_not_found",
"contact": {
"status": "subscribed",
"source": {
"type": "widget"
}
}
}
FieldDescription
GSC-Version
string

API version the response was produced for

X-Request-Id
string

Request id, echoed from the request when valid

X-RateLimit-Limit
integer

Limit of the tightest bucket

X-RateLimit-Remaining
integer

Requests left in its window

X-RateLimit-Reset
integer

End of its window, epoch seconds

Idempotency-Replayed
string

True when the response is a stored one, replayed

Unsupported GSC-Version

Media typeapplication/problem+json
Problem

The documented shape of every error body on the versioned surface.

object
FieldDescription
type
required
Type
string
title
required
Title
string
status
required
Status
integer
detail
Detail
string
nullable
instance
required
Instance
string
reason
Reason
string
nullable
next
Next
string
nullable
request_id
required
Request Id
string
errors
Errors
Array<object>
nullable
ValidationIssue
object
FieldDescription
loc
required
Loc
Array
msg
required
Msg
string
type
required
Type
string
metadata
Metadata
object

Example · Generated

{
"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": {}
}
FieldDescription
GSC-Version
string

API version the response was produced for

X-Request-Id
string

Request id, echoed from the request when valid

X-RateLimit-Limit
integer

Limit of the tightest bucket

X-RateLimit-Remaining
integer

Requests left in its window

X-RateLimit-Reset
integer

End of its window, epoch seconds

Missing or invalid API key or access token

Media typeapplication/problem+json
Problem

The documented shape of every error body on the versioned surface.

object
FieldDescription
type
required
Type
string
title
required
Title
string
status
required
Status
integer
detail
Detail
string
nullable
instance
required
Instance
string
reason
Reason
string
nullable
next
Next
string
nullable
request_id
required
Request Id
string
errors
Errors
Array<object>
nullable
ValidationIssue
object
FieldDescription
loc
required
Loc
Array
msg
required
Msg
string
type
required
Type
string
metadata
Metadata
object

Example · Generated

{
"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": {}
}
FieldDescription
GSC-Version
string

API version the response was produced for

X-Request-Id
string

Request id, echoed from the request when valid

X-RateLimit-Limit
integer

Limit of the tightest bucket

X-RateLimit-Remaining
integer

Requests left in its window

X-RateLimit-Reset
integer

End of its window, epoch seconds

WWW-Authenticate
string

Authentication scheme: Bearer

Access denied

Media typeapplication/problem+json
Problem

The documented shape of every error body on the versioned surface.

object
FieldDescription
type
required
Type
string
title
required
Title
string
status
required
Status
integer
detail
Detail
string
nullable
instance
required
Instance
string
reason
Reason
string
nullable
next
Next
string
nullable
request_id
required
Request Id
string
errors
Errors
Array<object>
nullable
ValidationIssue
object
FieldDescription
loc
required
Loc
Array
msg
required
Msg
string
type
required
Type
string
metadata
Metadata
object

Example · Generated

{
"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": {}
}
FieldDescription
GSC-Version
string

API version the response was produced for

X-Request-Id
string

Request id, echoed from the request when valid

X-RateLimit-Limit
integer

Limit of the tightest bucket

X-RateLimit-Remaining
integer

Requests left in its window

X-RateLimit-Reset
integer

End of its window, epoch seconds

Not found

Media typeapplication/problem+json
Problem

The documented shape of every error body on the versioned surface.

object
FieldDescription
type
required
Type
string
title
required
Title
string
status
required
Status
integer
detail
Detail
string
nullable
instance
required
Instance
string
reason
Reason
string
nullable
next
Next
string
nullable
request_id
required
Request Id
string
errors
Errors
Array<object>
nullable
ValidationIssue
object
FieldDescription
loc
required
Loc
Array
msg
required
Msg
string
type
required
Type
string
metadata
Metadata
object

Example · Generated

{
"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": {}
}
FieldDescription
GSC-Version
string

API version the response was produced for

X-Request-Id
string

Request id, echoed from the request when valid

X-RateLimit-Limit
integer

Limit of the tightest bucket

X-RateLimit-Remaining
integer

Requests left in its window

X-RateLimit-Reset
integer

End of its window, epoch seconds

Media typeapplication/problem+json

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

Problem

The documented shape of every error body on the versioned surface.

object
FieldDescription
type
required
Type
string
title
required
Title
string
status
required
Status
integer
detail
Detail
string
nullable
instance
required
Instance
string
reason
Reason
string
nullable
next
Next
string
nullable
request_id
required
Request Id
string
errors
Errors
Array<object>
nullable
ValidationIssue
object
FieldDescription
loc
required
Loc
Array
msg
required
Msg
string
type
required
Type
string
metadata
Metadata
object

Example · Generated

{
"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": {}
}
FieldDescription
GSC-Version
string

API version the response was produced for

X-Request-Id
string

Request id, echoed from the request when valid

X-RateLimit-Limit
integer

Limit of the tightest bucket

X-RateLimit-Remaining
integer

Requests left in its window

X-RateLimit-Reset
integer

End of its window, epoch seconds

Request body too large

Media typeapplication/problem+json
Problem

The documented shape of every error body on the versioned surface.

object
FieldDescription
type
required
Type
string
title
required
Title
string
status
required
Status
integer
detail
Detail
string
nullable
instance
required
Instance
string
reason
Reason
string
nullable
next
Next
string
nullable
request_id
required
Request Id
string
errors
Errors
Array<object>
nullable
ValidationIssue
object
FieldDescription
loc
required
Loc
Array
msg
required
Msg
string
type
required
Type
string
metadata
Metadata
object

Example · Generated

{
"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": {}
}
FieldDescription
GSC-Version
string

API version the response was produced for

X-Request-Id
string

Request id, echoed from the request when valid

X-RateLimit-Limit
integer

Limit of the tightest bucket

X-RateLimit-Remaining
integer

Requests left in its window

X-RateLimit-Reset
integer

End of its window, epoch seconds

Media typeapplication/problem+json

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

Problem

The documented shape of every error body on the versioned surface.

object
FieldDescription
type
required
Type
string
title
required
Title
string
status
required
Status
integer
detail
Detail
string
nullable
instance
required
Instance
string
reason
Reason
string
nullable
next
Next
string
nullable
request_id
required
Request Id
string
errors
Errors
Array<object>
nullable
ValidationIssue
object
FieldDescription
loc
required
Loc
Array
msg
required
Msg
string
type
required
Type
string
metadata
Metadata
object

Example · Generated

{
"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": {}
}
FieldDescription
GSC-Version
string

API version the response was produced for

X-Request-Id
string

Request id, echoed from the request when valid

X-RateLimit-Limit
integer

Limit of the tightest bucket

X-RateLimit-Remaining
integer

Requests left in its window

X-RateLimit-Reset
integer

End of its window, epoch seconds

Media typeapplication/problem+json

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

Problem

The documented shape of every error body on the versioned surface.

object
FieldDescription
type
required
Type
string
title
required
Title
string
status
required
Status
integer
detail
Detail
string
nullable
instance
required
Instance
string
reason
Reason
string
nullable
next
Next
string
nullable
request_id
required
Request Id
string
errors
Errors
Array<object>
nullable
ValidationIssue
object
FieldDescription
loc
required
Loc
Array
msg
required
Msg
string
type
required
Type
string
metadata
Metadata
object

Example · Generated

{
"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": {}
}
FieldDescription
GSC-Version
string

API version the response was produced for

X-Request-Id
string

Request id, echoed from the request when valid

X-RateLimit-Limit
integer

Limit of the tightest bucket

X-RateLimit-Remaining
integer

Requests left in its window

X-RateLimit-Reset
integer

End of its window, epoch seconds

Retry-After
integer

Seconds until the bucket refills

Temporarily unavailable; retry later

Media typeapplication/problem+json
Problem

The documented shape of every error body on the versioned surface.

object
FieldDescription
type
required
Type
string
title
required
Title
string
status
required
Status
integer
detail
Detail
string
nullable
instance
required
Instance
string
reason
Reason
string
nullable
next
Next
string
nullable
request_id
required
Request Id
string
errors
Errors
Array<object>
nullable
ValidationIssue
object
FieldDescription
loc
required
Loc
Array
msg
required
Msg
string
type
required
Type
string
metadata
Metadata
object

Example · Generated

{
"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": {}
}
FieldDescription
GSC-Version
string

API version the response was produced for

X-Request-Id
string

Request id, echoed from the request when valid

X-RateLimit-Limit
integer

Limit of the tightest bucket

X-RateLimit-Remaining
integer

Requests left in its window

X-RateLimit-Reset
integer

End of its window, epoch seconds