Number Purchase API
Programmatically browse, buy, and remove Blooio lines over the v4 API — including the KYC, access-approval, and billing steps required before you can purchase.
The Number Purchase API lets you provision Blooio lines from your own code instead of clicking through the dashboard. You can browse available inventory, buy shared, dedicated, or inbound lines, poll the purchase, and remove (unsubscribe) a line — all over the v4 API.
This is the same billing and provisioning flow the dashboard uses, so the two never drift. It's also how resellers and platforms (for example, AgentPhone) provision a fresh number for each of their end customers without a human in the loop.
Note
The Number Purchase API is a v4 feature. All endpoints live under
https://api.blooio.com/v4. Make sure the version toggle at the top of the docs is set to v4.
The full lifecycle — request access, browse, purchase, provision, track, and remove. Click a step for details; drag to pan, use the controls to zoom.
Before you can buy a line
Purchasing is gated. An organization has to clear three prerequisites before any purchase call will succeed — this keeps line provisioning tied to a verified, billable business.
1. Complete organization KYC
Open Organization Settings → Identity Verification in the dashboard and complete KYC with Stripe Identity. This verifies the business behind the organization and unlocks the features (including number purchasing) that aren't available to unverified organizations.
Until KYC shows as verified, requesting access is blocked and the API returns 403.
2. Request Number Purchase API access
In the same Organization Settings page, find the Number Purchase API section and submit the access request. You'll be asked for:
- What you're building — your use case.
- Website — your company/product URL.
- Notes — anything else the review team should know (optional).
Submitting opens a review ticket. A Blooio staff member approves it, which flips settings.number_purchase_api.enabled to true for your organization. Only an organization admin can submit the request.
Warning
Until access is approved, every Number Purchase API endpoint returns
403withcode: feature_not_enabledand the message "The number purchase API is not enabled for this organization. Request access in your organization settings."
3. Add a payment method
Number purchases are billable. They're paid from your organization's account credit first, then its default card on file. Add a card in the dashboard's billing settings unless your credit covers your purchases. When the credit doesn't cover a purchase and there's no saved card, POST /channels/blooio/purchases returns 402 with code: no_payment_method and charges nothing.
Per-order line cap
A single purchase can't exceed your organization's per-order line cap (default 10). If you need to buy more lines in one call, apply for an increase under Overview → Limits in the dashboard. Exceeding the cap returns 400 with code: line_limit_exceeded.
Authentication
Both credential types work, exactly like the rest of the v4 API:
- API keys —
Authorization: Bearer bl_live_.... Full organization access. - OAuth app tokens — when calling on behalf of another organization, the token must carry the right scope:
numbers:read— list available inventory and read purchase status.numbers:manage— purchase and remove lines.
Authorization: Bearer YOUR_BLOOIO_API_KEYSee Calling the API with OAuth tokens for the app flow.
Step 1 — Browse available inventory
Use GET /channels/blooio/available to see what's in stock. The type query selects the plan family:
shared— available shared lines (returns masked inventory rows).dedicated— unassigned dedicated inventory.inbound— reply-only inbound lines (provisioned by area code — passarea_codeto get a quote).
curl 'https://api.blooio.com/v4/channels/blooio/available?type=dedicated&country=US&limit=5' \
-H 'Authorization: Bearer YOUR_BLOOIO_API_KEY'Try it{
"data": [
{
"masked_national": "(801) ***-****",
"area_code": "801",
"country_code": "1",
"phone_number_country": "US",
"location": "Salt Lake City, UT"
}
],
"has_more": false,
"next_cursor": null
}Note
The full phone number is never returned before purchase — only a masked form (
(801) ***-****). The actual line is revealed once it's provisioned to your account.
Full schema: List available numbers.
Area-code quotes
For dedicated or inbound, pass one or more area_code values to check availability by area code. Each requested code comes back as either in stock (provisioned free) or needing a custom order (a custom area-code fee applies and a provisioning ticket is opened at purchase).
curl 'https://api.blooio.com/v4/channels/blooio/available?type=dedicated&area_code=801&area_code=212' \
-H 'Authorization: Bearer YOUR_BLOOIO_API_KEY'Try it{
"data": [
{ "area_code": "801", "matched": true },
{ "area_code": "212", "matched": false, "custom_order": true }
],
"matched_count": 1,
"custom_order_count": 1,
"has_more": false,
"next_cursor": null
}Step 2 — Purchase a line
Buy lines with POST /channels/blooio/purchases. The call charges your account credit, then your default payment method, and returns a purchase_id; the lines are provisioned asynchronously once Stripe confirms the invoice.
Warning
An
Idempotency-Keyheader is required on every purchase. It makes the billable call safe to retry — replaying the same key returns the same purchase instead of charging twice.
curl -X POST 'https://api.blooio.com/v4/channels/blooio/purchases' \
-H 'Authorization: Bearer YOUR_BLOOIO_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 7f3c1e0a-9b2d-4c11-8f6a-2b1c9d4e5f60' \
-d '{
"plan": "dedicated_com",
"quantity": 2,
"area_codes": ["415", "628"]
}'Try it| Field | Type | Notes |
|---|---|---|
plan |
string (required) | Plan id — use the specific id (e.g. dedicated_com), not the family name (dedicated returns invalid_plan). See Plans. |
quantity |
integer | Number of new lines (dedicated/inbound). Shared is always 1. Defaults to 1. |
area_codes |
string[] | Preferred 3-digit US area codes. Out-of-stock codes incur a custom fee and open a ticket. |
zip_codes |
string[] | Preferred ZIP codes, used when an area code isn't specified. |
custom_requests |
(object | null)[] | Per-line requests for a line Blooio staff set up by hand, like your own eSIM, a port-in, or a number from another country. Each custom line costs the one-time custom fee. See Custom provisioning requests. |
auto_bind |
string | The api_key_id of an API key in your organization. Every line in the purchase is assigned to that key once it has a number. See Assign the lines to an API key. |
A successful call returns 202 Accepted while provisioning proceeds in the background:
{ "data": { "purchase_id": "idem_abc123", "status": "provisioning" } }Full schema: Purchase numbers.
Rules enforced
Checked server-side before the card is charged, identically to the dashboard:
- Credit or a payment method required — without a saved card, a purchase your credit doesn't cover in full returns
402 no_payment_method. - One shared number per organization — a second shared purchase returns
409 shared_number_limit. - Per-order line cap — a single order can't exceed the cap (default 10 new lines); larger orders return
400 line_limit_exceeded. Raise it from Overview → Limits in the dashboard. - Custom area codes — out-of-stock codes are billed the custom area-code fee and open a provisioning ticket; in-stock codes are assigned immediately at no custom fee.
- Custom requests — each line with a custom request is billed the same custom fee and opens a provisioning ticket. A line is charged the fee once, even if it also asks for an out-of-stock area code.
auto_bind— the key must be a live key in your organization, and on plans without number pools it can own only one line. See Assign the lines to an API key.
Cards that need authentication (3DS/SCA)
If the card requires 3DS/SCA, you get 202 with status: action_required and an action_url. Send the customer to that URL to authenticate; a number.purchase.action_required webhook also fires.
{
"data": {
"purchase_id": "idem_abc123",
"status": "action_required",
"action_url": "https://invoice.stripe.com/i/..."
}
}Custom provisioning requests
Some lines can't come from our stock. Examples include running iMessage on an eSIM you already own, porting in a number from your carrier, a number from another country, or any other setup that needs a person. For these, send custom_requests with the purchase. Each line with a custom request is set up by Blooio staff with the same provisioning process we use for custom area codes.
Request shape
custom_requests is an array matched to the lines in your order by position, the same way area_codes is. Entry 0 describes line 1, entry 1 describes line 2, and so on.
| Entry | Meaning |
|---|---|
{ "notes": "..." } |
This line is a custom request. notes is free text telling our staff what you need. |
{} |
This line is a custom request with no notes. Staff will follow up on the ticket if they need details. |
null, or no entry |
This line is fulfilled normally: from stock, or by its area code or ZIP. |
notes is passed to our staff exactly as you send it. We don't parse or validate it, so write it for a person and include everything they need. The only checks are on the shape:
custom_requestsmust be an array of objects ornull.- It can't have more entries than
quantity. - It's only accepted on
dedicatedandinboundplans. Shared lines come from a shared pool and can't be customized.
Examples
One line that runs on an eSIM you own:
curl -X POST 'https://api.blooio.com/v4/channels/blooio/purchases' \
-H 'Authorization: Bearer YOUR_BLOOIO_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: esim-move-8f2c' \
-d '{
"plan": "dedicated_com",
"quantity": 1,
"custom_requests": [
{ "notes": "Install our eSIM for +15551234567 (T-Mobile). Activation code: LPA:1$smdp.example.com$K2-ABC123. ICCID 8901260123456789012." }
]
}'Try itA mixed order. Line 1 ports in an existing number, line 2 is a normal in-stock line in area code 415, and line 3 needs a UK number:
{
"plan": "dedicated_com",
"quantity": 3,
"area_codes": ["", "415", ""],
"custom_requests": [
{ "notes": "Port in +12125550123 from Verizon. Account 123456789, transfer PIN 4821, billing ZIP 10001." },
null,
{ "notes": "UK mobile number (+44), London if possible." }
]
}What happens after you buy
- You're charged up front. The call charges the plan price for every line, plus the one-time custom fee ($75) for each custom line. It returns
202withstatus: provisioning, like any purchase. - Each custom line opens a ticket. Once payment clears, every custom line becomes a pending line with its own provisioning ticket. The ticket's first message repeats your notes. A custom line is never assigned automatically, even when matching stock exists.
- Our staff set it up. The team reads the notes and does the work, such as installing the eSIM or completing the port. Then they assign the line to your organization, which closes its ticket.
- The line goes live. It gets its phone number, and you can send from it.
Turnaround depends on the request. A port-in, for example, waits on the losing carrier.
Following a custom line
- API: poll the purchase. Each pending custom line appears in
allocationswithphone_number: nulland aticket_id. Read that ticket withGET /v4/tickets/{ticketId}. The purchase stayspendinguntil every line has a number, then becomescompleted. - Dashboard: the line shows under Pending provisioning on the Blooio P2P page and as Provisioning… on the Billing page. Its View request link opens the ticket with your notes and its status.
Warning
number.purchase.completedfires when payment clears, not when staff finish a custom line. Custom lines aren't listed in that event'slinesorphone_numbers, and no second webhook fires when staff assign the line later. PollGET /channels/blooio/purchases/{purchaseId}or watch the ticket until the line has aphone_number.
Note
Your notes are stored as sent. Blooio staff can see them, and so can anyone in your organization who can view tickets. Only include credentials, such as an eSIM activation code or a port-out PIN, if you're comfortable with that. If you'd rather not include them, say so in the notes, and staff will follow up on the ticket. You can't reply on purchase tickets.
Assign the lines to an API key
A new line has no owner, so a send authenticated with an API key can't use it until you assign it to the key. To skip that step, name the key in the purchase with auto_bind, using the key's api_key_id. It's a public identifier, so you never send a key's secret to name it. Copy it from the key's ⋯ menu in the dashboard, or, with an app token, read it from GET /v4/api-keys.
curl -X POST 'https://api.blooio.com/v4/channels/blooio/purchases' \
-H 'Authorization: Bearer YOUR_BLOOIO_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: acme-lines-01' \
-d '{
"plan": "dedicated_ent",
"quantity": 2,
"area_codes": ["415", "212"],
"auto_bind": "acme-prod"
}'Try itEach line is assigned to the key as soon as it gets its number:
- In-stock lines are assigned when payment clears.
- Lines Blooio staff provision (custom requests, ZIP codes, out-of-stock area codes) are assigned when staff finish them, which can be days later. You don't need to do anything when they go live.
- A key deprecated before its line is provisioned doesn't get the line, which is then left unassigned.
A line is assigned once. If Blooio later swaps the number behind it, for example after a carrier ban, the replacement carries over the line's current owners, and auto_bind doesn't assign it again. A line you've moved to another key stays there.
Both API keys and app tokens can send auto_bind, and it can name any live key in your organization. Without it, the lines are bought unassigned, as before.
Note
Plans without number pools (
shared_nc,shared_com,dedicated_com) let a key own one line on such a plan. On those plans,auto_bindneedsquantity: 1and a key that doesn't already own a line on one of them. Otherwise the purchase returns409 auto_bind_exceeds_planbefore anything is charged.
Step 3 — Track provisioning
Provisioning finishes after the invoice is paid. You can either poll or listen for a webhook.
Poll the purchase
curl 'https://api.blooio.com/v4/channels/blooio/purchases/idem_abc123' \
-H 'Authorization: Bearer YOUR_BLOOIO_API_KEY'Try itThe status moves from pending to completed, once every line in allocations has a phone number, or to failed.
{
"data": {
"purchase_id": "idem_abc123",
"status": "completed",
"allocations": [
{ "binding_id": "bind_...", "phone_number": "+14155550142", "type": "dedicated", "ticket_id": null }
]
}
}Some lines are set up by Blooio staff: custom requests, and area codes or ZIPs that aren't in stock. While staff work on one, it has phone_number: null and the ticket_id of its provisioning ticket, and the purchase stays pending:
{
"data": {
"purchase_id": "idem_abc123",
"status": "pending",
"allocations": [
{ "binding_id": "bind_...", "phone_number": "+14155550142", "type": "dedicated", "ticket_id": null },
{ "binding_id": "bind_...", "phone_number": null, "type": null, "ticket_id": "tkt_..." }
]
}
}Full schema: Get purchase status.
Or listen for webhooks
Prefer webhooks over polling for production. Because purchases complete asynchronously, subscribe to these events to learn the outcome:
| Event | When |
|---|---|
number.purchase.completed |
Payment cleared and lines provisioned. Payload carries purchase_id, lines (each { phone_number, channel_id }), plus flat phone_numbers and channel_ids arrays covering every line assigned so far. Lines still waiting on staff (custom requests, out-of-stock area codes) aren't included yet. Poll the purchase for those. |
number.purchase.action_required |
Card needs 3DS/SCA. Payload carries purchase_id, action_url. |
number.purchase.failed |
Terminal failure; nothing provisioned. Payload carries purchase_id, reason. |
number.removed |
A line was removed/unsubscribed. Payload carries phone_number, channel_id, binding_id, reasons. |
For API-originated purchases, Blooio also emails your organization's admins the result. Always verify the webhook signature before trusting a payload.
Removing a line
Unsubscribe an owned line with DELETE /channels/{channel}. The {channel} path segment accepts either the E.164 number or the ch_ channel id. A reasons array is required — it's the same churn-reason handling as the dashboard unsubscribe button, and it emits a number.removed webhook.
curl -X DELETE 'https://api.blooio.com/v4/channels/%2B14155550142' \
-H 'Authorization: Bearer YOUR_BLOOIO_API_KEY' \
-H 'Content-Type: application/json' \
-d '{ "reasons": ["too_expensive", "no_longer_needed"] }'Try it{
"data": {
"phone_number": "+14155550142",
"channel_id": "ch_...",
"reasons": ["Too expensive", "No longer needed"]
}
}Full schema: Remove a number.
Plans
Pass the specific plan id when purchasing. Pricing shown is indicative — see the pricing page for current rates.
| Plan id | Family | Description |
|---|---|---|
shared_nc |
shared | Entry shared line (limited new conversations/day). One per organization. |
shared_com |
shared | Commercial shared line (higher new-conversation allowance). One per organization. |
dedicated_com |
dedicated | Dedicated commercial line. Buy several with quantity. |
dedicated_ent |
dedicated | Dedicated enterprise line with tiered volume pricing. |
inbound_basic |
inbound | Reply-only inbound line — outbound is gated by prior inbound history (see Number pools). |
Note
Shared plans are limited to a single line per organization. Sending
quantity> 1 on a shared plan returns409withcode: shared_number_limit.
Note
dedicatedandinboundplans charge a one-time $75 custom fee for each line Blooio staff set up by hand. That covers every line with a custom request and every requested area code or ZIP that isn't in stock. It appears on the invoice as the custom area code fee.
Errors
| Status | code |
Meaning |
|---|---|---|
403 |
feature_not_enabled |
Number Purchase API access hasn't been approved for the organization. |
402 |
no_payment_method |
Your credit doesn't cover the purchase and there's no saved card. Add one in billing. |
402 |
payment_failed |
The card was declined; nothing was provisioned. |
400 |
idempotency_key_required |
Purchases require an Idempotency-Key header. |
400 |
invalid_plan |
Unknown plan id (e.g. dedicated instead of dedicated_com). |
400 |
plan_sunset |
The plan is no longer available for new purchases. |
400 |
line_limit_exceeded |
Order exceeds your per-order line cap. Apply for an increase under Overview → Limits. |
400 |
invalid_custom_requests |
custom_requests isn't an array, or an entry isn't an object or null. |
400 |
custom_requests_exceed_quantity |
custom_requests has more entries than the order has lines (quantity). |
400 |
custom_requests_not_supported |
A custom request was sent on a shared plan. Use a dedicated or inbound plan. |
400 |
invalid_auto_bind |
auto_bind isn't a non-empty string. |
404 |
auto_bind_api_key_not_found |
No live API key in your organization has that api_key_id. |
409 |
shared_number_limit |
A shared plan is limited to one line per organization. |
409 |
auto_bind_exceeds_plan |
The plan has no number pools, and the order has more than one line or the key already owns a line on such a plan. |
Related
- Number provisioning — the same lifecycle as a core concept reference.
- Number pools — how a sender number is selected once you own lines.
- Webhooks and verifying signatures — subscribe to
number.purchase.*andnumber.removedevents. - Calling the API with OAuth tokens — provisioning lines on behalf of another organization.
- Reference: List available · Purchase · Get status · Remove.
- AgentPhone case study — provisioning a line per end customer at scale.