Skip to content

Enrichments

An enrichment is a field Sonora’s AI fills in on each customer from your calls, emails and tickets, such as a churn reason, a champion’s strength, or whether the account has a renewal risk. Your workspace defines its own enrichments, and each one is a column on the customers page. The API reads the latest values on customers with ?include=enrichments, and describes the enrichments themselves at GET /enrichment-configs. Both need a key with the customers:read scope. With the enrichments:manage scope, a key can also create, change and delete enrichments, and run them — which spends credits. Every field is in the API reference.

The API acts for your whole workspace, not for one person, so it reads what everyone in the workspace can see:

  • An enrichment someone kept private to themselves is never listed, and its values never appear.
  • Enrichments Sonora runs for its own bookkeeping, which aren’t a column for anyone, are left out too.
  • An enrichment that has been switched off isn’t listed, and its old values don’t appear.
  • Health is not an enrichment here. It is the customer’s health field.

When someone makes a private enrichment public, it shows up in the API from then on, with all its values.

Terminal window
curl "https://api.usesonora.com/v1/enrichment-configs" \
-H "Authorization: Bearer sonora_sk_…"
{
"data": [
{
"id": "5e0b…",
"title": "Churn reason",
"description": "The main reason the account might not renew.",
"entityType": "customer",
"outputType": "select",
"options": ["Price", "Product gaps", "Support", "Champion left"],
"multiple": false,
"createdAt": "2026-06-02T15:20:11.000Z",
"updatedAt": "2026-09-18T10:04:37.000Z",
"links": { "self": "https://api.usesonora.com/v1/enrichment-configs/5e0b…" }
}
],
"nextCursor": null
}

Enrichments come oldest first, in pages like every other list (see Paging). GET /enrichment-configs/{configId} returns one. An id that isn’t one you can read gets 404 enrichment_config_not_found.

An enrichment’s id stays the same when someone edits its prompt or options. A customer’s values carry it as configId, so you can store values against it and look the enrichment up later. updatedAt moves on each edit. If you cache the list, refresh it when an updatedAt changes or a configId you don’t know turns up.

entityType is customer for every enrichment today. Other record kinds will join it as the API grows to read them.

?include=enrichments on GET /customers or GET /customers/{customerId} adds an enrichments list to each customer:

"enrichments": [
{
"configId": "5e0b…",
"title": "Churn reason",
"outputType": "select",
"value": "Price",
"reason": "Procurement asked twice for a discount before renewal.",
"computedAt": "2026-10-12T09:14:55.000Z"
},
{
"configId": "a71c…",
"title": "Champion strength",
"outputType": "score",
"value": null,
"reason": null,
"computedAt": "2026-10-12T09:15:02.000Z"
}
]

There is one entry per enrichment that has a value for that customer, in the order GET /enrichment-configs lists them. An enrichment with no value yet for the customer has no entry. computedAt is when the value was worked out, and reason is the AI’s explanation when it gave one.

value takes the shape outputType names:

outputType value
short_text, long_text A string.
number A number.
score A number from 1 to 10, where 10 is strongest.
boolean true or false.
date YYYY-MM-DD.
email, url A string.
select One of the enrichment’s options, or a list of them when multiple is true.

value is null, whatever the outputType, when there wasn’t enough evidence to answer, the case Sonora shows as “No result”.

A value was worked out under the enrichment’s settings at computedAt. If someone has since changed the options, an older value may name an option that is no longer in the list. Handle that case rather than reject the value. New outputType values may be added, so treat a type you don’t recognise as opaque.

To mirror enrichment values into a warehouse, page through GET /customers?include=enrichments&sort=updatedAt&includeArchived=true the way Customers describes, and upsert each value by customer id and configId. A new value doesn’t always change the customer’s updatedAt, so run a full pass now and then rather than relying on updatedAfter alone.

POST /enrichment-configs adds an enrichment to your customers. You describe it the way the enrichment editor in Sonora does: the question the AI answers for each customer, the shape of the answer, and what the AI may read to work it out.

