Guides
Outbound calls
Place a call with an agent, follow it, end it, and know what each ending means.
An outbound agent calls a number you give it, with values you give it, and you hear about every
step. A call needs the calls:write permission.
Place a call
#!/bin/sh
# Place a call. With a test key this calls a test number and rings nobody.
curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/calls" \
-H "Authorization: Bearer $AIGENTLY_API_KEY" \
-H "Idempotency-Key: lead-20931" \
--json '{
"agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c",
"to_number": "+12025550100",
"variables": {
"first_name": "Sara"
},
"metadata": {
"crm_lead_id": "L-20931"
},
"external_id": "lead-20931"
}'
| Field | What it is |
|---|---|
agent_id |
One of your outbound agents. It must be published. |
to_number |
International format: +, the country code, then the number. |
variables |
Values for the agent's fields — see Variables. |
metadata |
Your own data, up to 20 keys. The agent never sees it; you get it back on every read and every event. |
external_id |
Your own id for the conversation — a lead, an order. Find it again with GET /v1/conversations?external_id=. |
from_number or phone_number_id |
Which of the agent's numbers to call from. Leave both out and the agent's own caller ID is used. |
language |
One of the agent's language versions, like ar. |
call_window |
The hours the call may start in — see Calling hours. |
Idempotency-Key is required. It is your own id for this call, up to 120 characters — build it
from your data, like lead-8812-attempt-2. The same request with the same key returns the same
conversation, with Idempotent-Replayed: true, and never calls twice; the same key with a different
request is refused with idempotency_key_reused. See Safe retries.
The answer is 201 with the conversation, pending until the
phone is answered. Everything a call can be refused for is checked before anything is dialled.
Follow it
Either listen for webhooks — conversation.started when it is answered and
conversation.ended exactly once when it ends, answered or not — or read the conversation:
#!/bin/sh
# Read a conversation with its transcript
curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d?include=transcript" \
-H "Authorization: Bearer $AIGENTLY_API_KEY"
status is pending, then active once answered, and finally completed or failed. The
revision only goes up, so of two copies of a conversation the higher one is the newer.
How a call ends
Every way a call can end, what the conversation says about it, and which events you get:
| What happened | status |
disposition |
ended_by |
Events | Call again? |
|---|---|---|---|---|---|
| Answered, talked, and somebody hung up | completed |
answered |
caller or agent |
conversation.started, conversation.ended, conversation.analyzed, conversation.recording_ready |
No. |
| Ended with /end after it was answered | completed |
answered |
api |
conversation.started, conversation.ended, conversation.analyzed |
No. |
| An answering machine answered, and the agent left its message | completed |
voicemail_left |
agent |
conversation.started, conversation.ended |
Your choice. |
| An answering machine answered, and no message was left | failed |
voicemail |
null | conversation.started, conversation.ended |
Yes, later. |
| The line was busy | failed |
busy |
null | conversation.ended |
Yes, later. |
| It rang and nobody answered | failed |
no_answer |
null | conversation.ended |
Yes, later. |
| The person rejected the call | failed |
declined |
null | conversation.ended |
Usually not. |
| The carrier would not put the call through | failed |
unreachable |
failure |
conversation.ended |
Check the number first. |
| The call could not be placed at all | failed |
— | failure |
conversation.ended |
Yes, in a moment. |
| Cancelled with /end while it was ringing | failed |
no_answer |
api |
conversation.ended |
No — you cancelled it. |
| Marked failed when its report was late, then reported as a full conversation | completed |
answered |
caller or agent |
conversation.started, conversation.ended, conversation.updated, conversation.analyzed |
No — always keep the highest revision. |
conversation.analyzedarrives only for an agent that analyses its conversations, and a little after the end.conversation.recording_readyarrives only for a call that was recorded.- Events can arrive out of order. Keep the one with the highest
revision.
An answering machine is handled by the agent's own voicemail setting: it leaves its message, or
hangs up and the call ends voicemail.
End it yourself
#!/bin/sh
# End a call or a chat
curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/conversations/5f0c9a52-7d3e-4b1a-9c8f-6e2d4a0b3c1d/end" \
-H "Authorization: Bearer $AIGENTLY_API_KEY"
A call still ringing is cancelled; one in progress ends with the agent's own goodbye. Ending always
answers 200 with the conversation, so it is safe to send again.
Calling hours
Name the hours a call may start in, in the time zone of the person you are calling:
"call_window": {
"timezone": "America/New_York",
"start": "09:00",
"end": "17:00",
"days": ["mon", "tue", "wed", "thu", "fri"]
}
timezone is a name like Asia/Riyadh. The window includes start and ends just before end.
Leave days out for every day of the week. A call sent outside its window is refused with
outside_call_window, and detail says when the window opens next — so schedule it for then.
Schedule a call for later
#!/bin/sh
# Schedule a call inside calling hours. It is placed at the first moment the window is open at or after dial_at.
curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/scheduled-calls" \
-H "Authorization: Bearer $AIGENTLY_API_KEY" \
-H "Idempotency-Key: reminder-20931" \
--json '{
"agent_id": "8a1f3c2e-0b6d-4f5e-9a7c-2d4e6f8a0b1c",
"to_number": "+12025550100",
"dial_at": "2026-10-12T13:00:00Z",
"call_window": {
"timezone": "America/New_York",
"start": "09:00",
"end": "17:00",
"days": [
"mon",
"tue",
"wed",
"thu",
"fri"
]
},
"variables": {
"first_name": "Sara"
},
"external_id": "reminder-20931"
}'
The body is the one a call takes, plus dial_at: the earliest the call may be placed, at most 30
days ahead. Leave it out, or send a time already past, and the call is placed now. With a
call_window, it is placed at the first moment the window is open at or after dial_at, and the
dial_at in the answer is already moved there. Idempotency-Key is required, as it is for a call.
Everything a call can be refused for is checked when you schedule it, so a mistake is said at once, and checked again when it is placed, because an agent can be unpublished or a number listed as do-not-call in the meantime. If the window has closed by then, the call waits for its next opening. If every line is busy, or the organization has placed today's calls, it waits and tries again: a minute later for a busy line, an hour later for the day's limit.
A scheduled call is scheduled until its time comes, and then:
status |
What happened |
|---|---|
placed |
It became a conversation. conversation_id names it, and it sends the usual events. |
failed |
It was refused when its time came, and no call was made. error_code says why — see below. |
cancelled |
You called it off. |
error_code on a failed call is the code POST /v1/calls would have answered, like
agent_not_published or do_not_call, or one of two more: key_revoked when the key that
scheduled it no longer works, and lines_busy when it was put off 30 times because every line was
busy or the day's calls were used up.
List the calls still waiting:
#!/bin/sh
# List the calls still waiting
curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/scheduled-calls?status=scheduled" \
-H "Authorization: Bearer $AIGENTLY_API_KEY"
Cancel one before its time:
#!/bin/sh
# Cancel a scheduled call. Only before its time: once placed, it is a conversation, which you end instead.
curl -sS --fail-with-body -X DELETE "https://api.aigently.ai/v1/scheduled-calls/4b6d8f0a-2c4e-4a6c-9e8a-0c2e4a6c8e0b" \
-H "Authorization: Bearer $AIGENTLY_API_KEY"
Once its time has come it can no longer be cancelled. A call being placed right now answers
scheduled_call_placing, and a call already placed is a conversation, which you
end instead.
A test key's scheduled calls become test calls that ring nobody, and only test keys see them. A scheduled call is kept for 30 days after it is placed, cancelled or fails; the conversation it made is kept like any other.
Calls refused before they start
| Code | Why |
|---|---|
agent_not_outbound |
The agent answers calls; it does not place them. |
agent_not_published |
Publish the agent first. |
invalid_variables |
A value is missing or does not fit — errors names each. |
destination_not_allowed |
A number this deployment does not call. |
do_not_call |
The number is on your organization's do-not-call list. |
caller_id_unavailable |
The agent has no number it may call from. |
concurrency_limit_reached |
The organization already has as many calls going as it may. |
daily_limit_reached |
The organization has placed today's calls. |
test_number_required |
A test key calls only test numbers. |
outside_call_window |
The call's window is closed. detail says when it opens. |
feature_disabled |
This deployment has phone calls switched off. A test key's calls are not affected. |
Numbers that must not be called
Your organization keeps one do-not-call list, and no agent rings a number on it: a call through the
API, a campaign's contact, an appointment reminder, a transfer to a person or a test dial from the
console is refused before anything is dialled, with do_not_call. Keep it from your own systems — when somebody opts out in
your CRM, add them:
#!/bin/sh
# Add a number to the do-not-call list. With a live key: the list decides which real people are called.
curl -sS --fail-with-body -X POST "https://api.aigently.ai/v1/do-not-call" \
-H "Authorization: Bearer $AIGENTLY_API_KEY" \
--json '{
"number": "+12025550188",
"reason": "Asked not to be called again"
}'
This needs do_not_call:write and a live key: the list decides which real people are called, so a
test key may read it but not change it. The list belongs to the whole organization, so reading or
changing it also takes a key that reaches every project. Numbers are matched by their digits, so +1 (202) 555-0100
and 12025550100 are one entry, and adding a listed number again answers 200 with the entry —
taking the new reason, if you sent one.
A number listed without its country code — 0798 798 906, (202) 555-0100 — also stops a call
that dials it with one, and the other way round: the national forms are worked out from the number
and from the line calling it. That can now and then refuse a number in another country that ends the
same way. The refusal's detail names the entry, so you can check it.
Ask whether a number is listed with do_not_call:read:
#!/bin/sh
# Ask whether a number may be called. An empty list means it is not on it.
curl -sS --fail-with-body -X GET "https://api.aigently.ai/v1/do-not-call?number=+12025550188" \
-H "Authorization: Bearer $AIGENTLY_API_KEY"
Taking a number off lets it be called again, and is recorded in your organization's audit log under the key:
#!/bin/sh
# Take a number off the do-not-call list. With a live key. Recorded in the audit log under it.
curl -sS --fail-with-body -X DELETE "https://api.aigently.ai/v1/do-not-call/+12025550188" \
-H "Authorization: Bearer $AIGENTLY_API_KEY"
A test key's own calls never ring anybody, so the list does not apply to them.