Signals Docs
SIP Trunks

Numbers and routing

A trunk needs phone numbers. Numbers let people call your PBX, and they are the caller IDs your PBX may show when it calls out. This page explains how you find and buy numbers on Signals, how you point them at a trunk, and how you hand slices of them to subtrunks.

How numbers are organised

Signals sells Nigerian numbers in blocks: runs of consecutive numbers of size 10, 50, 100, 500 or 1,000. A block of 100 might run from +2342013300100 to +2342013300199. Block IDs start with nbk_.

Buying a block takes four steps, each with its own word:

StepWordWhat it is
1SearchFind available blocks that match a prefix, size or memorable pattern.
2HoldReserve one exact block for 15 minutes so nobody else can take it while you decide.
3QuoteThe fixed price Signals gives you with the hold. It does not change while it is valid.
4OrderAccept the quote. Signals charges your balance and makes the block yours.

After you own a block, you route it: you tell Signals which trunk each number, or each slice of numbers, should ring.

Every number belongs to a tier, a price class such as standard. Memorable numbers sit in higher tiers. GET /v1/numbers/tiers lists the tiers in rank order, with their vanity patterns and your project's setup and monthly prices. A tier with null prices is not sold to your project.

Prices are integers of kobo in strings, so "1500000" is ₦15,000.00. The prices in the examples below are illustrative, and some responses are trimmed to the fields this page discusses.

Search for a block

GET /v1/numbers/blocks/search returns the blocks you can hold now under data:

TerminalCode
curl "$SIGNALS/numbers/blocks/search?size=100&prefix=201330" \ -H "Authorization: Bearer $SIGNALS_API_KEY"
ParameterMeaning
sizeRequired. 10, 50, 100, 500 or 1000.
prefixThe national digits after +234, up to 10 digits. Spaces are allowed. 201330 finds numbers starting +234201330.
patternsComma-separated pattern codes from GET /v1/numbers/tiers.
digitsDigits a memorable number should contain, up to 10.
matchHow digits match: ends_with (the default) or contains.
setup_up_to_minorThe most you want to pay in setup, in kobo.
JSONCode
{ "data": [ { "vanity_number": null, "start": "+2342013300100", "end": "+2342013300199", "size": 100, "tier": "standard", "reasons": [], "as_of": "2026-10-01T11:04:52Z", "adjacent_to": null, "setup_minor": 1500000, "monthly_minor": 2000000, "currency": "NGN", "tiers": [ { "tier": "standard", "count": 100, "setup_unit_minor": 15000, "monthly_unit_minor": 20000, "initial_unit_minor": 20000 } ] } ] }

When you search with digits, vanity_number shows the memorable number separately from the block that contains it. adjacent_to names a block you already own that this candidate sits next to.

Hold a block

Hold the exact candidate by sending its start, end and size. Add vanity_number if you searched for one:

TerminalCode
curl -X POST "$SIGNALS/numbers/blocks/holds" \ -H "Authorization: Bearer $SIGNALS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 3a8e1f62-5b0c-4d97-a2e4-8f6c1b9d0e35" \ -d '{ "start": "+2342013300100", "end": "+2342013300199", "size": 100 }'
JSONCode
{ "id": "nbk_01992f40-8e52-7c19-a3d4-6f0b2e9c1d77", "block_id": "nbk_01992f40-8e52-7c19-a3d4-6f0b2e9c1d77", "start": "+2342013300100", "end": "+2342013300199", "size": 100, "origin": "standard", "vanity": null, "held_at": "2026-10-01T11:05:30Z", "expires_at": "2026-10-01T11:20:30Z", "quote": { "id": "quo_01992f40-8e53-7b2a-91c5-0d4e7a3f6b12", "version": 1, "currency": "NGN", "setup_minor": 1500000, "monthly_minor": 2000000, "initial_minor": 1000000, "lines": [ { "label": "Setup, 100 standard numbers", "amount_minor": 1500000 }, { "label": "Rental to the end of October", "amount_minor": 1000000 } ], "expires_at": "2026-10-01T11:20:30Z" }, "target": null }
  • The hold lasts 15 minutes. expires_at is the server's deadline; after it, the block returns to stock.
  • initial_minor is the first month's rental, prorated from today to the end of the month.
  • Your organization can hold at most 25 blocks and 25,000 numbers at once by default. Going over returns 409 number_hold_limit_exceeded.
  • If someone else took the block first, you get 409 number_candidate_unavailable. Search again.