Terminal window
curl -X POST https://api.usesonora.com/v1/enrichment-configs \
-H "Authorization: Bearer sonora_sk_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0b8e5c1a-6f2d-4c39-9a71-3e5d2f0c8b14" \
-d '{
"title": "Churn reason",
"description": "The main reason the account might not renew.",
"prompt": "What is the main reason this account might not renew?",
"outputType": "select",
"options": ["Price", "Product gaps", "Support", "Champion left"],
"dataSources": ["meetings", "notes", "issues"]
}'

It answers 201 with the enrichment, as GET /enrichment-configs/{configId} returns it, and its ETag.

  • prompt is the question, in plain language. Ask one thing, and say what a good answer looks like: the AI answers it once per customer, from that customer’s own records.

  • outputType is one of the types in the table above. A select needs options — two to ten different values — and takes multiple: true to let a value be several of them. score is available where your workspace scores health the newer way; elsewhere it’s 422 invalid_fields.

  • dataSources is what the AI may read: the customer’s meetings, emails, notes, issues, feature_requests, product_usage, ecsat and engagement, or the public web (web_search, if web search is on for your workspace: it’s off when your plan doesn’t include it, or when your workspace asked us to turn it off). Give it what the question needs. Each source you add is another place it may look, and web_search costs more to run (see Credits). An enrichment reads emails with the access of the person who made it, and one you create through the API has no owner, so it can’t read email: emails is 422 invalid_fields here, and on an update to an enrichment made through the API. An enrichment someone made in Sonora reads email with their access, so you can add emails to one of those.

    Your key must be able to read what the enrichment reads. Its answers come back on the customers, so a key may only point an enrichment at data it could read itself: meetings needs meetings:read, notes needs notes:read, issues needs issues:read, feature_requests needs feature-requests:read, and emails needs contacts:read. The rest are customer data customers:read already covers. A create, or an update that changes the prompt, answer type, options or sources, is 403 insufficient_scope (naming the scope in details.required) when the key lacks one, even for a source the enrichment already had. Renaming an enrichment or running it needs none of them. Some enrichments made in Sonora read sources no key scope covers, such as person lookup; what those ask can only change in Sonora (422).

    An enrichment sees the customer as Sonora holds it. Its prompt is given the whole customer record — every custom field, including ones the API’s customer view leaves out, such as fields an import marked hidden or sensitive — exactly as for an enrichment someone writes in Sonora. Only someone who can manage enrichments in Sonora can grant enrichments:manage, so a key holding it can’t reach anything they couldn’t: treat it as that person’s access to enrichments, and grant it as you would that permission.

An enrichment you create is shared with the whole workspace, like one made in Sonora and made public. It shows up as a column on the customers page straight away, empty until you run it. Creating it spends nothing.

Send an Idempotency-Key so a retry after a timeout doesn’t create a second enrichment — see Idempotency. Segments, schedules and model settings aren’t part of the API: set those in Sonora.

PATCH /enrichment-configs/{configId} changes the settings you send and keeps the rest. It takes the same fields as creating one, all optional, and description: null (or a blank one) clears the description.

Terminal window
curl -X PATCH https://api.usesonora.com/v1/enrichment-configs/5e0b… \
-H "Authorization: Bearer sonora_sk_…" \
-H "Content-Type: application/json" \
-H 'If-Match: "Jq3v0ZpT6cYb1nX8wR2kDa5mHs7uLe9f"' \
-d '{ "options": ["Price", "Product gaps", "Support", "Champion left", "Budget cut"] }'

An edit is a new version of the enrichment, just like an edit in Sonora. Its id stays the same, so values you’ve stored against it still match. Values already worked out keep the settings they were worked out under until the enrichment runs again, which is why an older value may name an option you’ve since removed. A field you send with the value it already has is not a change, and a call that changes nothing makes no new version. dataSources is a set, so the same sources in another order are no change either. Options you remove from a select lose the colors Sonora gave them.

Every edit moves the enrichment’s ETag. Send the one you last read as If-Match, and if someone has edited the enrichment since, nothing changes and the call answers 412 precondition_failed — see Conditional requests. Without If-Match, your change is made to the enrichment as it is when the call runs: settings you didn’t send keep their current values, including any someone saved after you read it. In the rare case that edits keep landing while the call runs, nothing changes and it answers 409 enrichment_config_busy; retry it.

