white-label enrollment in your own admin
Run number onboarding, profile/identity management, verification payment, and trust monitoring
for your customers entirely from your own admin UI, using only a nol_ account key. Nothing
in this flow requires sending a person to a numbers.online page — except the Stripe-hosted card
form itself, which opens in a tab and returns automatically.
This is designed for platforms that manage many numbers on behalf of their own users (softphones, PBX fleets, AI voice/chat agents): one account per channel/customer, each onboarding its own number at its own tier under its own brand.
Your admin manages two customers. One is a plumbing company that wants a public verified business profile on its main line; the other is a solo consultant who just wants their personal number to read as "verified & online" on inbound calls. From inside your own UI, for each customer you:
nol_ key against your customer record,verified, then show the live trust grade, risk score, and reports
right inside your dashboard.No numbers.online page is ever shown to your customer except the Stripe card form. The rest is your brand, your UI.
Two independent axes. A number's verification level (unverified · personal $9 one-time · business $29/yr) is not the same thing as your API billing tier (free vs. prepaid
standard, topped up withPOST /api/v1/account/topup). Verifying a number does not top up API credit, and vice-versa. This page covers number verification + profile; top-ups are separate.
| Step | Where it runs |
|---|---|
| Sign up, issue key | API — POST /api/v1/account/signup |
| Prove number ownership (OTP) | API — POST /api/v1/account/verify-phone/start → /confirm |
| Stage business/personal profile (name, legal name, tax id, address, website, bio) | API — PUT /api/v1/account/listings/{e164} |
| Upload a logo | API — POST /api/v1/account/listings/{e164}/logo |
| Pay for verification ($9 / $29) | Stripe-hosted page, opened in a tab; publishes server-side on payment |
| See verified state, trust grade, live risk, reports | API — GET /api/v1/account/listings/{e164} + /reports |
| Edit the live profile later | API — PUT /api/v1/account/listings/{e164} |
| Update account name/email | API — PATCH /api/v1/account |
1. POST /api/v1/account/signup → nol_ key (once per channel)
2. POST /api/v1/account/verify-phone/start → OTP sent to the number
POST /api/v1/account/verify-phone/confirm → number bound to the account
3. PUT /api/v1/account/listings/{e164} → stage the profile (draft)
POST /api/v1/account/listings/{e164}/logo → attach a logo (business)
4. POST /api/v1/account/listings/{e164}/verify-checkout → { url }
→ open url in a new tab; customer pays on Stripe
→ numbers.online PUBLISHES the listing server-side (no return trip)
5. GET /api/v1/account/listings/{e164} → poll until state == "verified"
A partial PUT merges — send only the fields you have. kind fixes whether this number
verifies as business ($29/yr, public profile) or personal ($9, "verified & online", no
public profile fields). Tax id (ein) is stored privately and is never published.
curl -X PUT https://numbers.online/api/v1/account/listings/+14155550142 \
-H "Authorization: Bearer $NOL_KEY" -H "Content-Type: application/json" \
-d '{
"kind": "business",
"dba": "Acme Plumbing",
"legal_name": "Acme Plumbing LLC",
"ein": "12-3456789",
"formation_date": "2019-04-01",
"industry": "Home services",
"address": "1 Main St, Austin TX",
"website": "https://acme.example",
"bio": "Licensed plumbers, same-day service."
}'
# → { "ok": true, "state": "draft", "verification_id": "…", "kind": "business", "profile": {…} }
For the personal customer, set kind: "personal" and send just the name (no public profile is
published — a verified personal number reads as "verified & online", the name is never exposed):
curl -X PUT https://numbers.online/api/v1/account/listings/+14155550199 \
-H "Authorization: Bearer $NOL_KEY" -H "Content-Type: application/json" \
-d '{ "kind": "personal", "name": "Jordan Rivera", "role": "Independent consultant" }'
# → { "ok": true, "state": "draft", "verification_id": "…", "kind": "personal", "profile": {…} }
All PUT body fields (send only what you have; everything is optional, strings cap at 2000
chars):
| Field | Kind | Published? | Notes |
|---|---|---|---|
kind | both | — | business or personal; fixes the verification kind for a new draft (default business). Can't change once a draft exists. |
dba | business | yes (as the name) | Display/“doing-business-as” name. |
legal_name | business | yes | Registered legal entity name. |
ein | business | no — private | Tax id. Stored for your records; never on any public surface. |
jurisdiction | business | no — private | State/country of registration. |
formation_date | business | year only | e.g. 2019-04-01 → profile shows 2019. |
industry | business | yes | |
address | business | yes | |
website | business | yes | |
bio | both | yes | Short description. |
name | personal | no (kept private) | Owner name; used for your records, not exposed publicly. |
role | personal | no | Free-text role/title. |
marketing_opt_in | both | no | Boolean; your consent capture. |
Once a number is verified, the same
PUTedits the live listing, but only these columns are editable: business →name,bio,website,industry,address; personal →name,bio. (ein/jurisdictionare write-once-private and not re-editable through the API.)
Raw image bytes in the body; image/png · image/jpeg · image/webp, max 512 KB.
curl -X POST https://numbers.online/api/v1/account/listings/+14155550142/logo \
-H "Authorization: Bearer $NOL_KEY" -H "Content-Type: image/png" \
--data-binary @logo.png
# → { "ok": true, "media_id": "…", "logo_url": "https://numbers.online/api/media/…", "byte_size": 8123 }
curl -X POST https://numbers.online/api/v1/account/listings/+14155550142/verify-checkout \
-H "Authorization: Bearer $NOL_KEY"
# → { "ok": true, "provider": "stripe", "state": "pending", "url": "https://checkout.stripe.com/…" }
Open url in a new tab (the same pattern as a hosted OAuth/consent button). When the customer
pays, the Stripe webhook publishes the listing on our side — there is no page for them to
return to and no second API call for you to make. (In a dev environment with the dev payments
provider, this endpoint verifies instantly and returns the handle.)
curl https://numbers.online/api/v1/account/listings/+14155550142 \
-H "Authorization: Bearer $NOL_KEY"
{
"e164": "+14155550142",
"state": "verified",
"trust_grade": "A", // static verification badge, set at verification
"risk": { // LIVE community signal — moves as the number is reported
"score": 12, // 0–100, higher = more risk (a supplementary signal)
"report_count": 0, "reports_last_30d": 0,
"review_total": 4, "review_positive": 4
},
"listing": { "handle": "acme-plumbing-9f3a", "name": "Acme Plumbing", "website": "https://acme.example",
"logo_url": "/api/media/…", "verified_since": "2026-06-08T…", "next_billing": "2027-06-08T…" },
"draft": null
}
trust_gradeandrisk.scoreare different things. The grade is a fixed badge proving the number was verified. The score is live community reputation — a verified business can still accrue risk if it gets reported. Show both; act on the score.
curl "https://numbers.online/api/v1/account/listings/+14155550142/reports?limit=50" \
-H "Authorization: Bearer $NOL_KEY"
# → { summary: {…}, reports: [{ created_at, tags, rating, body, reporter, lane, provenance }], reviews: [...] }
Anonymous reporters are redacted (reporter: null).
The account's own name and contact email (separate from any number's profile) are editable too:
curl -X PATCH https://numbers.online/api/v1/account \
-H "Authorization: Bearer $NOL_KEY" -H "Content-Type: application/json" \
-d '{ "name": "Acme Voice", "email": "[email protected]" }'
# → { "ok": true, "account": { … } }
Send either field or both. Email is optional and used only for renewal reminders — it is not an identity and is not required to be unique across accounts.
| Personal ($9 one-time) | Business ($29/yr) | |
|---|---|---|
| Public profile page | No — shows "verified & online", name not exposed | Yes — /business/{handle} with name, website, address, logo |
| Profile fields (name/url/tax id/address/logo) | Not applicable | Yes (all shown on the public profile, tax/registration id included) |
| Trust grade | A- | A |
| Reporting lane | crowd | accountable (first-party) |
{e164} must be OTP-bound to the calling account.