Numbers Online Lookup API

Number lookup: line type, range carrier, CNAM, verstat, and a supplementary spam signal

Operations 3

GET /api/v1/lookup/{e164} Look up a single number #
POST /api/v1/lookup/batch Look up multiple numbers #
GET /api/v1/lookup/changes Reputation-change feed #

Documentation

Specifications

Other Resources

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/numbers-online:numbers-online-lookup-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

numbers-online-lookup-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Numbers Online Phone Intelligence Lookup API
  description: Phone number parsing, validation, and inbound caller-intelligence as a supplementary signal.
  version: 1.0.0
  contact:
    name: Phone Numbers Online
    url: https://numbers.online
servers:
- url: https://numbers.online
  description: Production server
- url: http://localhost:3000
  description: Development server
tags:
- name: Lookup
  description: 'Number lookup: line type, range carrier, CNAM, verstat, and a supplementary spam signal'
paths:
  /api/v1/lookup/{e164}:
    get:
      tags:
      - Lookup
      summary: Look up a single number
      description: 'Single-number lookup returning the uniform §2 response shape: deterministic parse fields (validity, formats, line type, range carrier, country) plus cache-aware CNAM, a normalized STIR/SHAKEN verstat, and a supplementary low-confidence spam signal. All enrichment is fail-open — a slow or failing supplier nulls that field rather than erroring. The shape is identical for known and unknown numbers (anti-enumeration). Invalid input returns **200** with `valid: false` and null fields (NOT 404) and is not billed. Requires an API key with the `lookup` use case. Billing (standard tier): `$0.004` when a fresh wholesale CNAM dip is performed, `$0.002` when served without one (CNAM cache hit, or no CNAM supplier configured). The free tier is not billed (rate-limited instead). Billed responses are returned with `Cache-Control: no-store` — the `cached` field and `max_cache_age` param are the cache contract.'
      operationId: lookupNumber
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: e164
        in: path
        required: true
        description: The number to look up, as a full E.164 string (`+14155552671`), its URL-encoded form (`%2B14155552671`), or a bare digit slug (`14155552671`).
        schema:
          type: string
        example: '+14155552671'
      - name: verstat
        in: query
        required: false
        description: STIR/SHAKEN verstat passthrough — a bare token (e.g. `TN-Validation-Passed`), a `verstat=...` parameter, or a full SIP/tel header value. Normalized to `verified` / `unverified` / `unknown` in the response. Absence of validation is NOT a failed validation (maps to `unknown`).
        schema:
          type: string
        example: TN-Validation-Passed
      - name: max_cache_age
        in: query
        required: false
        description: 'Operator TTL control: the maximum acceptable CNAM cache age in seconds. A cached value older than this triggers a fresh wholesale dip; `0` forces a fresh dip (which is billed at the `$0.004` rate).'
        schema:
          type: integer
          minimum: 0
        example: 86400
      - name: Idempotency-Key
        in: header
        required: false
        description: Optional client-supplied request id for at-most-once billing on retries. Repeated requests with the same key are not double-billed.
        schema:
          type: string
      responses:
        '200':
          description: Lookup result (uniform shape for valid, invalid, known, and unknown numbers).
          headers:
            Cache-Control:
              description: Always `no-store` — the response is per-request and billed.
              schema:
                type: string
                example: no-store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LookupResponse'
        '400':
          description: Malformed path (not a usable E.164 number)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          $ref: '#/components/responses/InsufficientBalance'
        '429':
          $ref: '#/components/responses/RateLimited'
  /api/v1/lookup/batch:
    post:
      tags:
      - Lookup
      summary: Look up multiple numbers
      description: 'Bulk number lookup for list processing. Submit up to 100 numbers; results are returned in input order, each as the same shape as the single lookup. This is a list-processing surface, not a call-path surface — for large batches the supplier dips run with bounded concurrency and can take seconds. Requires an API key with the `lookup` use case. Billing (standard tier) is per VALID number, split by what was delivered: `$0.004` for each number that triggered a fresh wholesale CNAM dip and `$0.002` for each served without one; invalid numbers are free. The free tier is not billed (rate-limited instead). Use the `Idempotency-Key` header for at-most-once billing on retries.'
      operationId: lookupNumberBatch
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: Idempotency-Key
        in: header
        required: false
        description: Optional client-supplied request id for at-most-once billing on retries.
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - numbers
              properties:
                numbers:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  maxItems: 100
                  description: Numbers to look up (E.164 recommended). Maximum 100 per request.
                  example:
                  - '+14155552671'
                  - '+442071234567'
                verstat:
                  type: string
                  description: STIR/SHAKEN verstat passthrough applied to every number in the batch. Same accepted forms as the single-lookup query param.
                  example: TN-Validation-Passed
                max_cache_age:
                  type: integer
                  minimum: 0
                  description: 'Operator TTL control: maximum acceptable CNAM cache age in seconds; `0` forces a fresh dip.'
                  example: 86400
      responses:
        '200':
          description: Per-number lookup results plus a batch billing summary.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LookupBatchResponse'
        '400':
          description: Invalid request (missing/empty `numbers`, over 100, or malformed JSON)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          $ref: '#/components/responses/InsufficientBalance'
        '429':
          $ref: '#/components/responses/RateLimited'
  /api/v1/lookup/changes:
    get:
      tags:
      - Lookup
      summary: Reputation-change feed
      description: 'Poll the numbers whose scraped-intel rollup was updated since a cursor, so cached lookups can be refreshed when the underlying intel moves instead of on a blind timer. Returns up to `limit` entries ordered by update time (ascending) plus a `cursor` — pass it as the next request''s `since`; with no `since`, returns the last hour. Two caveats to build against: (1) an entry means the number''s intel row was re-written by ingestion, which includes re-observations that left every value unchanged — treat it as a refresh hint, not proof of movement; (2) ingest batches stamp many rows with one identical update timestamp and the cursor is a strict greater-than, so a page boundary landing inside such a batch skips its remaining same-timestamp rows — use `limit=1000` (the maximum) so pages rarely split a batch. The values are the intel layer''s own signal, a re-dip HINT: the authoritative blended score is still `GET /api/v1/lookup/{e164}`, so the intended loop is poll changes → re-dip the numbers you care about, while still honoring the Terms §7 caching bounds (refresh or drop cached responses within 30 days even when no entry arrives). Not separately metered — it rides the account''s normal API access and per-key rate limits. Requires the `lookup` use case.'
      operationId: lookupChanges
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      parameters:
      - name: since
        in: query
        required: false
        description: ISO-8601 cursor — use the previous response's `cursor`. Defaults to one hour ago.
        schema:
          type: string
          format: date-time
        example: '2026-08-01T00:00:00.000Z'
      - name: limit
        in: query
        required: false
        description: Maximum changes per page (default 100, max 1000).
        schema:
          type: integer
          minimum: 1
          maximum: 1000
          default: 100
      responses:
        '200':
          description: Numbers whose intel reputation changed since the cursor, oldest change first.
          headers:
            Cache-Control:
              description: Always `no-store` — the feed is a live cursor read.
              schema:
                type: string
                example: no-store
          content:
            application/json:
              schema:
                type: object
                properties:
                  since:
                    type: string
                    format: date-time
                    description: The cursor this page was read from.
                  cursor:
                    type: string
                    format: date-time
                    description: The max change time in this page — pass as the next request's `since`.
                  count:
                    type: integer
                  has_more:
                    type: boolean
                    description: True when the page filled `limit`; poll again immediately with `cursor`. Prefer `limit=1000` so a page boundary rarely lands inside one ingest batch (see the endpoint description).
                  changes:
                    type: array
                    items:
                      type: object
                      properties:
                        e164:
                          type: string
                          example: '+14155551212'
                        risk_score:
                          type:
                          - integer
                          - 'null'
                          description: Intel-layer weighted risk 0–100 (higher = worse) — a supplementary re-dip hint, NOT the blended lookup score.
                        risk_level:
                          type: string
                          enum:
                          - low
                          - medium
                          - high
                          - unknown
                        top_category:
                          type:
                          - string
                          - 'null'
                        total_reports:
                          type: integer
                        has_verified_regulator:
                          type: boolean
                        last_observed_at:
                          type:
                          - string
                          - 'null'
                          format: date-time
                        changed_at:
                          type: string
                          format: date-time
        '400':
          description: Malformed `since` (must be ISO-8601 — use the previous response's `cursor`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          description: Change feed temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  responses:
    RateLimited:
      description: Per-key rate limit exceeded. Retry after the number of seconds in the Retry-After header.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InsufficientBalance:
      description: Standard-tier prepaid balance is exhausted. Top up (POST /api/v1/account/topup) to resume.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/InsufficientBalance'
  schemas:
    LookupResponse:
      type: object
      description: 'Uniform number-lookup result. The same shape is returned for valid, invalid, known, and unknown numbers (anti-enumeration). Invalid input yields `valid: false` with the deterministic fields (`e164`, `formatted`, `line_type`, `carrier`, `country`) null, `verstat` `unknown`, `confidence` `low`, and `spam_score` null. Enrichment fields (`cnam`, `spam_score`) are fail-open — they null out on a supplier/scoring error rather than failing the request.'
      properties:
        schema_version:
          type: string
          example: '2026-06-03'
          description: Shape version (date-stamped, unique per shape). Additive changes never bump it; remove/rename/retype does.
        e164:
          type:
          - string
          - 'null'
          description: Canonical E.164 number, or null when the input is invalid.
          example: '+14155552671'
        valid:
          type: boolean
          description: Whether the input is a valid number.
        formatted:
          type: object
          description: Display formats; both null when invalid.
          properties:
            national:
              type:
              - string
              - 'null'
              example: (415) 555-2671
            international:
              type:
              - string
              - 'null'
              example: +1 415-555-2671
        line_type:
          type:
          - string
          - 'null'
          description: Lowercased line type ('mobile', 'fixed_line', 'voip', …), or null when indeterminate.
        carrier:
          type:
          - string
          - 'null'
          description: Carrier of the number RANGE (original allocation, NOT porting-aware) — a supplementary signal.
        country:
          type:
          - string
          - 'null'
          description: ISO 3166-1 alpha-2 country code.
          example: US
        cnam:
          type:
          - string
          - 'null'
          description: 'Caller name (CNAM), or null when unavailable or not dipped. Privacy carve-out: the name of an individual who has verified their personal number on Numbers Online is never returned (same invariant as the Inbound API''s ''Verified & online'' rule).'
        verstat:
          type: string
          enum:
          - verified
          - unverified
          - unknown
          description: Normalized STIR/SHAKEN verstat. `unknown` when no validation was performed or none was supplied.
        spam_score:
          type:
          - integer
          - 'null'
          minimum: 1
          maximum: 99
          description: Supplementary low-confidence spam signal on a 1–99 scale (higher = riskier); null when no signal is available. FROZEN field name — `risk` is the disclosed read.
        risk:
          $ref: '#/components/schemas/RiskView'
        confidence:
          type: string
          enum:
          - low
          description: Confidence label for the supplementary signals — always `low`.
        cached:
          type: boolean
          description: True when CNAM was served from cache (no fresh supplier dip — billed at the cheaper rate).
        sources:
          type: object
          description: Per-field data provenance (resale transparency).
          properties:
            carrier:
              type:
              - string
              - 'null'
              enum:
              - number_range_allocation
              - null
              description: Provenance of the carrier field.
            cnam:
              type:
              - string
              - 'null'
              enum:
              - wholesale_cnam
              - cache
              - null
              description: Provenance of the CNAM field.
            spam_score:
              type:
              - string
              - 'null'
              description: Provenance of the spam signal (e.g. 'baseline_prior' or a '+'-joined basis list); null when no signal.
        as_of:
          type: string
          format: date-time
          description: Timestamp the lookup was assembled.
    LookupBatchResponse:
      type: object
      description: 'Bulk lookup result: one entry per submitted number (input order) plus a billing summary.'
      properties:
        schema_version:
          type: string
          example: '2026-06-12'
          description: Version of the batch ENVELOPE (results/summary wrapper); each per-number result carries its own schema_version.
        results:
          type: array
          items:
            $ref: '#/components/schemas/LookupResponse'
          description: Per-number lookup results, in the order the numbers were submitted.
        summary:
          type: object
          properties:
            total:
              type: integer
              description: Numbers submitted.
            valid:
              type: integer
              description: Numbers that parsed as valid (includes tenant-suppressed numbers, which are valid but not billed).
            invalid:
              type: integer
              description: Numbers that were invalid (not billed).
            suppressed:
              type: integer
              description: 'Valid numbers on the calling tenant''s suppression list: returned with deterministic fields only (no enrichment) and not billed.'
            billed_fresh_cnam:
              type: integer
              description: Valid numbers billed at $0.004 (fresh wholesale CNAM dip).
            billed_enriched:
              type: integer
              description: Valid numbers billed at $0.002 (cache hit or no CNAM supplier).
    Error:
      type: object
      properties:
        success:
          type: boolean
          example: false
          description: Legacy field emitted ONLY by the pre-v1 parse family (/api/parse, /api/parse/bulk, /api/countries). /v1 routes return only `error` — do not depend on `success` there.
        error:
          type: string
          description: Human-readable error message (prose — switch on `code`, not on this string).
        code:
          type: string
          enum:
          - missing_key
          - invalid_key
          - use_case_forbidden
          - rate_limited_key
          - rate_limited_pool
          - rate_limited_ip
          - signature_invalid
          - insufficient_balance
          - account_suspended
          - paid_verification_required
          - receipt_invalid
          description: 'Stable machine-readable error code (added 2026-06-12, additive — older errors may omit it). See the "Error codes" section in the API description for the full table. A valid key on the wrong use case returns 403 use_case_forbidden (not 401): re-authing will not fix a permissions problem.'
        retry_after_seconds:
          type: integer
          description: 'Present on 429s: seconds until the window resets (mirrors the Retry-After header).'
      required:
      - error
    InsufficientBalance:
      type: object
      description: 402 body returned when a standard-tier account has no remaining credit for a billed request. A SUSPENDED account instead returns 403 with code `account_suspended` and no top-up pointer (payment does not lift a suspension).
      properties:
        error:
          type: string
          example: Insufficient prepaid balance for this request.
        code:
          type: string
          enum:
          - insufficient_balance
          description: Stable machine-readable code (added 2026-06-12).
        balance_micros:
          type:
          - integer
          - 'null'
          description: Remaining balance in microdollars (may be 0 or null).
          example: 0
        topup:
          type: string
          description: How to add credit (prose; prefer the structured siblings).
          example: 'POST /api/v1/account/topup with {"amount_cents": 500} (minimum $5) to add credit.'
        topup_url:
          type: string
          example: /api/v1/account/topup
          description: Top-up endpoint path.
        topup_min_cents:
          type: integer
          example: 500
          description: Minimum top-up amount in cents.
    RiskView:
      type: object
      description: 'One risk vocabulary (added 2026-06-11, additive — no schema_version bumps): the same numeric signal as the surface''s legacy field, plus a band and the MODEL label that says which pipeline scored it. The legacy fields (`spam_score`, `risk_score`/`risk_level`) are frozen forever; this object is the disclosed, consistent read. A null score yields the uniform unknown shape ({score:null, level:"unknown", model:null}) on every branch (anti-enumeration; fail-open nulls are load-bearing). The numeric scales are deliberately NOT unified across models — `model` is what tells them apart. See "Risk models" in the spec intro.'
      properties:
        score:
          type:
          - integer
          - 'null'
          description: The risk score on the MODEL's own scale (1–99 for first_party_plus_restricted_sources; 0–100 for the others); null when no signal.
        level:
          type: string
          enum:
          - low
          - medium
          - high
          - unknown
          description: 'Shared banding: <40 low, <70 medium, ≥70 high; unknown when score is null. NOTE: the SBC default flag threshold is a separate policy knob (80) — level high does not automatically flag.'
        model:
          type:
          - string
          - 'null'
          enum:
          - first_party_plus_restricted_sources
          - first_party_plus_external
          - dial_structural
          - null
          description: Which scoring pipeline produced the score; null when score is null.
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key for authentication
    BearerAuth:
      type: http
      scheme: bearer
      description: Bearer token authentication
    CidQueryKeyAuth:
      type: apiKey
      in: query
      name: key
      description: API key in the `?key=` query param. Accepted by the header-less PBX endpoint GET /api/v1/cid/{number} and by the webhook adapters POST /api/v1/integrations/retell/inbound, POST /api/v1/integrations/vapi/tool, and POST /api/v1/sbc/redirect, whose upstream platforms set only a static webhook URL and cannot send an Authorization/X-API-Key header. The key can leak into access logs — use a dedicated, rotated key, and prefer header auth wherever the client supports it.