openapi: 3.1.0
info:
  title: SwitchID Verification API
  version: 2026-10-15.2
  description: |
    Phone-based identity evidence for US numbers. Phase 1 exposes one SKU,
    `core_phone`: carrier, line type, caller-ID name where one is registered,
    and a comparison against the person or business name you supply.

    Results are evidence, not decisions. A `name_match.state` of `unavailable`
    means no usable caller-ID name existed to compare; it is never a mismatch.
    `phone.rate_center` is the number's rate-center location, not the
    subscriber's address. `signals.voip_or_virtual` is descriptive; many
    legitimate businesses use VoIP.

    Test keys (`sid_test_...`) run against a deterministic sandbox and never
    reach a paid provider. Live keys (`sid_live_...`) perform real lookups and
    are metered.
servers:
  - url: https://www.switchlabs.dev/api/switchid/v1
security:
  - bearerKey: []
paths:
  /verify:
    post:
      summary: Verify a phone number against a supplied name
      operationId: verify
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          description: |
            Required. 1-128 printable characters, unique per logical request.
            Re-sending the same key with the same body returns the original
            result and is never billed twice. The same key with a different
            body returns 409.
            A stored failed/rejected outcome is terminal for this key. Network
            uncertainty must be retried with the same key, then inspected with
            GET /requests/{id}. A new key deliberately starts a new check only
            after a confirmed terminal nonbillable failure and its cause is
            resolved. Pending or pending_reconciliation requires polling or
            operator recovery; never use a fresh key to bypass it.
          schema: { type: string, minLength: 1, maxLength: 128 }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/VerifyRequest' }
            examples:
              person:
                value:
                  phone: "+12025550100"
                  name: "John Smith"
                  subject_type: person
                  reference_id: "APP-2026-000123"
              business:
                value:
                  phone: "(202) 555-0104"
                  name: "Acme Roofing LLC"
                  subject_type: business
      responses:
        '200':
          description: Completed check (or an explicit `unavailable` result for an unallocated number).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VerifyResult' }
        '202':
          description: The same Idempotency-Key is still being processed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Pending' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
        '422': { $ref: '#/components/responses/Error' }
        '429':
          description: Rate limit or tenant spend cap reached. Honour `Retry-After`.
          headers:
            Retry-After: { schema: { type: integer } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
        '500': { $ref: '#/components/responses/Error' }
        '502': { $ref: '#/components/responses/Error' }
        '503': { $ref: '#/components/responses/Error' }
  /requests/{id}:
    get:
      summary: Fetch a previous request by id
      operationId: getRequest
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Stored result, or a successfully retrieved failed/rejected/reconciliation resource. Inspect its status and error.
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: '#/components/schemas/VerifyResult' }
                  - { $ref: '#/components/schemas/StoredRequestFailure' }
        '202':
          description: Still pending.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Pending' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
        '500': { $ref: '#/components/responses/Error' }
        '503': { $ref: '#/components/responses/Error' }
  /usage:
    get:
      summary: Usage summary for the calling key's tenant and environment
      operationId: getUsage
      parameters:
        - in: query
          name: from
          schema: { type: string, format: date }
          description: Inclusive start date (UTC). Defaults to the first day of the month of `to` when only `to` is given, otherwise the first of the current month.
        - in: query
          name: to
          schema: { type: string, format: date }
          description: Inclusive end date (UTC). Defaults to today.
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UsageSummary' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
components:
  securitySchemes:
    bearerKey:
      type: http
      scheme: bearer
      description: "`Authorization: Bearer sid_live_...` or `sid_test_...`"
  responses:
    Error:
      description: Error envelope.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
  schemas:
    StoredRequestFailure:
      type: object
      required: [request_id, status, error, billing_disposition]
      properties:
        request_id: { type: string, format: uuid }
        status: { type: string, enum: [failed, rejected, pending_reconciliation] }
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
        billing_disposition: { type: [string, 'null'], enum: [billable, not_billable, pending_reconciliation, null] }
    VerifyRequest:
      type: object
      additionalProperties: false
      required: [phone, name, subject_type]
      properties:
        phone:
          type: string
          description: US number in any common format. Non-US numbers (including Canada and the Caribbean) return 422 `unsupported_country`.
        name:
          type: string
          maxLength: 200
          description: The person or business name you want compared with the registered caller-ID name.
        subject_type:
          type: string
          enum: [person, business]
        reference_id:
          type: string
          maxLength: 128
          description: Your own identifier. Stored and echoed back, never interpreted.
        add_ons:
          type: array
          items: { type: string }
          description: Phase 1 accepts only an empty list or `["core_phone"]`.
    Pending:
      type: object
      properties:
        request_id: { type: string, format: uuid }
        status: { type: string, enum: [pending] }
    ApiError:
      type: object
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - unauthorized
                - forbidden_scope
                - forbidden_sku
                - tenant_suspended
                - invalid_request
                - invalid_phone
                - unsupported_country
                - missing_idempotency_key
                - idempotency_conflict
                - rate_limited
                - spend_cap_reached
                - enforcement_unavailable
                - provider_unavailable
                - provider_unconfigured
                - ledger_unavailable
                - not_found
                - internal_error
            message: { type: string }
            request_id: { type: string, format: uuid }
            details: {}
    VerifyResult:
      type: object
      required: [request_id, environment, created_at, result_state, subject, checks, phone, caller_id, name_match, signals, provenance, billing, warnings]
      properties:
        request_id: { type: string, format: uuid }
        environment: { type: string, enum: [live, test] }
        created_at: { type: string, format: date-time }
        completed_at: { type: [string, 'null'], format: date-time }
        product_version: { type: string }
        rule_version: { type: string }
        result_state:
          type: string
          enum: [completed, unavailable, error]
          description: "`unavailable` is returned when the provider reports the number as not allocated or invalid; it is not billed."
        subject:
          type: object
          properties:
            phone_e164: { type: string }
            phone_national: { type: string }
            subject_type: { type: string, enum: [person, business] }
            supplied_name: { type: string }
            reference_id: { type: [string, 'null'] }
        checks:
          type: object
          properties:
            requested: { type: array, items: { type: string, enum: [core_phone] } }
            completed: { type: array, items: { type: string, enum: [core_phone] } }
        phone:
          type: object
          properties:
            valid: { type: [boolean, 'null'], description: Provider validity flag when returned. Not an activity or reachability signal. }
            country: { type: string, enum: [US] }
            line_type: { type: string, enum: [mobile, landline, voip, toll_free, premium, pager, unknown] }
            line_type_raw: { type: [string, 'null'] }
            carrier_name: { type: [string, 'null'] }
            carrier_type: { type: string, enum: [wireless, landline, voip, unknown] }
            ported: { type: [boolean, 'null'], description: Carrier porting. Not evidence that the subscriber changed. }
            rate_center:
              type: [object, 'null']
              description: Rate-center location of the number. Not the subscriber's address.
              properties:
                city: { type: [string, 'null'] }
                state: { type: [string, 'null'] }
        caller_id:
          type: object
          properties:
            state:
              type: string
              enum: [available, placeholder, unavailable, error]
              description: "`placeholder` means the carrier returned a generic string such as WIRELESS CALLER."
            name: { type: [string, 'null'] }
            error_code: { type: [string, 'null'] }
        name_match:
          type: object
          properties:
            state:
              type: string
              enum: [match, partial_match, mismatch, ambiguous, unavailable]
            score:
              type: [integer, 'null']
              minimum: 0
              maximum: 100
              description: Similarity evidence, not a probability that the person owns the number.
            reasons: { type: array, items: { type: string } }
        signals:
          type: object
          properties:
            voip_or_virtual: { type: [boolean, 'null'] }
            toll_free: { type: [boolean, 'null'] }
        provenance:
          type: array
          items:
            type: object
            properties:
              provider:
                type: string
                enum: [carrier_lookup, sandbox]
                description: Public source label. `carrier_lookup` is a live carrier data lookup; `sandbox` is the deterministic test provider used by test keys.
              sku: { type: string }
              cache_hit: { type: boolean }
              attempted_at: { type: string, format: date-time }
        billing:
          type: object
          properties:
            disposition:
              type: string
              enum: [billable, not_billable, pending_reconciliation]
              description: A completed check is billable even when no caller-ID name is on file. Provider or system failures, provider-reported invalid numbers, and a re-check of the same number within 24 hours (warning `free_recheck_within_24h`) are not billable.
            sku: { type: string }
            unit_price_microdollars: { type: integer, description: "$0.015 is 15000." }
            cache_hit: { type: boolean }
        warnings: { type: array, items: { type: string } }
    UsageSummary:
      type: object
      properties:
        tenant_id: { type: string, format: uuid }
        environment: { type: string, enum: [live, test] }
        period:
          type: object
          properties:
            from: { type: string, format: date-time }
            to: { type: string, format: date-time }
        by_sku:
          type: array
          items:
            type: object
            properties:
              sku: { type: string }
              billable_requests: { type: integer }
              cache_hits: { type: integer }
              amount_microdollars: { type: integer }
        total_amount_microdollars: { type: integer }