Some enrichments aren’t written as a prompt, such as ones Sonora sets up for you. You can rename those and change their description; changing their prompt or answer is 422 invalid_fields. If an enrichment searches the web on a schedule more often than every two weeks, adding web_search to its sources is refused too, as it is in Sonora.

Terminal window
curl -X DELETE https://api.usesonora.com/v1/enrichment-configs/5e0b… \
-H "Authorization: Bearer sonora_sk_…"

It answers 204, and the enrichment is gone as if someone had deleted it in Sonora: its column leaves the customers page, its schedule stops, GET /enrichment-configs stops listing it, and its values stop appearing on customers.

If the enrichment fills a field in your Salesforce export mapping, deleting it would break that mapping, so the call answers 409 enrichment_config_mapped and nothing changes. Send ?confirm=true to delete it anyway; the mapping for it is removed too, in the same step, so a delete that fails leaves the mapping in place. Like an update, a delete that meets edits landing at the same moment answers 409 enrichment_config_busy and changes nothing; retry it.

POST /enrichment-configs/{configId}/runs queues the enrichment to be worked out for the customers you name, or for all of them.

Terminal window
curl -X POST https://api.usesonora.com/v1/enrichment-configs/5e0b…/runs \
-H "Authorization: Bearer sonora_sk_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4c2f9e7d-1a3b-4d5e-8f60-7a8b9c0d1e2f" \
-d '{ "customerIds": ["c1d2…", "e3f4…"] }'
{ "queued": 2 }

Send either customerIds, up to 100 Sonora ids, or "all": true for every customer that isn’t archived. A customer id that isn’t one of your live customers is 422 invalid_fields, and nothing is queued. By default every customer you name is worked out again. Add "onlyMissing": true to skip the ones that already have a value, so you pay only for the gaps. Sonora normally drops those customers before it prices and queues the run. For an enrichment it can’t check that way up front, such as one set up in Sonora to work out several values, queued counts every customer and the credit check prices them all, though each customer that has a value is still skipped when its turn comes.

The call answers 202 once the run is queued, and queued says how many customers it covers. That can be fewer than you named. An enrichment set up in Sonora to apply to some customers only skips the rest, and onlyMissing skips customers that have a value. Each value arrives on its customer as it’s worked out. Read the values with GET /customers?include=enrichments, and use computedAt to tell a fresh value from an old one. A value someone has corrected by hand in Sonora is kept, and the run doesn’t change it.

Runs started by the API wait behind the customers someone in Sonora is watching fill in on their screen, so a big API run never holds that up. They share a place in line with the rest of a large run someone starts in Sonora, in the order each was queued. On a large book, an "all": true call can take a minute or more to answer while it queues every customer.

Send an Idempotency-Key with every run. A retry then answers with the first call’s result instead of running, and charging, a second time, and a retry while the first call is still queuing answers 409 idempotency_key_in_use. Unlike a create, a run’s jobs aren’t saved together with its answer. If a call fails on Sonora’s side (a 5xx) or never answers, it may have queued the run, or part of it, anyway, and a retry with the same key once the key is freed, a minute later, queues it all again. Before you retry a run that ended that way, check computedAt on its values to see whether it went ahead.

Running an enrichment spends your workspace’s credits. It usually costs 1 credit per customer, or 4 when its dataSources include web_search. Some enrichments made in Sonora read sources the API doesn’t name, such as a person lookup, and cost more per customer. GET /enrichment-configs doesn’t show those sources, so a run of one of them may need more credits than you’d work out, and if your credits can’t cover it the call answers 402 as below. Creating, changing or deleting an enrichment costs nothing.

Sonora prices the whole run before it queues anything. If your workspace’s credits can’t cover it, nothing is queued and the call answers 402 credits_exhausted, with the credits the run needs in details.required and what’s left in details.remaining:

{
"error": {
"code": "credits_exhausted",
"message": "Your workspace doesn't have the credits for this run, so nothing was queued.",
"details": { "required": 400, "remaining": 120 },
"requestId": "…"
}
}

Run fewer customers, or use onlyMissing. If your contract allows overages, a run past your allowance goes ahead, as it would in Sonora. Before running "all": true on a large book, check what it will cost: GET /customers?include=totalCount&limit=1 gives the number of customers.