openapi: 3.2.0 info: title: Numbers Online Phone Intelligence Inbound 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: Inbound description: Inbound caller-intelligence lookup for operators, PBX, and softphones paths: /api/v1/inbound/lookup: post: tags: - Inbound summary: Inbound caller lookup description: 'Signed, privacy-safe caller intelligence as a supplementary signal for an inbound call/message event. Returns identity type, a risk score (0-100, higher = worse), evidence signals, and a recommended action. A verified individual returns only "Verified & online" — never a name. Requires an API key with the inbound_lookup use case. Billed per dip on the standard tier ($0.004); free on the free tier (rate-limited, and a high-risk caller is degraded from block_candidate to challenge_or_route). Anti-enumeration: a known and an unknown number return the same 200 shape (found vs no_record). Caller-ID-authentication inputs `verstat` and `attestation` are read at the top level: a supplied value modulates whether this number''s standing applies to THIS call (a verified call trusts our read; a failed validation may indicate spoofing of the number) and, when an auth signal is present, contributes to a corroboration-gated spoofing-prevalence signal accumulated across sources — a supplementary signal, never stamped onto the number''s stored standing; absence is not a failed validation. The optional `to` (the receiver''s own number) binds call-provenance. The remaining fields (destination_number, client_type, client_name, the nested stir_shaken object, call_id_hash, timestamp) are accepted but currently reserved and ignored.' operationId: inboundLookup security: - ApiKeyAuth: [] - BearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: - number properties: number: type: string example: '+14155551212' context: type: string enum: - inbound_voice - inbound_sms - inbound_waba verstat: type: string 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. Absence is not a failed validation. example: TN-Validation-Passed attestation: type: string enum: - A - B - C description: P-Attestation-Indicator level; an explicit A/B/C overrides verstat. to: type: string description: The receiver's own number (E.164). Binds call-provenance so a later spoofing report can be attributed; stored only as a hash. example: '+14155550100' destination_number: type: string description: Reserved; accepted but currently ignored. client_type: type: string description: Reserved; accepted but currently ignored. client_name: type: string description: Reserved; accepted but currently ignored. call_id_hash: type: string description: Reserved; accepted but currently ignored. timestamp: type: string format: date-time description: Reserved; accepted but currently ignored. responses: '200': description: Signed caller assessment content: application/json: schema: $ref: '#/components/schemas/InboundLookupResponse' '400': description: Invalid request 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' components: responses: 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' 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' schemas: InboundLookupResponse: type: object properties: schema_version: type: string example: '2026-05-31' result: type: string enum: - found - no_record number: type: string identity_type: type: string enum: - verified_business - verified_individual - unverified - unknown display_label: type: string description: Business name, or "Verified & online" for individuals — never a personal name. profile_url: type: - string - 'null' description: Business profile URL only; null for individuals and unknown. personal_details_exposed: type: boolean example: false risk_score: type: - integer - 'null' description: 0-100, higher = worse; null when identity_type is unknown. FROZEN field name — `risk` is the disclosed read. risk_level: type: string enum: - low - medium - high - unknown risk: $ref: '#/components/schemas/RiskView' signals: type: array items: type: string description: PII-free evidence labels. recommended_action: type: string enum: - allow - label - challenge_or_route - block_candidate - allow_with_default_policy ttl_seconds: type: integer receipt_id: type: string example: nol_rec_… response_signature: type: string description: '''ed25519:'', or ''unsigned'' when no signing key is configured.' 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.