Migrate from Telserv to Bird
This guide is for integrations that call the Telserv partner API, V1 or V2, to buy, route, and release numbers, and that now move to the Bird API.
The Bird API serves every Bird product, so its numbers and voice operations follow Bird's own conventions instead of Telserv's. Most capabilities your integration uses have a Bird call. The endpoints, payloads, identifiers, and authentication differ, so plan to rewrite the integration. Where Bird does a job differently, this guide shows how.
This guide is a work in progress. It covers what Bird does today, and it grows as the migration does. If a Telserv operation you use is not listed here, it still needs scoping and work before it can be added; ask your migration contact about it.
Before you start
- A Bird organization with completed identity verification, which the first number purchase requires.
- An API key with the scopes the recipes use:
numbersat write level for numbers and orders, andvoice_managementat write level for SIP trunks, voice numbers, and verified numbers. The test call also needsvoice:write, and reading call legs needsvoice:read. - Wallet balance. Orders are paid from the wallet. When an order is refused explains the
402and412refusals.
The numbers your integration manages in Telserv do not need to be bought again. Once your numbers are moved to your Bird workspace, they appear in GET /v1/numbers. Your migration contact confirms when that happens. To get the nda_ identifier of a moved number, look it up with GET /v1/numbers?number=<digits>, giving the digits with their country code, and keep the id as NUMBER_ID.
The examples read your key from BIRD_API_KEY and its regional host from BIRD_API_URL: https://eu1.platform.bird.com for a bk_eu1_ key, or https://us1.platform.bird.com for a bk_us1_ key. The other variables, such as TRUNK_ID and NUMBER_ID, hold an identifier from an earlier response; each step names the one it sets.
What changes in the contract
| Area | Telserv partner API | Bird API |
|---|---|---|
| Authentication | X-ApiKey header (V2) or apiKey parameter (V1), checked against your IP allowlist | Authorization: Bearer with an API key whose scopes name the product areas it may use. An IP allowlist is optional: set it under IP restrictions on the key in API keys |
| Identifiers | Integers. Buying a number gives it a new id | Prefixed TypeIDs such as nda_, nor_, vnu_, and spt_. A number keeps its nda_ id while it is allocated to you |
| Phone numbers | Digits without a leading + | E.164 with the leading + |
| Pagination | start and limit offsets, up to 1000 per page | Cursors: limit up to 100 (default 25), then starting_after set to the previous page's next_cursor. See Pagination |
| Errors | errorCode names an exception class, and 403 covers a missing resource and many validation failures | error.code is a stable E code with a documentation page and, where a recovery is known, a remediation. A missing resource answers 404, an unmet precondition 412, and invalid input 422 with field details. See Errors |
| Environments | LIVE and SANDBOX, each with its own keys | One environment. Each order buys and charges for a real number, and each test call is a real call |
| Retries | No idempotency key | An Idempotency-Key header on writes. The same key and body replay the first response with Idempotency-Replay: true; the same key with a different body answers 409. See Idempotency |
The Bird API refuses a query parameter it does not recognize with 422 and E01029. Remove Telserv-only filters such as city from your requests instead of passing them through.
Two identifiers for one number
A voice-capable number allocated to your workspace has two records, each with its own identifier:
- The allocation,
nda_, returned byGET /v1/numbersand as an order'snumber_id. Use it to read, name, reference, and release the number. - The voice record,
vnu_, returned byGET /v1/voice/numbers. Use it to read and change where the number's incoming calls go. A number without voice capability, such as an SMS-only number, has no voice record.
The voice operations refuse an nda_ identifier with 422. To find a number's voice record, list voice numbers with search set to the number's digits and take the entry whose provider.number_id equals the nda_ identifier. Route a number to your SIP trunk shows the request, and Managing your phone numbers covers the lookup.
Telserv's GET /numbers/{dnisId} becomes two reads in Bird: the allocation for status and ownership paperwork, and the voice record for the route.
Map Telserv operations to Bird
Each table lists a job, the Telserv call that did it, and the Bird call that does it now. The notes under each table cover what changes in the request or the response.
Shop
| Job | Telserv | Bird |
|---|---|---|
| Search numbers on sale | GET /shop | GET /v1/numbers/available |
| Check one number | GET /shop/{stockDnisId} | GET /v1/numbers/available/{number} |
| Buy a number | POST /shop/{stockDnisId} | POST /v1/numbers/orders |
| Secure a number | POST /shop/{stockDnisId}/reserve, GET /shop/reserved | POST /v1/numbers/orders |
- Search.
country_codeis required, so a search covers one country.prefixtakes the national digits after the country code: Telservprefix=3170becomescountry_code=NL&prefix=70. Narrow by area code withprefixin place ofcity, and bynumber_typeandcapabilitiesin place ofproductId. - Paperwork in search results. Each result carries
ownership_registration_requiredin place of theexcludeNumbersRequireVerificationfilter. Atruevalue means the number is billed from purchase but carries no voice calls until its paperwork is approved. - Check one number. It is addressed by its E.164 number.
- Buy. Send
numberand an optionalreference. The order is paid from your wallet. Most orders answer201; an order that waits on a carrier answers202, and you poll it. - Secure. Ordering the number secures it. Check a candidate first, then order it.
Numbers
| Job | Telserv | Bird |
|---|---|---|
| List your numbers | GET /numbers | GET /v1/numbers |
| Read one number | GET /numbers/{dnisId} | GET /v1/numbers/{number_id} and GET /v1/voice/numbers/{number_id} |
| Change a number | PATCH /numbers/{dnisId} | PATCH /v1/numbers/{number_id} and PATCH /v1/voice/numbers/{number_id} |
| Release a number | DELETE /numbers/{dnisId}?requestedCancelDate= | DELETE /v1/numbers/{number_id} |
- List. The list holds the numbers allocated to you now. A released number you ordered in Bird stays visible through its order in
GET /v1/numbers/orders. Reconcile your numbers maps the filters. - Read. Status and ownership paperwork are on the allocation, and the route is on the voice record.
- Change.
nameandreferenceare on the allocation, and the route is on the voice record. Set a forced codec and a cap on concurrent calls on your SIP equipment.destinationDistributionTypebecomes gatewaypriorityon the SIP trunk:FailOvergives each gateway a differentpriority, andRoundRobingives them the same one. - Release. The release is immediate: the number stops working at once, and its subscription ends with the period you already paid for instead of renewing. Release a number shows the request.
Destinations and profiles
| Job | Telserv | Bird |
|---|---|---|
| Send calls to SIP equipment | POST /numbers/{dnisId}/destinations | POST /v1/voice/trunks/{trunk_id}/gateways, then route trunk |
| Forward calls to a phone | POST /numbers/{dnisId}/destinations | Route forward |
| Stop taking calls | DELETE /numbers/{dnisId}/destinations | Route reject |
| Read destination profiles | GET /destinationprofiles, GET /destinationprofiles/{id} | GET /v1/voice/trunks and GET /v1/voice/trunks/{trunk_id}/gateways |
| Apply a profile to a number | POST /numbers/{dnisId}/destinations/profile | Route trunk with trunk_id |
- SIP equipment. A gateway belongs to the trunk and serves each number routed to it, so numbers that need different destination lists need different trunks.
sortOrderbecomespriority. Apply ring time and time windows on your SIP equipment. - Forward. Set
forward_toandforward_as.forward_tomust be one of your verified numbers; an unverified number answers412withE21053.forward_aschooses which number the forwarded call shows as its caller. A number has one route at a time, so to fall back from SIP equipment to a phone number, forward the call from your SIP equipment. - Stop taking calls. The number rejects calls until you route it again. To keep a standing default in place of a default profile, route the number to a trunk that holds your default gateways.
- Profiles.
distributionTypebecomespriority:FailOvergives each gateway a differentpriority, andRoundRobingives them the same one. Distribution is set per trunk, so numbers that need different distribution need different trunks. The{dnis}placeholder becomes{number}indestination_format, and aprefixCLIPbecomes the text before{number}inorigination_format. - Apply a profile. A trunk is addressed by its
spt_identifier, where Telserv addressed a profile by name.
Documents and end-user data
| Job | Telserv | Bird |
|---|---|---|
| Check paperwork progress | GET /numbers/{dnisId}/documents | ownership on GET /v1/numbers/{number_id} |
| File paperwork | POST /numbers/{dnisId}/documents, POST /numbers/{dnisId}/enduserdata | The number on the Numbers page |
| Read the requirements | GET /products/conditions, GET /masterdata/productconditions | The number on the Numbers page, when you file |
ownership.status reports progress, and the filed documents are on the number in the dashboard. ownership is usually null when the number needs no paperwork.
Products and master data
| Job | Telserv | Bird |
|---|---|---|
| Browse products | GET /products, GET /products/{productId} | GET /v1/numbers/available |
Each search result describes its product with number_type, capabilities, and ownership_registration_required. List prices are on the phone number pricing page.
Telserv number types map onto Bird's number_type like this:
Telserv numberType | Bird number_type |
|---|---|
DID | local or national, depending on the number |
FreePhone | toll_free |
Mobile | mobile |
What works differently today
These Telserv jobs are done differently in Bird. Plan for them before you move an integration that depends on one.
- Prices. List prices are on the phone number pricing page.
- Paperwork. File documents and end-user data from the number in the dashboard, and follow their progress in
ownershipover the API. - Reservations. Order the number when you have chosen it; the order secures it.
- Per-number call settings. Apply a forced codec, a cap on concurrent calls, ring time, and time windows on your SIP equipment.
- A release date. Keep the cancellation dates you sent to Telserv on your side, and release each number on its date.
- Wallet balance. Check the balance and top up in the dashboard.
Search and buy a number with your reference
Search one country. Combine number_type, capabilities, and prefix to narrow the results:
curl "$BIRD_API_URL/v1/numbers/available?country_code=NL&number_type=mobile&capabilities=voice&limit=10" \
--header "Authorization: Bearer $BIRD_API_KEY"To confirm a candidate before you order it, read it with GET /v1/numbers/available/{number}. A 404 means it is no longer for sale.
Order the number with the reference you used in Telserv, and choose an idempotency key you keep for any retry of this purchase:
curl --request POST "$BIRD_API_URL/v1/numbers/orders" \
--header "Authorization: Bearer $BIRD_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: order-CUST-778812" \
--data '{"number": "+31612345678", "reference": "CUST-778812"}'A 201 response has status: completed and a number_id, the number's nda_ identifier. Keep it as NUMBER_ID. A 202 response means the order waits on a carrier: poll the order until its status is completed or failed. A setup fee already charged is not refunded if the order then fails. The reference is set on the number when the order completes. It holds up to 100 characters and does not need to be unique.
Retrying an order for a number your workspace has allocated returns the existing order, so a retry does not charge twice. A 404 with E03007 means the order is refused because that country and number type has no price. Buy and release a number explains the other refusals.
To set or change a reference later, send reference (and optionally name) to PATCH /v1/numbers/{number_id}. Send null to clear a field.
Check that a number can carry calls
A number whose status is pending_ownership_registration is allocated and billed, but it rejects incoming and outgoing voice calls until its ownership paperwork is approved. Before you route a number or place a test call to it, read it with GET /v1/numbers/{number_id} and check that status is active. If it is pending, file the paperwork from the number on the Numbers page. The status of an allocated number explains each value.
The examples below use a Dutch mobile number. Check the status of your own number in the same way, whatever its country.
Route a number to your SIP trunk
A new number starts on the reject route, so it turns callers away until you route it. To deliver its calls to your equipment, create a SIP trunk with inbound calling on, give it a gateway, and point the number at the trunk. SIP trunks covers connecting your equipment to the trunk.
-
Create the trunk with inbound calling on. Keep the
idfrom the response, the trunk'sspt_identifier, asTRUNK_ID:Code examplecurl --request POST "$BIRD_API_URL/v1/voice/trunks" \ --header "Authorization: Bearer $BIRD_API_KEY" \ --header "Content-Type: application/json" \ --header "Idempotency-Key: trunk-production-pbx" \ --data '{"name": "Production PBX", "inbound_enabled": true}' -
Add a gateway. By default the gateway receives the dialed and calling numbers in E.164 with their
+. If your equipment expects numbers without the+, as Telserv's{dnis}placeholder did, setdestination_formatfor the dialed number andorigination_formatfor the calling number to{number}:Code examplecurl --request POST "$BIRD_API_URL/v1/voice/trunks/$TRUNK_ID/gateways" \ --header "Authorization: Bearer $BIRD_API_KEY" \ --header "Content-Type: application/json" \ --header "Idempotency-Key: gateway-pbx-primary" \ --data '{"sip_uri": "sip:pbx.example.com:5060", "priority": 0, "destination_format": "{number}", "origination_format": "{number}"}'For failover, add a second gateway; What a gateway needs explains
priorityand the number formats. Creating the same gateway again without an idempotency key answers409withE21047. -
Find the number's voice record. Search with the number's digits and take the
idof the entry whoseprovider.number_idis yournda_identifier. Keep it asVOICE_NUMBER_ID:Code examplecurl "$BIRD_API_URL/v1/voice/numbers?search=31612345678" \ --header "Authorization: Bearer $BIRD_API_KEY" -
Point the number at the trunk:
Code examplecurl --request PATCH "$BIRD_API_URL/v1/voice/numbers/$VOICE_NUMBER_ID" \ --header "Authorization: Bearer $BIRD_API_KEY" \ --header "Content-Type: application/json" \ --data '{"inbound_configuration": {"route": {"type": "trunk", "trunk_id": "'"$TRUNK_ID"'"}}}'A
200response shows the new route. A trunk with inbound calling off answers412withE21052.
Turning inbound calling off on a trunk, or deleting the trunk, puts each number routed to it back on reject, and turning it on again does not restore those routes. Deliver calls to a SIP trunk explains this and the trunk, forward, and reject routes.
Reconcile your numbers
Walk the list with the largest page size. Pass each page's next_cursor as NEXT_CURSOR to read the next page, until next_cursor is null. The list returns no total count.
curl "$BIRD_API_URL/v1/numbers?limit=100" \
--header "Authorization: Bearer $BIRD_API_KEY"
curl "$BIRD_API_URL/v1/numbers?limit=100&starting_after=$NEXT_CURSOR" \
--header "Authorization: Bearer $BIRD_API_KEY"Telserv's list filters map onto Bird's like this:
Telserv filter on GET /numbers | Bird filter on GET /v1/numbers |
|---|---|
number, which matches the start of the number | prefix without country_code, which matches the digits from the start of the full number |
prefix, which equals the access and area code such as 3170 | country_code with prefix set to the national digits: country_code=NL&prefix=70 |
reference with a partial value | search, which matches part of the number, name, or reference, ignoring case |
reference with a quoted exact value | reference, which returns the numbers with that exact reference. The match is case-sensitive |
| New in Bird | number returns one whole number given with its country code, with or without + or 00: +31612345678, 31612345678, and 0031612345678 find the same number |
Find a number in your list covers each filter in detail.
A released number leaves this list, and reading it by number_id answers 404. If you ordered it in Bird, its completed order stays in GET /v1/numbers/orders.
Release a number
Release a dedicated number with its nda_ identifier:
curl --request DELETE "$BIRD_API_URL/v1/numbers/$NUMBER_ID" \
--header "Authorization: Bearer $BIRD_API_KEY" \
--header "Idempotency-Key: release-CUST-778812"A 204 response means the number is released, effective immediately. The request takes no date, so keep the schedule you sent to Telserv as requestedCancelDate on your side and send the release on the day. The number stops working at once, and its subscription ends with the period you already paid for instead of renewing. Release a number covers what a release does to the number, its charges, and a retry.
Place a test call to your number
To check a route end to end, place a call to the number from another number your workspace may present as a caller ID. The number's country must be enabled in your voice destinations, or the call does not go through. The request below runs an inline sequence that hangs up once the call is answered, so no saved sequence is needed. It places a real call, and normal calling charges apply.
Save the request as test-call.json. Put the caller ID you present in from, an owned or verified number of your workspace, and the number under test in to:
{
"from": "+31612345679",
"to": "+31612345678",
"sequence": {
"definition": {
"schema_version": 1,
"expression_environment": "bird.cel.v1",
"nodes": [
{
"id": "entry",
"type": "trigger.start_call",
"type_version": 1,
"config": {},
"input": {},
"connections": { "event": { "node_id": "end", "port": "input" } }
},
{ "id": "end", "type": "voice.hangup", "type_version": 1, "config": {}, "input": {} }
]
},
"entry_node_id": "entry",
"trigger_data": {}
}
}Send it:
curl --request POST "$BIRD_API_URL/v1/voice/calls" \
--header "Authorization: Bearer $BIRD_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: ${TEST_CALL_KEY:?Set TEST_CALL_KEY once for this test call}" \
--data-binary @test-call.jsonSet TEST_CALL_KEY to a new value for each test, and reuse it when you retry the same test after a timeout or an unclear result: the same key and request within three hours replay the first acceptance instead of placing a second call. A 202 response accepts the call and returns its initial_leg_id. Acceptance does not prove the call connected: read the leg with GET /v1/voice/legs/{leg_id}, or list the legs placed to the number with GET /v1/voice/legs and its to filter, and check that your equipment received the call. Create a voice call covers the caller-ID and destination requirements and the retry rules.
Next steps
Related resources
Continue with the documentation, guides and examples for this topic.