Overview
Sonora’s API is plain HTTPS and JSON, authenticated with an API key. Browsers can call it too (any origin, never with cookies), but a key in front-end code can be read by anyone who loads the page, so keep keys on a server. This page covers what every endpoint shares; the API reference lists each one with its parameters, fields and error codes, and the guides walk through a job end to end.
API keys
Section titled “API keys”Create a key under Settings → API → API keys in Sonora. Give it a name, the scopes it needs and an expiry. The key is shown once — copy it into your secret store before closing the dialog. Revoke a key there at any time; it stops working immediately.
A key belongs to your workspace, not to the person who made it: it keeps working if they leave, and what it does is recorded as the key’s.
Base URL and authentication
Section titled “Base URL and authentication”Every workspace uses the same base URL:
https://api.usesonora.com/v1The key says which workspace a call is for, so there is nothing else to configure. Every request carries it as a Bearer token:
curl https://api.usesonora.com/v1/me \ -H "Authorization: Bearer sonora_sk_…"GET /me needs no scope: it returns your workspace and the key’s name, prefix, scopes and expiry, so it’s the quickest way to check a key works.
Try it from the reference
Section titled “Try it from the reference”Every read endpoint in the API reference ends with a Try it panel that runs the request against your own workspace, with no key needed. Sign in with Sonora takes you to Sonora, where you pick the workspace and approve read access; you come back with an access token that lasts an hour, stays in that browser tab, and reads only what your role in Sonora lets you see. Sign out revokes it. Each panel also shows the request as curl, so you can run it from your own code with a key.
Try it only reads. Endpoints that change data, and GET /me, which describes the key calling it, need an API key.
Scopes
Section titled “Scopes”A key carries scopes, and each endpoint needs one. A call without it answers 403 insufficient_scope, naming the scope in details.required. Give each integration only the scopes it uses: a new key starts with read scopes ticked, and a scope that changes data has to be chosen. You can only grant a scope your own role allows when you create the key. After that the key belongs to your workspace, not to you: it keeps its scopes if your role changes or you leave, until someone revokes it on Settings → API. A write scope needs its resource’s read scope: a write answers with the record it changed, so customers:write comes with customers:read — ticking Write ticks Read, and a call from a key holding the write without the read answers 403 insufficient_scope naming the read scope.
| Scope | Allows |
|---|---|
customers:read |
Reading customers, their custom fields, account teams, health, eCSAT and enrichments — see Customers and Enrichments |
customers:write |
Creating customers, changing their fields (which takes them over from your CRM’s sync) and their account teams — see Create a customer, Update a customer and Account team. Needs customers:read. |
customers:archive |
Archiving and unarchiving customers, with their contacts — see Archive and unarchive. Needs customers:read. |
enrichments:manage |
Creating, changing and deleting enrichments, and running them on customers — runs spend credits — see Enrichments. Needs customers:read. |
custom-fields:manage |
Creating, renaming and deleting customers’ custom fields, and adding options to select fields. Deleting a field erases every customer’s value for it. Granted only by people who can change workspace settings — see Define custom fields. Needs customers:read. |
contacts:read |
Reading the people at your customers — see Contacts |
contacts:write |
Marking people do-not-contact, so Sonora stops emailing them, and lifting the mark — see Mark someone do-not-contact. Needs contacts:read. |
issues:read |
Reading customer issues, their sources and quotes — see Issues and feature requests |
issues:write |
Resolving and reopening issues no support ticket backs — see Resolve or reopen an issue. Needs issues:read. |
feature-requests:read |
Reading customer feature requests, their sources and quotes — see Issues and feature requests |
meetings:read |
Reading customer meetings, their attendees and summaries — see Meetings |
notes:read |
Reading the notes your team shares on customers — see Notes |
notes:write |
Adding shared notes to customers, and changing or deleting the notes written through the API — see Write a note. Needs notes:read. |
workflows:trigger |
Starting API-triggered workflows — see Workflows |
workflows:read |
Reading workflows, their runs and the contact roles |
Requests and responses
Section titled “Requests and responses”- Bodies are JSON, sent with
Content-Type: application/json, up to 64 KB unless an endpoint says otherwise. - Field and parameter names are camelCase. Ids are UUIDs. Times are ISO 8601 in UTC (
2026-09-20T14:03:11.412Z); a time you send in a query needs an offset (2026-09-01T00:00:00Z). - A field with no value is
null, never left out. - A parameter or body field Sonora doesn’t know is refused with
400 invalid_requestrather than ignored, and so is a query parameter sent twice — a typo never silently widens a query. - Every response carries an
X-Request-Idheader. Send your own (up to 128 letters, digits,_or-) and Sonora echoes it back; otherwise it mints one. Quote it when you contact support.
Updating
Section titled “Updating”PATCH changes only what you send: a field you leave out is kept, and null clears one. A field set to the value it already holds is not a change. A call saves everything or nothing — if any field can’t be saved, it answers 422 invalid_fields with each problem in details.issues, and nothing changed.
Every change made with a key is recorded as that key’s, with the names of the fields it changed.
Conditional requests
Section titled “Conditional requests”A resource you can update comes with an ETag header — a fingerprint of the fields an update can change:
ETag: "q0SgAuHn2fW1rDk7p-3Ytl8xZ0cE9bMa"Send it back as If-Match on the update. If one of those fields changed in the meantime — someone edited it in Sonora, a sync wrote it, another integration patched it — nothing is written and the call answers 412 precondition_failed. Read the resource again, and decide what to write with the new value in front of you.
curl -X PATCH https://api.usesonora.com/v1/customers/c1d2… \ -H "Authorization: Bearer sonora_sk_…" \ -H "Content-Type: application/json" \ -H 'If-Match: "q0SgAuHn2fW1rDk7p-3Ytl8xZ0cE9bMa"' \ -d '{ "stage": "Renewal" }'The ETag only moves when a field you could change does — not on Sonora’s own bookkeeping, like a customer’s engagementsCount — so it doesn’t fail writes that conflict with nothing. Without If-Match the last write wins. If-Match: * matches any version.
Idempotency
Section titled “Idempotency”Networks fail, and a call that timed out may still have worked. Retrying a create could then make a second customer. To retry safely, send an Idempotency-Key header with any call that creates something — any unique string of up to 255 printable characters, such as a UUID or your own record’s id:
curl -X POST https://api.usesonora.com/v1/customers \ -H "Authorization: Bearer sonora_sk_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6f1c2e0a-4b7d-4d43-9a0e-2c5d8f1b7e44" \ -d '{ "domain": "acme-cloud.com" }'Send the same call again with the same key and Sonora doesn’t act twice. It answers with the first call’s status and body, plus an Idempotent-Replayed: true header.
- A key belongs to one request. Reusing it for a different call — another endpoint or path, a different body, or a different API key — is refused with
422 idempotency_key_mismatch. Use a new key for each new thing you create. - Only finished calls are replayed. If the first call is still running, a retry answers
409 idempotency_key_in_use; wait a moment and retry. A call that dies without finishing frees its key a minute later; one that is just slow keeps it until it finishes. - Failed calls don’t use up the key. A create and the answer a retry replays are saved together, so a call either created its record and will replay it, or created nothing. If it failed with a
4xx— a bad field, a conflict — correct the request and send it with the same key. After a5xx, retry with the same key as it is. (Running an enrichment queues work rather than creating a record, so it has its own rule — see Run an enrichment.) - Keys last 24 hours. After that a key is forgotten, and a call that reuses it is a new request.
Keys are per workspace, and the reference lists Idempotency-Key on each endpoint that takes it. Starting a workflow run has its own idempotency, kept with the run for 30 days and scoped to the workflow — see Workflows.
Paging
Section titled “Paging”List endpoints return a page at a time:
{ "data": [ … ], "nextCursor": "MjAyNi0wOS0yMFQxNDow…" }Pass nextCursor back unchanged as cursor for the next page; it is null on the last one. limit sets the page size, 1–100 (default 25). Where an endpoint offers include=totalCount, the response also carries totalCount, every match under the same filters — leave it out when you don’t need it, as counting costs more than a page.
Rate limits
Section titled “Rate limits”Requests are rate limited per key. Over the limit, a call answers 429 rate_limited with a Retry-After header: wait that many seconds, then retry. Spread bulk work out rather than bursting — and for a large read, page with a bigger limit rather than more calls.
Errors
Section titled “Errors”Every error has the same shape:
{ "error": { "code": "insufficient_scope", "message": "…", "details": { "required": "workflows:read" }, "requestId": "…" } }code is stable and safe to branch on; message is for people and may change. details carries what the code says it does. The reference lists each endpoint’s codes; these apply everywhere:
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_json, invalid_request |
The body isn’t JSON, or a field or parameter is wrong or unknown. details.issues lists each one. |
| 401 | unauthorized |
Missing, malformed, unknown, revoked or expired key. |
| 403 | insufficient_scope |
The key lacks the scope in details.required. |
| 404 | not_found |
No such endpoint. |
| 405 | method_not_allowed |
Wrong HTTP method; the Allow header lists the right ones. |
| 413 | payload_too_large |
The body is over the endpoint’s limit (details.maxBytes). |
| 429 | rate_limited |
Too many requests for this key; wait Retry-After seconds. |
| 500 | internal_error |
Something went wrong on our side. Safe to retry; quote requestId if it persists. |
Compatibility
Section titled “Compatibility”/v1 only changes in ways that don’t break a client written to these docs:
- Responses may gain fields. Ignore fields you don’t know.
- Enumerations may gain values — statuses, types, reasons and error
codes. Handle a value you don’t know: treat an unknown status as “not done yet”, and fall back on the HTTP status for an unknown errorcode. - New endpoints and new optional parameters may appear.
Anything else — removing or renaming a field, changing a type or a meaning — ships only in a new version (/v2), with /v1 kept running alongside it.
OpenAPI
Section titled “OpenAPI”The whole API is described by an OpenAPI 3.1 document at https://api.usesonora.com/v1/openapi.json — no key needed. It is generated from the same definitions the API enforces, so it is always current: load it into Postman or Insomnia, or generate a typed client from it.