Skip to content
KugelAudioDocs

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

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

JSON
{
  "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"
}
FieldMeaning
toThe number to call: international (+49..., 0049...) or German national (0176...). Germany, Austria and Switzerland; premium-rate numbers are refused
metadataWhat 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
numberIdWhich of your numbers calls, when the agent has several. Leave it out when it has one
maxAttempts1 (default) or 2: one more try two minutes after nobody answered or the line was busy
Idempotency-Key headerRequired: 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

Bash
curl https://api.agents.eu.kugelaudio.com/v1/agents/outbound-calls/CALL_ID \
  -H "Authorization: Bearer $KUGELAUDIO_API_KEY"
stateMeaning
queuedWaiting for its first dial, or for the retry
dialingThe phone is ringing
answeredThe callee picked up and is talking with the agent
completedThe call took place and ended
no_answer, busyNobody picked up, or the line was busy, on the last attempt
failedThe call could not be placed; lastDialStatus says why
canceledYou 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:

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

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

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

codeWhy
upgrade_requiredThe organization is on the Free plan
transfer_unsupported_sessionNot a phone call (a browser call or the dashboard preview)
transfer_unsupported_numberThe call's number is from your own Twilio or Telnyx account
transfer_in_progressAnother transfer of this call is still ringing
callback_destination_blockedThe 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 429 with callback_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

StatuscodeWhat to do
400callback_phone_invalidSend a full number, +49...
402upgrade_requiredMove to a paid plan
403callback_destination_blockedCall a number in Germany, Austria or Switzerland
409outbound_call_number_missingBuy a number and route it to the agent
409outbound_call_number_ambiguousName the calling number with numberId
409idempotency_key_reusedUse a new Idempotency-Key for a new call
429callback_number_limited, callback_endpoint_limitedTry again tomorrow

Next steps