Skip to content

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.

MethodPathPurpose
GET/transfer-reasonsList transfer reasons (optionally by assistant)
POST/transfer-reasonsCreate 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.

FieldTypeDescription
namestringA friendly name for this transfer reason
reasonstringThe trigger that causes the assistant to transfer the call, e.g. "user wants to talk to a human"
messagestringWhat the assistant says before transferring the call
assistantIdstringThe assistant this rule applies to
phoneNumberstringPhone number to forward the call to
extensionstringExtension to dial automatically once the call connects. Leave blank to keep current behavior.
isActivebooleanWhether the transfer reason is active
waitForResponsebooleanWhether the assistant should wait for the caller's response before transferring. Defaults to true.
propertiesobjectValues 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:

RequiredOptional
assistantId, reason, message, phoneNumber, propertiesname, 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 extension in 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 says message, waits, and transfers only after an affirmative response. With false, it transfers immediately without asking for confirmation.
  • properties replace 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 reason text; 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.

bash
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:

200 response
{
  "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 paramTypeRequiredDescription
assistantIdstringNoOnly reasons belonging to this assistant.
limitnumberNoPage size. Default 50, maximum 500.
offsetnumberNoItems to skip. Default 0.
bash
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.

200 response
{
  "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 paramTypeRequiredDescription
idstringYesThe transfer reason to fetch.
bash
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.

bash
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:

  • PUT requires a complete record. It is validated with the same schema as create, so assistantId, reason, message, phoneNumber and properties must all be present. Omitting one is rejected with 400 rather than silently leaving it unchanged.
  • PATCH takes only what you want to change, plus assistantId.

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}

bash
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:

200 response
{
  "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).

StatusWhen
400The body or query failed validation. error.details is an array of { field, message } objects, one per offending field.
401Missing, malformed, or invalid Authorization header. See Authentication.
403The key is valid but not allowed to act on this resource.
404No transfer reason with that id.
500Something 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_id tells you which rule fired, supervisor_answered whether 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.