To let a hold go before it expires, call DELETE /v1/numbers/blocks/{block_id}/hold with Idempotency-Key. GET /v1/numbers/holds lists your open holds.

Read the quote

The order step needs the quote's hash, which the hold response does not include. Read the quote:

TerminalCode
curl "$SIGNALS/numbers/quotes/quo_01992f40-8e53-7b2a-91c5-0d4e7a3f6b12" \ -H "Authorization: Bearer $SIGNALS_API_KEY"
JSONCode
{ "id": "quo_01992f40-8e53-7b2a-91c5-0d4e7a3f6b12", "block_id": "nbk_01992f40-8e52-7c19-a3d4-6f0b2e9c1d77", "version": 1, "status": "active", "quote_hash": "c5e1a9f3d7b20864e3f1a5c9d7b2e06f4a8c1e5d9b3f7a2c6e0d4b8f1a5c9e37", "setup_minor": 1500000, "initial_minor": 1000000, "expires_at": "2026-10-01T11:20:30Z", "accepted_at": null, "order": null }

status is active while you can accept it, then expired or accepted.

Buy the block

Accept the quote:

TerminalCode
curl -X POST "$SIGNALS/numbers/blocks/nbk_01992f40-8e52-7c19-a3d4-6f0b2e9c1d77/assign" \ -H "Authorization: Bearer $SIGNALS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6d2b9f47-0e1a-4c83-b5f6-9a3e7c1d4b20" \ -d '{ "quote_id": "quo_01992f40-8e53-7b2a-91c5-0d4e7a3f6b12", "quote_hash": "c5e1a9f3d7b20864e3f1a5c9d7b2e06f4a8c1e5d9b3f7a2c6e0d4b8f1a5c9e37" }'
JSONCode
{ "block_id": "nbk_01992f40-8e52-7c19-a3d4-6f0b2e9c1d77", "state": "pending_charge", "total_minor": 2500000, "currency": "NGN" }

total_minor is the setup fee plus the prorated first month. Signals charges your balance in the background. Read the block with GET /v1/numbers/blocks/{block_id} until it is active. If your balance is too low, the order ends in insufficient_funds. An expired quote returns 409 number_quote_expired.

Route numbers to a trunk

A route connects a slice of a block to a target. Calls to numbers in the slice ring that target. Each block has a revision of its own, on GET /v1/numbers/blocks/{block_id} and in the block's ETag. Send it, in quotes, as If-Match:

TerminalCode
curl -X PUT "$SIGNALS/numbers/blocks/nbk_01992f40-8e52-7c19-a3d4-6f0b2e9c1d77/routes" \ -H "Authorization: Bearer $SIGNALS_API_KEY" \ -H "Content-Type: application/json" \ -H 'If-Match: "1"' \ -H "Idempotency-Key: 4c7e2a91-8d3f-4b56-a0e9-1f5b8d2c7a64" \ -d '{ "slices": [ { "start": "+2342013300100", "end": "+2342013300199", "target": { "type": "trunk", "id": "trk_01992f3a-6c1e-7d42-9b8a-5e7f0c2d1a94" } } ] }'

The API returns 200 OK with the block's routes as saved:

JSONCode
{ "data": [ { "start": "+2342013300100", "end": "+2342013300199", "count": 100, "desired": { "type": "trunk", "id": "trk_01992f3a-6c1e-7d42-9b8a-5e7f0c2d1a94", "name": "office-pbx" }, "effective": null, "status": "pending", "failure": null } ] }

