NumbersOnline
API reference/Integration guides/API-first onboarding
Accounts & onboarding

API-first onboarding

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.

Example: an AI agent platform onboards two customer numbers

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:

  1. create a numbers.online account and store its nol_ key against your customer record,
  2. prove the customer controls the number over OTP,
  3. stage the profile (business details, or just a name) you collected in your form,
  4. open the $29/yr (business) or $9 one-time (personal) payment link in a tab,
  5. poll until the number reads 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 with POST /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.

What's in-admin vs. hosted

StepWhere it runs
Sign up, issue keyAPI — 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 logoAPI — 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, reportsAPI — GET /api/v1/account/listings/{e164} + /reports
Edit the live profile laterAPI — PUT /api/v1/account/listings/{e164}
Update account name/emailAPI — PATCH /api/v1/account

The onboarding sequence

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"

3 · Stage the profile

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):

FieldKindPublished?Notes
kindbothbusiness or personal; fixes the verification kind for a new draft (default business). Can't change once a draft exists.
dbabusinessyes (as the name)Display/“doing-business-as” name.
legal_namebusinessyesRegistered legal entity name.
einbusinessno — privateTax id. Stored for your records; never on any public surface.
jurisdictionbusinessno — privateState/country of registration.
formation_datebusinessyear onlye.g. 2019-04-01 → profile shows 2019.
industrybusinessyes
addressbusinessyes
websitebusinessyes
biobothyesShort description.
namepersonalno (kept private)Owner name; used for your records, not exposed publicly.
rolepersonalnoFree-text role/title.
marketing_opt_inbothnoBoolean; your consent capture.

Once a number is verified, the same PUT edits the live listing, but only these columns are editable: business → name, bio, website, industry, address; personal → name, bio. (ein/jurisdiction are write-once-private and not re-editable through the API.)

3b · Upload a logo

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 }

4 · Mint the payment link and open it

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.)

5 · Poll for the result, then show trust

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_grade and risk.score are 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.

Reports detail

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).

Update the account itself

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 vs. business — what you get

Personal ($9 one-time)Business ($29/yr)
Public profile pageNo — shows "verified & online", name not exposedYes — /business/{handle} with name, website, address, logo
Profile fields (name/url/tax id/address/logo)Not applicableYes (all shown on the public profile, tax/registration id included)
Trust gradeA-A
Reporting lanecrowdaccountable (first-party)

Notes & limits

  • Account-level key required for every endpoint here — identity and money are owner actions; tenant sub-keys are rejected.
  • You can only manage a number you proved{e164} must be OTP-bound to the calling account.
  • A free account can bind one number; binding additional numbers requires verifying a number first (the per-number fee keeps one account from cheaply claiming many numbers).
  • Each E.164 belongs to one account. A number already bound elsewhere is rejected at OTP confirm — so a customer's number can't be silently re-claimed by another integrator.
  • All of this is a supplementary signal, never an official or compliance determination.
Need a key? Get one self-service. It’s shown once and stored only as a hash. Browse the other guides, grab copy-paste artifacts on the integrations page, or read the full machine-readable schema at /api/spec.
FCC robocall-mitigation evidence bundle