Create or update a contact
const url = 'https://api.nexacloser.com/v1/contacts';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"email":"[email protected]","phone":"+14155550101","first_name":"Dana","last_name":"Levi","source":"Website contact form","tags":["website","consult-request"],"custom_fields":{"visit_type":"first_visit","contact_time":"morning"},"attribution":{"utm_source":"google","utm_medium":"cpc","utm_campaign":"fall-promo","landing_url":"https://www.example.com/contact"},"consent":{"granted":true,"channels":["email","sms"],"text":"I agree to receive emails and text messages from Example Clinic. Reply STOP to opt out.","version":"2026-11-01","at":"2026-11-05T14:03:11Z","ip":"203.0.113.24","page_url":"https://www.example.com/contact"}}'};
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.nexacloser.com/v1/contacts \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "email": "[email protected]", "phone": "+14155550101", "first_name": "Dana", "last_name": "Levi", "source": "Website contact form", "tags": [ "website", "consult-request" ], "custom_fields": { "visit_type": "first_visit", "contact_time": "morning" }, "attribution": { "utm_source": "google", "utm_medium": "cpc", "utm_campaign": "fall-promo", "landing_url": "https://www.example.com/contact" }, "consent": { "granted": true, "channels": [ "email", "sms" ], "text": "I agree to receive emails and text messages from Example Clinic. Reply STOP to opt out.", "version": "2026-11-01", "at": "2026-11-05T14:03:11Z", "ip": "203.0.113.24", "page_url": "https://www.example.com/contact" } }'Creates a CRM contact, or updates the matching one.
Matching. Inside your workspace, among live (not deleted) contacts, NexaCloser looks for
the same phone number first, then the same email. A match is updated: non-empty values
you send overwrite, empty values keep what is there, tags are added to the existing tags and
custom_fields are merged into the existing values. No match: a new contact is created.
If the workspace turned on allow duplicate contacts, every call creates a new contact.
Source. Every contact created through the API gets the CRM source api. The source
label you send (for example Website contact form) is saved as the contact’s source
channel, only when the contact is created; an existing contact keeps its own.
Automations. Creating a contact fires the workspace’s contact created automations. New tags fire tag added automations.
Consent. consent.granted: true records an opt-in per channel. granted: false records
nothing: an unticked box is not an opt-out. A person who opted out earlier stays opted out.
What comes back. Only the contact id, whether it was created, how it was matched, the consent outcome per channel and warnings. Other data of an existing contact is never returned, so a contacts-only key can write but cannot read your CRM.
Warnings. Fields the endpoint does not know are ignored and listed in warnings
(unknown_field:<name>, unknown_attribution_key:<key>), as are custom field keys with no
field defined in the workspace (unknown_custom_field:<key>, still saved).
Test keys (nxa_test_…) validate the request exactly like a live key but write nothing
and read no contact: the response is 200 with test: true and the normalized values.
Access. Works with a full-access key or a Contacts: create & update only key. Call it from your server; the only browser origin accepted is the try-it console on docs.nexacloser.com.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”Send email, phone or both.
object
The contact’s email. Stored lowercase.
The contact’s phone. E.164 (+14155550101) is best; a number without a country code is read with the workspace’s country. Must come out as 9 to 15 digits.
Full name. Or send first_name and last_name.
A label for where the contact came from, e.g. Website contact form. Saved as the contact’s source channel, only when the contact is created; an existing contact keeps its own. (The CRM source of an API-created contact is always api.)
Added to the contact’s tags. Never removes a tag.
Field key → value. Keys match ^[a-z0-9_]{1,64}$; values are string, number, boolean or null.
Merged into the contact’s existing values. A key with no field defined in the workspace is
still saved and is listed in warnings as unknown_custom_field:<key>.
object
Language for messages to this contact. Omit to use the workspace default.
Where this visit came from. The first touch is kept forever; the last touch is updated on every call.
Values are trimmed, up to 2,048 characters. Any other key is ignored and listed in warnings as
unknown_attribution_key:<key>.
object
What the person agreed to. Stored per channel, plus an append-only audit record with the
wording, its version, the time, the page and the key that sent it. A person who opted out of
a channel earlier stays opted out: that channel comes back withdrawn with skipped: true.
object
true = the person opted in; one record per channel is stored. false = nothing is recorded (an unticked box is not an opt-out).
Default email when you sent an email, plus sms and whatsapp when you sent a phone.
The exact wording the person saw. Required when granted is true.
Your version label for that wording, e.g. 2026-11-01.
Language of the wording. Default is the contact’s preferred_language, else en.
When the person agreed. Default is the time of the request. May not be more than 5 minutes in the future.
The person’s IP address (not your server’s). IPv4 or IPv6.
The person’s browser user agent.
The page where they agreed.
Send email, phone or both.
object
The contact’s email. Stored lowercase.
The contact’s phone. E.164 (+14155550101) is best; a number without a country code is read with the workspace’s country. Must come out as 9 to 15 digits.
Full name. Or send first_name and last_name.
A label for where the contact came from, e.g. Website contact form. Saved as the contact’s source channel, only when the contact is created; an existing contact keeps its own. (The CRM source of an API-created contact is always api.)
Added to the contact’s tags. Never removes a tag.
Field key → value. Keys match ^[a-z0-9_]{1,64}$; values are string, number, boolean or null.
Merged into the contact’s existing values. A key with no field defined in the workspace is
still saved and is listed in warnings as unknown_custom_field:<key>.
object
Language for messages to this contact. Omit to use the workspace default.
Where this visit came from. The first touch is kept forever; the last touch is updated on every call.
Values are trimmed, up to 2,048 characters. Any other key is ignored and listed in warnings as
unknown_attribution_key:<key>.
object
What the person agreed to. Stored per channel, plus an append-only audit record with the
wording, its version, the time, the page and the key that sent it. A person who opted out of
a channel earlier stays opted out: that channel comes back withdrawn with skipped: true.
object
true = the person opted in; one record per channel is stored. false = nothing is recorded (an unticked box is not an opt-out).
Default email when you sent an email, plus sms and whatsapp when you sent a phone.
The exact wording the person saw. Required when granted is true.
Your version label for that wording, e.g. 2026-11-01.
Language of the wording. Default is the contact’s preferred_language, else en.
When the person agreed. Default is the time of the request. May not be more than 5 minutes in the future.
The person’s IP address (not your server’s). IPv4 or IPv6.
The person’s browser user agent.
The page where they agreed.
Examples
Website form with marketing consent
{ "phone": "+14155550101", "first_name": "Dana", "last_name": "Levi", "source": "Website contact form", "tags": [ "website", "consult-request" ], "custom_fields": { "visit_type": "first_visit", "contact_time": "morning" }, "attribution": { "utm_source": "google", "utm_medium": "cpc", "utm_campaign": "fall-promo", "landing_url": "https://www.example.com/contact" }, "consent": { "granted": true, "channels": [ "email", "sms" ], "text": "I agree to receive emails and text messages from Example Clinic. Reply STOP to opt out.", "version": "2026-11-01", "at": "2026-11-05T14:03:11Z", "ip": "203.0.113.24", "page_url": "https://www.example.com/contact" }}Email only
{ "full_name": "Dana Levi"}Responses
Section titled “Responses”An existing contact was updated, or (test key) the request is valid and nothing was written.
object
The contact id.
true = a new contact; false = an existing one was updated.
How the existing contact was found; null when created.
One entry per channel when you sent consent; empty otherwise.
object
granted = stored. withdrawn = an earlier opt-out was kept (skipped: true). error = this channel could not be stored; the contact itself was saved.
true when an earlier opt-out was kept and your consent was not applied.
Non-blocking notes, e.g. unknown_custom_field:visit_type.
object
Test keys never look up existing contacts.
What a live key would send on: email, phone (E.164), full_name, source, tags, custom_fields, preferred_language, attribution and consent.
object
Examples
Matched by email and updated
{ "id": "cnt_8hT2kQx91L", "created": false, "matched_by": "email", "consent": [ { "channel": "email", "status": "granted" }, { "channel": "sms", "status": "withdrawn", "skipped": true } ], "warnings": [ "unknown_custom_field:contact_time" ]}Test key, nothing written
{ "test": true, "valid": true, "would": "create_or_update", "normalized": { "phone": "14155550101", "full_name": "Dana Levi", "source": "Website contact form", "tags": [ "website", "consult-request" ], "custom_fields": { "visit_type": "first_visit", "contact_time": "morning" }, "preferred_language": null, "attribution": { "utm_source": "google", "utm_medium": "cpc", "landing_url": "https://www.example.com/contact" }, "consent": { "granted": true, "channels": [ "email", "sms" ], "purpose": "marketing", "text": "I agree to receive emails and text messages from Example Clinic. Reply STOP to opt out.", "version": "2026-11-01", "at": "2026-11-05T14:03:11.000Z", "language": "en", "ip": "203.0.113.24", "user_agent": null, "page_url": "https://www.example.com/contact" } }, "warnings": [ "unknown_custom_field:visit_type", "unknown_custom_field:contact_time" ]}Headers
Section titled “Headers”Requests allowed in the current short window.
Requests left in the current short window.
A new contact was created.
object
The contact id.
true = a new contact; false = an existing one was updated.
How the existing contact was found; null when created.
One entry per channel when you sent consent; empty otherwise.
object
granted = stored. withdrawn = an earlier opt-out was kept (skipped: true). error = this channel could not be stored; the contact itself was saved.
true when an earlier opt-out was kept and your consent was not applied.
Non-blocking notes, e.g. unknown_custom_field:visit_type.
Example
{ "id": "cnt_8hT2kQx91L", "created": true, "matched_by": null, "consent": [ { "channel": "email", "status": "granted" }, { "channel": "sms", "status": "granted" } ], "warnings": []}Headers
Section titled “Headers”Requests allowed in the current short window.
Requests left in the current short window.
The body is not valid JSON.
object
A stable, machine-readable code.
Example
{ "error": "invalid_json"}Missing, wrong, revoked or rotated key, a key whose access level does not include this endpoint, or a key whose workspace cannot be resolved.
object
A stable, machine-readable code.
Example
{ "error": "unauthorized"}The key is valid but its access level does not include this endpoint.
object
A stable, machine-readable code.
Example
{ "error": "forbidden"}Only POST is accepted.
object
A stable, machine-readable code.
Example
{ "error": "method_not_allowed"}The body is larger than 64 KB.
object
A stable, machine-readable code.
Example
{ "error": "payload_too_large"}The body is valid JSON but a field is wrong. Every problem is listed.
object
object
The field, with dots for nested fields (e.g. consent.text, custom_fields.visit_type).
Example
{ "error": "validation_failed", "details": [ { "field": "email", "code": "invalid_email" }, { "field": "custom_fields.visit_type", "code": "invalid_custom_field_value" }, { "field": "consent.text", "code": "consent_text_required" } ]}Too many requests. Wait Retry-After seconds.
object
Seconds to wait.
Example
{ "error": "rate_limited", "message": "Too many requests", "retry_after": 7}Headers
Section titled “Headers”Seconds to wait before retrying.
Something failed on our side. Safe to retry with backoff.
object
A stable, machine-readable code.
Example
{ "error": "internal"}