The route takes effect within a minute. Then effective names the trunk and status is applied.

  • Slices in one request must not overlap. Slices you leave out keep their current route.
  • A slice target can be a trunk, subtrunk or voice_application.
  • A null target parks the numbers: they stay yours but ring nothing.
  • GET /v1/numbers/blocks/{block_id}/routes shows each slice with the target you saved (desired), the target calls reach now (effective) and a status of applied, pending or failed.

The target trunk must be ready to take calls. If it is not, the request fails with 409 conflict_target_version and a reason:

ReasonWhat to fix
target_not_foundThe trunk does not exist in this project.
target_disabledThe trunk is turned off.
target_not_inboundThe trunk's direction is outbound. Use inbound or both.
target_not_admittingThe network is refusing traffic on the trunk, for example while it is suspended.

Routed numbers become caller IDs

A trunk may present only the numbers routed to it as caller ID on outbound calls. Until a number is routed, the trunk has no caller ID it may present. A number can be used as caller ID as soon as its route applies. The trunk's numbers field shows how many numbers are routed to it.

Delegate slices to subtrunks

A subtrunk is a child of a trunk. It shares the parent's connection and authentication but has its own name, status and limits, and the limits can only be tighter than the parent's. Use subtrunks to separate departments or customers on one PBX connection, each with its own numbers and caps. Subtrunk IDs start with str_.

Create a subtrunk

TerminalCode
curl -X POST "$SIGNALS/trunks/$TRUNK_ID/subtrunks" \ -H "Authorization: Bearer $SIGNALS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 9b4f1d36-2c8e-4a70-b3d5-7e0a6c9f2b18" \ -d '{ "name": "sales-line", "status": "enabled", "limits": { "concurrency": 10, "cps": 2, "max_call_duration_seconds": 3600, "max_destination_rate_minor": null, "daily_spend_cap_minor": 1000000, "velocity_cap_minor": null } }'

A null limit inherits the parent's value. A value above the parent's is refused. When you send limits, on a create or an update, send all six keys: a body that leaves one out is refused with 422 invalid_subtrunk rather than clearing that limit. The subtrunk is live once created and takes effect within a minute.

Delegate a slice

A delegation hands a slice of a block, already routed to the parent trunk, to one of its subtrunks:

TerminalCode
curl -X POST "$SIGNALS/numbers/blocks/nbk_01992f40-8e52-7c19-a3d4-6f0b2e9c1d77/delegations" \ -H "Authorization: Bearer $SIGNALS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 2e6a0c84-7f1b-4d39-9c52-a8d3f5b1e076" \ -d '{ "subtrunk_id": "str_01992f45-3b07-7f2c-8a16-4d9e1c5b7f30", "start": "+2342013300100", "end": "+2342013300199", "size": 100 }'

The API returns 200 OK with the block as saved, the new delegation listed under delegations. It takes effect within a minute. A delegation must meet all of these rules:

  • size is 100, 500 or 1000, and start to end covers exactly that many numbers.
  • The slice is aligned: its distance from the start of the block is a multiple of size.
  • The block is active, and the slice is inside it.
  • The parent trunk already has an applied route covering the slice.
  • The subtrunk and its parent trunk are both enabled.
  • The slice does not overlap another live delegation.

A rule that fails returns 409 number_delegation_conflict or 422. To undo a delegation, call DELETE /v1/numbers/blocks/{block_id}/delegations/{delegation_id} with Idempotency-Key. It also returns 200 OK with the block.

Release a block

POST /v1/numbers/blocks/{block_id}/release with the exact start and end of the block starts returning it and answers 200 OK with the block. The API refuses a block that still has active routes or delegations (409 number_block_in_use). The block moves to pending_release, then to quarantined once the network confirms. quarantine_until on the block shows when the quarantine ends.

Last modified on