# API-first onboarding — white-label enrollment in your own admin

- Category: Accounts & onboarding
- Published: June 8, 2026 · Updated: June 11, 2026
- Canonical: https://numbers.online/docs/integrations/api-first-onboarding

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

| 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` |

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

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

```bash
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 `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.

```bash
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

```bash
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

```bash
curl https://numbers.online/api/v1/account/listings/+14155550142 \
  -H "Authorization: Bearer $NOL_KEY"
```
```jsonc
{
  "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

```bash
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:

```bash
curl -X PATCH https://numbers.online/api/v1/account \
  -H "Authorization: Bearer $NOL_KEY" -H "Content-Type: application/json" \
  -d '{ "name": "Acme Voice", "email": "ops@acme.example" }'
# → { "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 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) |

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

## Keys & references

Keys are self-service at https://numbers.online/docs — shown once, stored only as a hash.
Machine-readable schema: https://numbers.online/api/spec · all guides: https://numbers.online/md/docs/integrations

- Previous guide: [FCC robocall-mitigation evidence bundle](https://numbers.online/md/docs/integrations/fcc-evidence-bundle)

---

Numbers Online — community-powered phone-number trust signals. Every figure on this
page is a supplementary, low-confidence signal — advisory, not a verdict on any number
or its owner.

- Canonical page (HTML): https://numbers.online/docs/integrations/api-first-onboarding
- Report a call: https://numbers.online/report · Listed in error? Request removal: https://numbers.online/opt-out
- Machine guide to the phone-intelligence API: https://numbers.online/llms.txt · OpenAPI: https://numbers.online/api/spec
