Calls API
Use the Calls API to trigger an outbound call from one of your assistants.
Base URL
https://api.yourcalls.ai
This host serves the public API only. Apart from the /health and /ready checks, every path outside /public/ returns 404.
Authentication
Include your API key on every request:
Authorization: Bearer YOUR_API_KEY
You can also use:
x-api-key: YOUR_API_KEY
Your key determines which company the call belongs to and which assistants you can reach. See Authentication.
POST /public/v1/calls
Trigger an outbound call using an existing assistant.
curl --request POST "https://api.yourcalls.ai/public/v1/calls" \
--header "Authorization: Bearer YOUR_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"assistant_id": "12345678-1234-4234-9234-1234567890ab",
"target_phone_number": "+442079460958",
"variables": {
"first_name": "Alex"
},
"metadata": {
"crm_record": "12345"
}
}'
Fields
assistant_id(required): UUID of the assistant. It must belong to your company, be published, and have an outbound phone number linked.target_phone_number(required): the number to call, in E.164 format, for example+442079460958.variables(optional): an object of string values. Each key becomes a call variable the assistant reads as{{vars.<name>}}, so thefirst_nameabove is{{vars.first_name}}in a prompt, a tool, or a workflow. Names must be lower case with underscores. A camelCase key such asfirstNameis not a valid variable name at all, so the value arrives on the call and can never be read as a variable. Variables that describe the person are also saved to the contact, see Variables also fill the contact. See Variables for the naming rules.metadata(optional): an object of string values stored with the call and forwarded to your tools.use_contact_lookup(deprecated): accepted and ignored. Contacts are now matched on every outbound call, so there is nothing to switch on. The field is removed in the next release, so stop sending it.
The body is checked strictly. Any field not on this list is rejected with a 400 rather than ignored, so a typo such as target_phone tells you straight away instead of silently placing a call without the value you meant to send.
variables and metadata take string values only. Send numbers, booleans, and dates as strings.
There is no company_id field
The company is taken from your API key. Sending company_id in the body is rejected with a 400.
Reserved metadata keys
These keys are reserved for call attribution and are rejected with a 400 if they appear in metadata:
source, request_id, company_id, assistant_id, use_contact_lookup, contact_lookup_matched, campaign_id, contact_id
If you need to carry your own identifier, prefix it so it cannot collide, for example crm_contact_id rather than contact_id.
Variable merge order
The keys you send in variables land under vars.*, read as {{vars.<name>}}. Two sources write there, with later entries winning:
- request
variables - variables returned by a before-call tool, if the assistant has one (highest priority)
Step 2 is worth knowing before you debug a value that looks wrong. If your assistant runs a before-call tool and that tool returns a variable under a key you also sent, it replaces yours. That is deliberate, since the tool runs later and can see information you did not have at trigger time.
Details YourCalls already holds sit outside that set and nothing you send can overwrite them. The assistant is read as {{assistant.name}} and the rest of assistant.*, and your business as {{company.name}} and the rest of company.*.
The contact matched to the number is not merged into this set either. Contacts are recorded against the call for reporting and for your tools, and a saved contact's details do not overwrite a variable you sent. To use those details, read the contact directly: {{contact.first_name}}, {{contact.postcode}} and the rest of the contact.* names work in prompts, tools, and workflows. See Variables for every name and when it is available.
Variables also fill the contact
Every outbound call is matched to a contact by the number you dial, and a new contact is created when there is no match. The variables you send are used to fill that contact in, so {{contact.first_name}} works on the very first call to a new lead.
- A variable named after a contact detail is saved to it:
salutation,first_name,last_name,email,job_title,company_name,address_line1,address_line2,city,county,postcodeandcountry. Common spellings are recognised too, such asfirstName,surname,town,zipandcompany. Anameorfull_nameis split into first and last name. - A variable whose name matches one of your own contact fields is saved to that field and read as
{{contact.custom.<key>}}. A variable with no matching field is not saved to the contact. It is still there as{{vars.<name>}}. - On a contact you already hold, a detail is filled only where it is empty. A name or an address you have saved is never replaced by a variable. Your own contact fields take the latest value you send.
- The phone number and the do-not-contact flags are never set from variables.
- A value that does not fit, such as an email address that is not one, is left off the contact. The call still goes ahead and the value is still available under
vars.*.
Nothing changes in vars.*. Every key you send is still there under the name you gave it.
Success response (201)
All responses use the standard envelope:
{
"success": true,
"data": {
"request_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
"call_id": "ffffffff-1111-4222-8333-444444444444",
"status": "in_progress",
"from_number": "+447700900999"
},
"error": null,
"requestId": "req-abc123",
"apiVersion": 1
}
data.request_id: identifies the call record held in your account. This is the durable one. Store it, and quote it if you contact support about a specific call.data.call_id: identifies the live call on the calling service. Useful when correlating with events as the call happens, butrequest_idis the one to keep.data.status: alwaysin_progresson a successful trigger. It means the call has been accepted and dialling has started, not that it has been answered.data.from_number: the number the call is placed from.
A 201 means the call was accepted. It does not tell you how the call went.
The two request IDs
A response carries two fields whose names look similar and whose values are not:
data.request_ididentifies the call. It is the one to store against your own records.requestId, at the top level of the envelope, identifies the HTTP request that created it. Quote it when reporting a problem with a specific API call.
The envelope is camelCase and its contents are snake_case, so the two are easy to tell apart at a glance in a response body.
Error response
Failures use the same envelope with success: false:
{
"success": false,
"data": null,
"error": {
"code": "CONCURRENCY_FULL",
"message": "Concurrency is currently full, please retry shortly"
},
"requestId": "req-abc123",
"apiVersion": 1
}
See Errors for the full list of codes and what to do about each one.
Retrieving call results
There is no endpoint for reading calls back. Completed calls, including outcome, summary, transcript, and recording, are reviewed in the dashboard under Call History.
Region support
Outbound call triggers currently go to UK numbers (+44), not including Jersey, Guernsey or the Isle of Man. Other regions are available by arrangement: get in touch if you need one.
If the target number is outside the regions open to outbound calling, the API returns 422 REGION_NOT_SUPPORTED. The same refusal applies to numbers that can't be called in any region: UK premium-rate, personal and corporate numbers (those starting 070, 076, 084, 087, 09 or 055), and premium-rate, shared-cost, personal and pager numbers elsewhere.
A number in an open region must also be a real number for its country. One that is correctly formatted but cannot exist there, for example a mobile prefix that has never been issued, returns 422 INVALID_PHONE_NUMBER. Both checks run before the call is created, so neither refusal uses one of your concurrent calls.