Outbound calls
Have your hosted agent call a phone number from your own system, with what you already know about the call, and let the agent forward a call to a person.
On this page
Your backend can ask your agent to call someone: a lead who just filled in a form, a customer whose order is ready. You send one request with the number and anything the agent should know; the agent calls from your KugelAudio number and starts the conversation. During any phone call, the agent can also forward the caller to another number.
What you need
- An agent that is live, with a number bought through KugelAudio routed to it. The call comes from that number, and calling it back reaches the same agent. Numbers from your own Twilio or Telnyx account cannot place calls yet.
- An API key of your organization, from API keys in the dashboard. A key acts for the whole organization: it can place calls from any of its agents and read or cancel any of its calls, so keep it on your server.
- The agent's API ID: open the agent in the dashboard, go to Settings, and copy API ID.
Place a call
curl -X POST https://api.agents.eu.kugelaudio.com/v1/agents/projects/AGENT_API_ID/outbound-calls \
-H "Authorization: Bearer $KUGELAUDIO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: lead-L-1042" \
-d '{
"to": "+49 176 12345678",
"metadata": { "lead": "L-1042", "name": "Erika", "topic": "Solar offer" },
"maxAttempts": 2
}'The answer is 202 with the call:
{
"callId": "6f1c2b9e-1d4a-4c3e-9a51-2f0d7b8e4a10",
"source": "api",
"state": "queued",
"to": "+4917612345678",
"from": "+15752600676",
"attempts": 0,
"maxAttempts": 2,
"createdAt": "2026-10-25T14:00:00Z",
"updatedAt": "2026-10-25T14:00:00Z"
}| Field | Meaning |
|---|---|
to | The number to call: international (+49..., 0049...) or German national (0176...). Germany, Austria and Switzerland; premium-rate numbers are refused |
metadata | What the agent should know. It reads each key as client.<key> from its first sentence, like the web widget's context: at most 16 keys, values text, numbers or booleans, 8,000 bytes in all |
numberId | Which of your numbers calls, when the agent has several. Leave it out when it has one |
maxAttempts | 1 (default) or 2: one more try two minutes after nobody answered or the line was busy |
Idempotency-Key header | Required: a string you choose per call. Sending the same key with the same body again answers the same call instead of calling twice; the same key with a different body answers 409 idempotency_key_reused |
Follow the call
curl https://api.agents.eu.kugelaudio.com/v1/agents/outbound-calls/CALL_ID \
-H "Authorization: Bearer $KUGELAUDIO_API_KEY"state | Meaning |
|---|---|
queued | Waiting for its first dial, or for the retry |
dialing | The phone is ringing |
answered | The callee picked up and is talking with the agent |
completed | The call took place and ended |
no_answer, busy | Nobody picked up, or the line was busy, on the last attempt |
failed | The call could not be placed; lastDialStatus says why |
canceled | You canceled it before it was dialed |
sessionId names the conversation once the call started: its transcript and recording are
in Calls like any other call. Cancel a call that is still queued:
curl -X POST https://api.agents.eu.kugelaudio.com/v1/agents/outbound-calls/CALL_ID/cancel \
-H "Authorization: Bearer $KUGELAUDIO_API_KEY" \
-H "Idempotency-Key: cancel-CALL_ID"Greet like a caller
The agent reads call.direction: outbound when it placed the call. Open with who is
calling and why, and say that an AI speaks:
import { Text, useAgentInfo, useClient } from "curb";
export function OutboundOpening() {
const direction = useAgentInfo((info) => info["call.direction"]);
const { name, topic } = useClient();
if (direction !== "outbound") return null;
return (
<Text>
You are calling {name ?? "the customer"} about {topic ?? "their request"}. Open with:
"Hallo, hier ist die digitale Assistenz von Example." Then explain why you call.
</Text>
);
}Forward a call to a person
The agent can connect the caller to another number during any phone call, inbound or outbound. Give the model a tool, or call it from your own code:
import { Instruction, TransferCallTool, useTransferCall } from "curb";
export function Handover() {
return (
<>
<Instruction>When the caller asks for a person, call transfer_call.</Instruction>
<TransferCallTool to="+493012345678" ringTimeoutSeconds={25} />
</>
);
}
// Or in your own tool: const transfer = useTransferCall();
// const result = await transfer({ to: "+493098765432" });The number rings from your number. When someone answers, the caller and that person talk,
and the agent goes quiet; if the person hangs up while the caller is still there, the agent
picks up again. When nobody answers or the line is busy, the agent stays on the call and
learns the outcome (no_answer, busy, failed), so it can tell the caller. A refused
transfer comes back as outcome: "refused" with a code:
code | Why |
|---|---|
upgrade_required | The organization is on the Free plan |
transfer_unsupported_session | Not a phone call (a browser call or the dashboard preview) |
transfer_unsupported_number | The call's number is from your own Twilio or Telnyx account |
transfer_in_progress | Another transfer of this call is still ringing |
callback_destination_blocked | The number is outside Germany, Austria and Switzerland, or premium-rate |
The whole call, the forwarded part included, counts against your plan's agent minutes.
Limits
- One number gets at most three calls a day from your organization (and ten from all
KugelAudio customers together); more answer
429withcallback_number_limited. - At most 500 calls a day per number of yours (
callback_endpoint_limited). - Calls run within your plan's concurrent calls and are held against your balance like any call.
- You are responsible for having the callee's consent to be called.
Errors
| Status | code | What to do |
|---|---|---|
400 | callback_phone_invalid | Send a full number, +49... |
402 | upgrade_required | Move to a paid plan |
403 | callback_destination_blocked | Call a number in Germany, Austria or Switzerland |
409 | outbound_call_number_missing | Buy a number and route it to the agent |
409 | outbound_call_number_ambiguous | Name the calling number with numberId |
409 | idempotency_key_reused | Use a new Idempotency-Key for a new call |
429 | callback_number_limited, callback_endpoint_limited | Try again tomorrow |
Next steps
- Callback requests let website visitors ask for the call themselves.
- Phone numbers to buy the number your agent calls from.