Calls
Transfer reasons
The rules that decide when an assistant hands a call to a person, and which number each kind of request goes to.
A transfer reason is a single rule: this situation goes to that number. An assistant carries as many as you give it (billing to one line, complaints to another, "just let me talk to someone" to the front desk), and when the conversation matches one, the assistant says the line you wrote and hands the call over.
Transfers covers how a hand-off is performed: cold, warm, or warm-return. This page covers when it happens and where it goes. The two compose (a reason picks the destination, the mode governs the hand-off), but they are configured in different places: the mode is a setting on the assistant, and none of the fields below set one. The generated field-by-field reference lives at Transfer Reasons API.
| Method | Path | Purpose |
|---|---|---|
| GET | /transfer-reasons | List transfer reasons (optionally by assistant) |
| POST | /transfer-reasons | Create a transfer reason |
| GET | /transfer-reasons/{id} | Get one transfer reason |
| PUT | /transfer-reasons/{id} | Update a transfer reason |
| PATCH | /transfer-reasons/{id} | Partially update a transfer reason |
| DELETE | /transfer-reasons/{id} | Delete a transfer reason |
The shape of a rule#
Every transfer reason answers three questions: what triggers it, what the caller hears, and where the call goes.
| Field | Type | Description |
|---|---|---|
name | string | A friendly name for this transfer reason |
reason | string | The trigger that causes the assistant to transfer the call, e.g. "user wants to talk to a human" |
message | string | What the assistant says before transferring the call |
assistantId | string | The assistant this rule applies to |
phoneNumber | string | Phone number to forward the call to |
extension | string | Extension to dial automatically once the call connects. Leave blank to keep current behavior. |
isActive | boolean | Whether the transfer reason is active |
waitForResponse | boolean | Whether the assistant should wait for the caller's response before transferring. Defaults to true. |
properties | object | Values substituted into {{key}} placeholders in this rule's reason and message |
phoneNumber is E.164 like every number in the API: +15555550123, no spaces or punctuation. See API conventions.
Write reason as the caller's situation rather than as an instruction to yourself: "the caller asks about a charge they do not recognise" is a trigger, "billing stuff" is not.
A stored record carries the same fields plus id, createdAt and updatedAt. Timestamps are ISO 8601 in UTC, again per convention.
The destination has to be able to take the call
Make sure the number is reachable and properly configured for handling forwarded calls. A rule pointing at a line nobody can answer sends callers somewhere worse than the assistant they started with.
What the create body requires
The spec marks nothing required, but the server does:
| Required | Optional |
|---|---|
assistantId, reason, message, phoneNumber, properties | name, extension, isActive, waitForResponse |
properties is an object with arbitrary keys. Send {} if you have nothing to put in it.
Unknown fields are rejected rather than ignored, so a misspelled key fails the request.
isActive defaults to true on create.
Runtime behavior
- Omit
extensionin a PATCH to leave it unchanged. Send""to clear it. A non-empty extension is appended to the destination after a comma when the transfer is dialled. - With
waitForResponse: true(the default), the assistant saysmessage, waits, and transfers only after an affirmative response. Withfalse, it transfers immediately without asking for confirmation. propertiesreplace matching{{key}}placeholders in both the trigger and spoken message before the assistant receives them.- There is no priority field or guaranteed winner when two reasons overlap. Write mutually exclusive
reasontext; do not rely on list order to resolve ambiguity.
Create a transfer reason#
POST /transfer-reasons
Reach for a new reason when a caller wants a person for a specific case (an escalation, a special request) or when you want the assistant to have somewhere to send queries it cannot handle itself.
curl -X POST https://core-api.heysadie.ai/transfer-reasons \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"assistantId": "0195f1e2-0000-0000-0000-000000000000",
"name": "Billing escalation",
"reason": "The caller asks to speak to a person about an invoice, a refund, or a charge they do not recognise.",
"message": "Let me put you through to our billing team.",
"phoneNumber": "+15555550123",
"extension": "204",
"isActive": true,
"waitForResponse": true
}'A successful create returns 200 with the new record under data, in the standard envelope:
{
"success": true,
"data": {
"id": "0195f1e2-2222-0000-0000-000000000000",
"name": "Billing escalation",
"reason": "The caller asks to speak to a person about an invoice, a refund, or a charge they do not recognise.",
"message": "Let me put you through to our billing team.",
"assistantId": "0195f1e2-0000-0000-0000-000000000000",
"phoneNumber": "+15555550123",
"extension": "204",
"isActive": true,
"waitForResponse": true,
"createdAt": "2026-07-30T12:00:00.000Z",
"updatedAt": "2026-07-30T12:00:00.000Z"
},
"metadata": {
"timestamp": "2026-07-30T12:00:00.000Z",
"version": "1.0",
"path": "/transfer-reasons"
}
}properties is present on every record too. It is left out of the samples on this page because the spec does not define its shape. See the callout above.
Keep the returned id. It is what the end-of-call report sends back in transfer_reason_id when this rule fires. That webhook names its fields in snake_case, unlike the camelCase of the REST API.
List transfer reasons#
GET /transfer-reasons
| Query param | Type | Required | Description |
|---|---|---|---|
assistantId | string | No | Only reasons belonging to this assistant. |
limit | number | No | Page size. Default 50, maximum 500. |
offset | number | No | Items to skip. Default 0. |
curl "https://core-api.heysadie.ai/transfer-reasons?assistantId=0195f1e2-0000-0000-0000-000000000000" \
-H "Authorization: ApiKey YOUR_API_KEY"data.transferReasons holds the page, data.total the size of the whole collection. Advance offset until you have seen total items.
{
"success": true,
"data": {
"transferReasons": [
{
"id": "0195f1e2-2222-0000-0000-000000000000",
"name": "Billing escalation",
"reason": "The caller asks to speak to a person about an invoice, a refund, or a charge they do not recognise.",
"message": "Let me put you through to our billing team.",
"assistantId": "0195f1e2-0000-0000-0000-000000000000",
"phoneNumber": "+15555550123",
"extension": "204",
"isActive": true,
"waitForResponse": true,
"createdAt": "2026-07-30T12:00:00.000Z",
"updatedAt": "2026-07-30T12:00:00.000Z"
}
],
"total": 3
},
"metadata": {
"timestamp": "2026-07-30T12:00:00.000Z",
"version": "1.0",
"path": "/transfer-reasons"
}
}Filtering by assistantId is the call worth building a habit around: it answers "what can this assistant currently escalate, and to where?" in one request.
Get one transfer reason#
GET /transfer-reasons/{id}
| Path param | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The transfer reason to fetch. |
curl https://core-api.heysadie.ai/transfer-reasons/0195f1e2-2222-0000-0000-000000000000 \
-H "Authorization: ApiKey YOUR_API_KEY"The response is 200 with a single record under data: the same object shown for create, not wrapped in an array. An id that does not exist returns 404; see Errors.
Update a transfer reason#
The spec gives both verbs an identical body, the same fields as create: name, reason, message, assistantId, phoneNumber, extension, isActive, waitForResponse, properties. Both return 200 with the updated record under data, in the same shape as create. What differs is how each treats the fields you leave out.
PATCH /transfer-reasons/{id}
A partial update: only the fields you include in the request are changed, and everything else is left alone. It is the right verb for a quick edit: a new number, a reworded message, taking a rule out of service.
curl -X PATCH https://core-api.heysadie.ai/transfer-reasons/0195f1e2-2222-0000-0000-000000000000 \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "phoneNumber": "+15555550124", "isActive": false }'isActive is how you retire a rule without losing it: set it to false and the record (its wording, its destination, its history in your own logs) stays exactly where it is.
PUT /transfer-reasons/{id}
Same body, same response. What it does with an omitted field is the open question:
PUT replaces, PATCH merges
The two verbs validate against different schemas, so the difference is real and it matters:
PUTrequires a complete record. It is validated with the same schema as create, soassistantId,reason,message,phoneNumberandpropertiesmust all be present. Omitting one is rejected with400rather than silently leaving it unchanged.PATCHtakes only what you want to change, plusassistantId.
Both reject unknown fields outright rather than ignoring them: a typo in a field name
fails the request. Reach for PATCH for an ordinary edit; reach for PUT when you are
writing the whole record from your own source of truth and want the API to reject a
partial one.
Delete a transfer reason#
DELETE /transfer-reasons/{id}
curl -X DELETE https://core-api.heysadie.ai/transfer-reasons/0195f1e2-2222-0000-0000-000000000000 \
-H "Authorization: ApiKey YOUR_API_KEY"Delete returns 200 with the deleted record under data (the full object, not just its id), so you can log exactly what was removed:
{
"success": true,
"data": {
"id": "0195f1e2-2222-0000-0000-000000000000",
"name": "Billing escalation",
"reason": "The caller asks to speak to a person about an invoice, a refund, or a charge they do not recognise.",
"message": "Let me put you through to our billing team.",
"assistantId": "0195f1e2-0000-0000-0000-000000000000",
"phoneNumber": "+15555550124",
"extension": "204",
"isActive": false,
"waitForResponse": true,
"createdAt": "2026-07-30T12:00:00.000Z",
"updatedAt": "2026-07-30T12:05:00.000Z"
},
"metadata": {
"timestamp": "2026-07-30T12:06:00.000Z",
"version": "1.0",
"path": "/transfer-reasons/0195f1e2-2222-0000-0000-000000000000"
}
}If you might want the rule back later, PATCH it to "isActive": false instead of deleting it.
Errors#
Every endpoint on this page uses the standard error envelope. These are the statuses the spec documents for the transfer-reason routes; the API-wide statuses apply too, including throttling (see Rate limits).
| Status | When |
|---|---|
400 | The body or query failed validation. error.details is an array of { field, message } objects, one per offending field. |
401 | Missing, malformed, or invalid Authorization header. See Authentication. |
403 | The key is valid but not allowed to act on this resource. |
404 | No transfer reason with that id. |
500 | Something went wrong on OnCore's side. |
If a create or update comes back 400, check the destination number first: it has to be E.164, like +15555550123 rather than (555) 555-0123.
Next steps#
- Transfers: the three hand-off modes; a reason picks the destination, the mode decides how the hand-off sounds.
- Dynamic transfer: resolve the destination from your own directory at call time instead of maintaining a fixed list of reasons.
- End-of-call report:
transfer_reason_idtells you which rule fired,supervisor_answeredwhether anyone picked up. - IVR router mode: let a short router transfer on these same reasons before the assistant takes the call.
- Transfer Reasons API: the generated reference, regenerated from the spec on every build.