Receiving calls
A number your workspace holds can deliver an incoming call to a SIP trunk, forward it to a verified number, run a published sequence, or reject it. A number has one route at a time: its own, or your workspace's default route when it has none.
The workspace default route starts as reject, so a number nobody has configured turns callers away rather than having no answer at all. Change the default to give every such number the same answer. See Set a default route.
To handle incoming calls in a native Bird flow, publish a sequence and bind the number to its call entry. The selected entry must accept empty data.
Prerequisites
Before you point a number at an answer:
- A number that can receive calls. Open Voice > Numbers and check the Directions column for an inbound mark. A number you registered as a caller ID from another carrier does not receive calls here: that carrier routes the calls made to it, so it carries no answer.
- For delivery to a trunk: a SIP trunk with inbound calling on and at least one delivery gateway.
- For a forward: a verified caller ID to forward to.
- For a sequence: an active, published sequence in the same workspace, with a call entry that accepts empty entry data. Under Inbound routing, select Run a sequence, choose the sequence and call entry, and save. The sequence builder guide explains publication and incoming draft testers.
- To change the setting over the API or CLI: an API key holding the
voice_managementscope at write level. That scope covers voice configuration; thevoicescope covers call traffic and statistics, so reading the call log needs the other one.
Deliver calls to a SIP trunk
Delivery dials your own phone system at addresses you declare on the trunk. Turn the direction on first, because a number can only be pointed at a trunk that already accepts inbound calls.
- Open Voice > SIP Trunks, open the trunk, and under Inbound calling select Enable inbound.
- Add at least one gateway under the same section. A trunk with no gateway refuses every incoming call to the numbers it answers.
- Open Voice > Numbers, open the number, and under Inbound routing choose Deliver to a SIP trunk.
- Pick the trunk and select Save. Only trunks with inbound calling on appear in the list.
The Used for column on the Numbers list then shows the number as delivered to that trunk, and the trunk's own page lists the numbers it answers.
Over the API, update the trunk with inbound_enabled: true, add a gateway, then point the number's voice record at the trunk. The voice record has an ID starting with vnu_, which differs from the nda_ ID that /v1/numbers returns for the same number. Passing the nda_ ID to a voice number operation is refused with 422. To find the voice record, search your voice numbers for the number's digits:
for await (const number of bird.voice.numbers.list({ search: "31201234567" })) {
console.log(number.id, number.phone_number);
}for number, err := range client.Voice.Numbers.List(context.Background(), bird.VoiceNumbersListParams{
Search: "31201234567",
}) {
if err != nil {
log.Fatal(err)
}
fmt.Println(number.Id, number.PhoneNumber)
}foreach ($bird->voice->numbers->list(['search' => '31201234567']) as $number) {
echo $number->getId(), ' ', $number->getPhoneNumber(), "\n";
}bird voice numbers list --search 31201234567curl -X GET "https://{region}.platform.bird.com/v1/voice/numbers" \
-H "Authorization: Bearer $TOKEN" \
--url-query "search=31201234567"Each result carries its id, its phone_number, and the current inbound_configuration.route. Send the trunk route to update the voice number with that id:
const number = await bird.voice.numbers.update("NUMBER_ID", {
inbound_configuration: {
route: { type: "trunk", trunk_id: "spt_01krdgeqcxet5s7t44vh8rt9mg" },
},
});
console.log(number.id, number.inbound_configuration?.route?.type);var route bird.VoiceCallRouteWritable
if err := route.FromVoiceCallRouteTrunk(bird.VoiceCallRouteTrunk{
TrunkId: "spt_01krdgeqcxet5s7t44vh8rt9mg",
}); err != nil {
log.Fatal(err)
}
number, err := client.Voice.Numbers.Update(context.Background(), "NUMBER_ID", bird.VoiceNumbersUpdateParams{
InboundConfiguration: &bird.VoiceInboundConfigurationPut{Route: route},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(number.Id)$number = $bird->voice->numbers->update(
'NUMBER_ID',
(new VoiceNumberUpdate())->setInboundConfiguration(
(new VoiceInboundConfigurationPut())->setRoute([
'type' => 'trunk',
'trunk_id' => 'spt_01krdgeqcxet5s7t44vh8rt9mg',
]),
),
);
echo $number->getId(), "\n";bird voice numbers update <number-id> --body-file - <<'JSON'
{
"inbound_configuration": {
"route": {
"type": "trunk",
"trunk_id": "spt_01krdgeqcxet5s7t44vh8rt9mg"
}
}
}
JSONcurl -X PATCH "https://{region}.platform.bird.com/v1/voice/numbers/{number_id}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"inbound_configuration": {
"route": {
"type": "trunk",
"trunk_id": "spt_01krdgeqcxet5s7t44vh8rt9mg"
}
}
}'A trunk whose inbound calling is off is refused with 412 and E21052. The route replaces whatever the number had before. Sending {"type": "reject"} as the route turns callers away whatever the default is, and sending null returns the number to the workspace default route.
What a gateway needs
A gateway is one address a call is delivered to, and how that peer wants the call's two numbers spelled:
| Setting | What it is |
|---|---|
| SIP URI | The host of your phone system, with an optional port, as in sip:pbx.example.com:5060. Give the host only: a URI carrying a user part is refused |
| Priority | The order gateways are tried in, lowest first |
| Destination format | How the dialed number is spelled to this peer. Defaults to E.164 |
| Origination format | How the calling number is spelled to this peer, in the delivered call's P-Asserted-Identity header. Defaults to E.164 |
Gateways sharing a priority take an equal share of calls, and any of them may be tried first on a given call. To fail delivery over to a second address, give that gateway a larger priority number: it is tried when the first does not answer.
Both number formats are templates over one placeholder, {number}, which stands for the number without its leading +. The destination format is placed before the SIP URI host, so 1234#{number} delivers a call to +31201234567 as sip:1234#31201234567@pbx.example.com:5060. The default for both is +{number}, which is E.164. A format containing no {number} at all sends every number the trunk answers to one fixed address. A peer that expects numbers without the + takes {number} on its own as the format.
Over the API, add a gateway to the trunk with these settings. Turn on the trunk's inbound calling first: creating a gateway on a trunk without it is refused with 412 and E21052.
const gateway = await bird.voice.trunks.gateways.create("TRUNK_ID", {
sip_uri: "sip:pbx.example.com:5060",
priority: 0,
destination_format: "1234#{number}",
});
console.log(gateway.id, gateway.priority);gateway = client.voice.trunks.gateways.create(
"TRUNK_ID",
sip_uri="sip:pbx.example.com:5060",
priority=0,
destination_format="1234#{number}",
)
print(gateway.id, gateway.priority)gateway, err := client.Voice.Trunks.Gateways.Create(context.Background(), "TRUNK_ID", bird.VoiceTrunksGatewaysCreateParams{
SipURI: "sip:pbx.example.com:5060",
Priority: 0,
DestinationFormat: bird.Ptr("1234#{number}"),
})
if err != nil {
log.Fatal(err)
}
fmt.Println(gateway.Id, gateway.Priority)$gateway = $bird->voice->trunks->gateways->create(
'TRUNK_ID',
(new VoiceTrunkGatewayCreate())
->setSipUri('sip:pbx.example.com:5060')
->setPriority(0)
->setDestinationFormat('1234#{number}'),
);
echo $gateway->getId(), ' ', $gateway->getPriority(), "\n";bird voice trunks gateways create <trunk-id> \
--destination-format '1234#{number}' \
--priority 0 \
--sip-uri sip:pbx.example.com:5060curl -X POST "https://{region}.platform.bird.com/v1/voice/trunks/{trunk_id}/gateways" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sip_uri": "sip:pbx.example.com:5060",
"priority": 0,
"destination_format": "1234#{number}"
}'Update a gateway to change its priority or formats later.
Warning: turning inbound calling off on a trunk, or deleting the trunk, returns every number pointing at it to the workspace default route. A workspace default route that names the trunk goes back to reject. Turning inbound back on does not restore either, so each has to be pointed at a trunk again.
Set a default route
The workspace default route answers calls for every number without a route of its own. It starts as reject. A number with its own route keeps it when the default changes.
- Open Voice > Numbers.
- Next to Calls to numbers without their own route, select Change, choose the answer, and save.
The change applies from the next call each of those numbers receives. Over the API, update the voice settings:
curl -X PATCH "https://{region}.platform.bird.com/v1/voice/settings" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"inbound_configuration": {
"route": {
"type": "trunk",
"trunk_id": "spt_01krdgeqcxet5s7t44vh8rt9mg"
}
}
}'The default route is checked the same way as a number's route. To return a number to the default, set its route to null, or choose Use the workspace default on the number.
Forward calls to another number
A forward answers the incoming call and places a second call to a number you have verified, then connects the two.
- Open Voice > Numbers, open the number, and under Inbound routing choose Forward to another number.
- Pick the number to forward to. The list holds your verified caller IDs, because a forward may only target a number you have proven you control.
- Choose which number the forwarded call shows as the caller, then select Save.
Over the API, search your voice numbers for the number's digits to read its vnu_ ID, then send a forward route with forward_to and forward_as:
const number = await bird.voice.numbers.update("NUMBER_ID", {
inbound_configuration: {
route: { type: "forward", forward_to: "+14155551234", forward_as: "dialed_number" },
},
});
console.log(number.id, number.inbound_configuration?.route?.type);var route bird.VoiceCallRouteWritable
if err := route.FromVoiceCallRouteForward(bird.VoiceCallRouteForward{
ForwardTo: "+14155551234",
ForwardAs: "dialed_number",
}); err != nil {
log.Fatal(err)
}
number, err := client.Voice.Numbers.Update(context.Background(), "NUMBER_ID", bird.VoiceNumbersUpdateParams{
InboundConfiguration: &bird.VoiceInboundConfigurationPut{Route: route},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(number.Id)$number = $bird->voice->numbers->update(
'NUMBER_ID',
(new VoiceNumberUpdate())->setInboundConfiguration(
(new VoiceInboundConfigurationPut())->setRoute([
'type' => 'forward',
'forward_to' => '+14155551234',
'forward_as' => 'dialed_number',
]),
),
);
echo $number->getId(), "\n";bird voice numbers update <number-id> --route forward --forward-to +14155551234 --forward-as dialed_numbercurl -X PATCH "https://{region}.platform.bird.com/v1/voice/numbers/{number_id}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"inbound_configuration": {
"route": {
"type": "forward",
"forward_to": "+14155551234",
"forward_as": "dialed_number"
}
}
}'forward_to has to be a caller ID whose verification is complete. A number you have not registered, or one whose verification is pending or failed, is refused with 412 and E21053. Caller IDs covers registering and verifying one over the API.
The forward target is checked when you set it and again on every call it forwards. A caller ID you later remove stops forwarding rather than carrying on, and the incoming calls are rejected from that point.
The forwarded call rings for 45 seconds before it is given up on, which is longer than a trunk delivery rings because the far end is usually a person's phone rather than a phone system.
Forwarding places a call, so the outbound rules apply to the second leg: forwarding to a country you have not turned on under Destinations is refused with destination_not_enabled.
One forward, two call records
A forwarded call produces two leg records that share a call_id:
| Record | What it is |
|---|---|
| The arriving call | direction is inbound, and route says the number was set to forward, to which number, and which number the forwarded leg presented |
| The forwarded call | direction is outbound, from the number the leg presented to the number you forward to. It carries no route of its own |
Use the call_id filter on the leg list to find the related connections, or open Voice > Calls. The arriving record is the one that says what the number was configured to do.
Choose which number a forwarded call shows as the caller
A forwarded call has two numbers it could present to whoever answers, and the choice changes both what they see and how likely a carrier is to interfere with the call:
- Calling number is the caller's own number, so the phone rings as though they had dialed it directly and the call can be returned from the call log. Because the number is not one you own, some carriers, most often in the US and parts of Europe, mark such calls as unverified, replace the number, or screen them.
- Dialed number is the number the caller dialed, which is one of yours. Whoever answers sees which of your numbers was called rather than who called it.
State the choice on every forward you write over the API or CLI. An older configuration without a stored choice reads back the dialed number.
The number's inbound_configuration.forward_as_options lists the choices available to the editor. The current options include the calling number and the dialed number. Read those options when building an integration, and use the returned forward_as to confirm the effective setting.
On a read, forward_as is the value the calls actually carry, which can differ from the value last written.
Read what a number did with a call
An incoming call's record carries a route alongside its status, and route is what the number was set to do at the moment the call was handled. Changing the number's setting afterwards does not change what its past calls say.
route.type | What the number did |
|---|---|
trunk | The call was delivered to the SIP trunk named in trunk_id |
forward | The call was forwarded to the number in forward_to, presenting the number in forward_as |
reject | The number turned the call away |
sequence | The call selected the sequence in sequence_id and the entry in entry_node_id |
route says what the number was set to do, not that it worked. A trunk route on a call that never connected is a number pointed at a trunk that did not take the call, and the call's status is what carries the outcome. route is absent on outbound calls and on calls recorded before the field existed.
In the dashboard, open the call from Voice > Legs and read the Inbound route row, which links to the number whose settings decided it. Over the API, route is on GET /v1/voice/legs/{leg_id} and GET /v1/voice/legs, and direction filters the list to incoming calls.
For a sequence route, also inspect the sequence's Runs page to identify the entry and version that ran. The number's current configuration can differ from the version retained by an earlier call.
Diagnose a refused incoming call
An incoming call that was refused is recorded with the status rejected. Two different things produce it, and rejection_reason is what separates them:
- Rejected with no
rejection_reason. The number itself turned the call away. The call failed no check of ours, so it names no reason, androutesays what the number was set to do. Arejectroute is a number set to refuse, or one with no route of its own while the workspace default route is reject. - Rejected with a
rejection_reason. The call failed one of our checks before reaching your phone system. The reason names the check. Rejected calls lists every reason and its fix.
failed is a different status and does not mean refused: it means the call was attempted and did not work, with sip_response_code carrying the response that came back.
Read route and rejection_reason together to tell the refusals apart:
route and reason | Cause |
|---|---|
reject, no reason | Either the number has its own route set to reject, or it has no route of its own and the workspace default route is reject. Open the number to see which. Deleting a trunk, or turning its inbound calling off, can land a number that used to work here |
trunk, no_route_found | The number is pointed at a trunk, and that trunk has no gateway to deliver the call to. Add one on the trunk page |
forward, no reason | The forward target is no longer a verified caller ID. Re-verify it under Caller IDs, or forward to another number |
forward, destination_not_enabled | The second leg could not be placed to the forward target's country. Turn that country on under Destinations |
The account ceilings apply to incoming calls as well: past your wallet balance, your organization's daily voice spend limit, or your concurrency and per-second ceilings, an incoming call is rejected with the matching reason. Voice overview covers the ceilings themselves.
Check what a received call costs
Receiving a call is charged. The rate depends on the country and type of the number receiving it, and is published per country under Receiving calls on the voice pricing page, alongside the rates for calls you place.
A forward is billed as two calls: the arriving call at the receiving rate, and the leg we place at the outbound rate for the number you forward to. A single handling fee is charged once for the call rather than once per leg.
The wallet is checked before an incoming call is delivered, so a balance that cannot cover it means the call is rejected rather than billed to you afterwards. Cost and billing covers how billable time, rates, and the wallet work for both directions.
Next steps
| Page | What it covers |
|---|---|
| SIP trunks | Creating a trunk, its two directions, and controlling who may send |
| Caller IDs | Registering a number and proving you control it |
| Call log | Every field on a call record, and every rejection reason |
| Voice events | Getting call outcomes pushed to your own systems |
| Voice troubleshooting | Diagnosing a call that does not go through, from its symptom |
Related resources
Continue with the documentation, guides and examples for this topic.