All pages

Guides

Outbound calls

Place a call with an agent, follow it, end it, and know what each ending means.

View as Markdown

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

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:

Read a conversation with its transcript
#!/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.analyzed arrives only for an agent that analyses its conversations, and a little after the end.
  • conversation.recording_ready arrives 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

End a call or a chat
#!/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

Schedule a call inside calling hours
#!/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:

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:

Cancel a scheduled call
#!/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:

Add a number to the do-not-call list
#!/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:

Ask whether a number may be called
#!/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:

Take a number off the do-not-call list
#!/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.