Skip to content

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.

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.

Every workspace uses the same base URL:

https://api.usesonora.com/v1

The key says which workspace a call is for, so there is nothing else to configure. Every request carries it as a Bearer token:

Terminal window
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.

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.

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
  • 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_request rather than ignored, and so is a query parameter sent twice — a typo never silently widens a query.
  • Every response carries an X-Request-Id header. Send your own (up to 128 letters, digits, _ or -) and Sonora echoes it back; otherwise it mints one. Quote it when you contact support.

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.

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.

Terminal window
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.

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:

Terminal window
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 a 5xx, 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.

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.

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.

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.

/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 error code.
  • 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.

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.