Update a customer
const url = 'https://api.usesonora.com/v1/customers/example';const options = { method: 'PATCH', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"name":"example","arr":1,"stage":"example","tier":"example","renewalDate":"2026-04-15","contractStartDate":"2026-04-15","owner":{"email":"hello@example.com","name":"example"},"customFields":{"additionalProperty":"example"}}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request PATCH \ --url https://api.usesonora.com/v1/customers/example \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "name": "example", "arr": 1, "stage": "example", "tier": "example", "renewalDate": "2026-04-15", "contractStartDate": "2026-04-15", "owner": { "email": "hello@example.com", "name": "example" }, "customFields": { "additionalProperty": "example" } }'Change a customer’s fields, all or nothing. A field you change is taken over from your CRM exactly as an edit in Sonora is: its value sticks, and the CRM’s later changes to it are held back until someone resumes sync on it in Sonora. A field you send unchanged is left alone, so sending a whole record back only takes over what differs. Send If-Match with the ETag you read to refuse the write if the customer changed in between. The key needs customers:read as well as customers:write, since the response is the customer as saved.
Requires the customers:write scope. API keys only.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”The customer’s Sonora id — or, with idType, its id in another system, or its domain.
Query Parameters
Section titled “Query Parameters”How id names the account: sonora (the default; Sonora’s customer id), salesforce, hubspot, domain, or custom_field:<key>. So GET /customers/001Ab…?idType=salesforce finds a customer by its Salesforce Account id.
Header Parameters
Section titled “Header Parameters”The ETag from your last read. If the resource has changed since, nothing is written and the call answers 412 precondition_failed. Without it, the last write wins.
Request Bodyrequired
Section titled “Request Bodyrequired”The fields to change. A field you leave out is kept, null clears one, and a value equal to what the customer already holds is not a change.
object
Annual recurring revenue, 0–9,999,999,999.99; kept to the cent.
Custom fields by key, each as its type says (GET /customer-fields); null clears one. A user field takes a list of people, [{ "email" }]. Keys you leave out are kept.
object
Examplegenerated
{ "name": "example", "arr": 1, "stage": "example", "tier": "example", "renewalDate": "2026-04-15", "contractStartDate": "2026-04-15", "owner": { "email": "hello@example.com", "name": "example" }, "customFields": { "additionalProperty": "example" }}Responses
Section titled “Responses”The customer, as saved.
A customer account, with its account team and the fields edits have taken over.
object
Domains whose email addresses belong to this customer.
Annual recurring revenue.
Sonora’s running summary of the account.
Your workspace’s custom fields, by key — only fields visible in Sonora.
object
As the field’s type says (GET /customer-fields). A user field is a list of people: [{ name, email }].
The record’s id in another system.
object
The system the id is from: salesforce, hubspot, zendesk, intercom, …
The customer’s health, as the customers page shows it.
object
The state’s key on your workspace’s health scale — by default thriving, healthy, emerging_concerns or at_risk — or no_data when nothing could be scored.
The state as Sonora shows it: Thriving, At Risk, …
0–10, higher is healthier. Null when there is nothing to score (no_data).
Whether your scale counts this state as at risk.
ECSAT: satisfaction Sonora estimates from what the customer says, averaged over recent touchpoints.
object
The average estimated satisfaction of the customer’s scored calls, emails and tickets, 0–10, to one decimal.
The days back the average covers.
Only with ?include=enrichments: the latest value of each enrichment that has one, in GET /enrichment-configs order.
A record’s latest value of one enrichment.
object
The enrichment’s id (GET /enrichment-configs/{configId}).
What the value is: short_text/long_text a string, number a number, score 1–10 (10 strongest), boolean, date YYYY-MM-DD, email, url, or select one of options (a list of them with multiple).
As outputType says. Null when there wasn’t enough to go on, for any outputType.
Why the AI gave this value, when it said.
Calls, emails, meetings and tickets Sonora has seen.
Moves on any change to the record, including Sonora’s own bookkeeping (engagement counts), so a sync may see a customer again with no field you read changed.
object
This resource’s canonical URL on the API host.
The account team beyond the owner, with each member’s role where the CRM gives one.
object
Null for a name typed in by hand or synced without a user.
Fields an edit — in Sonora or through this API — has taken over from sync: your CRM’s changes to them are held back until someone resumes sync on the field in Sonora. Standard fields by name (arr, owner), custom fields as customFields.<key>.
Example
{ "ecsat": { "windowDays": 90 }, "enrichments": [ { "outputType": "short_text" } ]}Headers
Section titled “Headers”A fingerprint of the fields PATCH can change. Send it back as If-Match to update only if none of them has changed since.
invalid_json: The request body is not valid JSON.
invalid_request: A parameter or body field is missing, malformed or unknown; details.issues names each.
The one error shape every endpoint answers with.
object
object
Stable snake_case code to branch on; see the error table.
For humans; may change.
object
Quote it when asking for help.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": { "additionalProperty": "example" }, "requestId": "example" }}unauthorized: The Authorization header is missing, or the key is invalid, revoked or expired.
The one error shape every endpoint answers with.
object
object
Stable snake_case code to branch on; see the error table.
For humans; may change.
object
Quote it when asking for help.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": { "additionalProperty": "example" }, "requestId": "example" }}insufficient_scope: The key or token lacks the scope this endpoint needs, and details.required names it; or an OAuth access token called an endpoint only an API key can.
The one error shape every endpoint answers with.
object
object
Stable snake_case code to branch on; see the error table.
For humans; may change.
object
Quote it when asking for help.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": { "additionalProperty": "example" }, "requestId": "example" }}customer_not_found: No customer matches the id (and idType) in the path.
The one error shape every endpoint answers with.
object
object
Stable snake_case code to branch on; see the error table.
For humans; may change.
object
Quote it when asking for help.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": { "additionalProperty": "example" }, "requestId": "example" }}customer_archived: The customer is archived; unarchive it before changing it.
The one error shape every endpoint answers with.
object
object
Stable snake_case code to branch on; see the error table.
For humans; may change.
object
Quote it when asking for help.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": { "additionalProperty": "example" }, "requestId": "example" }}precondition_failed: The resource changed since the ETag that If-Match names. Read it again, then retry.
The one error shape every endpoint answers with.
object
object
Stable snake_case code to branch on; see the error table.
For humans; may change.
object
Quote it when asking for help.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": { "additionalProperty": "example" }, "requestId": "example" }}payload_too_large: The request body is too large; details.maxBytes gives the limit.
The one error shape every endpoint answers with.
object
object
Stable snake_case code to branch on; see the error table.
For humans; may change.
object
Quote it when asking for help.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": { "additionalProperty": "example" }, "requestId": "example" }}invalid_fields: A field’s value can’t be saved, so nothing was; details.issues names each field and why.
The one error shape every endpoint answers with.
object
object
Stable snake_case code to branch on; see the error table.
For humans; may change.
object
Quote it when asking for help.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": { "additionalProperty": "example" }, "requestId": "example" }}rate_limited: Too many requests for this key; retry after Retry-After seconds.
The one error shape every endpoint answers with.
object
object
Stable snake_case code to branch on; see the error table.
For humans; may change.
object
Quote it when asking for help.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": { "additionalProperty": "example" }, "requestId": "example" }}internal_error: Something went wrong on Sonora’s side. Retry, and quote requestId if it persists.
The one error shape every endpoint answers with.
object
object
Stable snake_case code to branch on; see the error table.
For humans; may change.
object
Quote it when asking for help.
Examplegenerated
{ "error": { "code": "example", "message": "example", "details": { "additionalProperty": "example" }, "requestId": "example" }}Try it
Try it runs the reference's read endpoints, and this one changes data, so it needs an API key with the customers:write scope. Create a key in Sonora under Settings → API and call it from your own code.