> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spenza.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Register a voice quality webhook

> Register an HTTPS endpoint to receive `voice.test.completed` when a line on your account calls a Spenza test number. Needs a **read-write** key.

- The URL must be `https`, on port 443, and publicly reachable — private, loopback and link-local addresses are refused.
- Up to **10** webhooks per account. Every active webhook receives every result.
- The response carries `signingSecret` **once**. Store it to verify `X-Spenza-Signature` on deliveries.




## OpenAPI

````yaml /openapi.yaml post /api/v1.1/voice-quality/webhooks
openapi: 3.1.0
info:
  title: Spenza Partner API
  version: 3.0.0
  summary: >-
    Partner API for SIMs, eSIMs, plans, billing, users, groups and numbers —
    v1.1 surface.
  description: >
    The **Spenza Partner API** lets you manage SIMs, eSIMs, plans,
    subscriptions,

    billing, users, phone numbers, teams and device/SIM/user groups
    programmatically.


    All endpoints follow one consistent standard:


    - **Auth** — exchange your API `key` + `secret` for a bearer token, then
    send
      `Authorization: Bearer <token>` on every request.
    - **Envelopes** — success responses are `{ success, data, meta? }`; errors
    are
      `{ success: false, error: { code, message, details? } }`.
    - **Pagination** — list endpoints accept `page` (1-indexed) and `pageSize`
      (max 100); totals are returned in `meta`.
    - **Idempotency** — many write endpoints honor an optional `Idempotency-Key`
      request header so a retried call replays the original result instead of
      repeating the side effect.
    - **Async operations** — a handful of endpoints (eSIM provisioning, port-in,
      the `/api/v3/...` purchase variants, top-up) return `202` (`201` for
      top-up) with a `transactionId` + `statusEndpoint` instead of the finished
      resource. Poll
      `GET /api/v3/transactions/{transactionId}` (no auth required) until the
      transaction reaches a terminal status.

    One resource — **port-in submission** — is a documented exception: it lives
    at

    `POST /api/v3/port-in`, requires an Admin-tier account role rather than just
    a

    valid bearer token, and is **not** on the standard envelope (see that
    operation's

    description for its exact response shape).
  contact:
    name: Spenza API Support
    email: support@spenza.com
    url: https://spenza.com
  license:
    name: Proprietary
    url: https://spenza.com/terms
  x-logo:
    url: https://spenza.com/logo.png
    altText: Spenza
servers:
  - url: https://api.spenza.com
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Authentication
    description: Exchange API credentials for a bearer token.
  - name: SIMs
    description: List, inspect, assign and manage SIMs.
  - name: Subscriptions
    description: Plan subscriptions attached to SIMs.
  - name: Plans & Catalog
    description: Purchasable plans and SIM products.
  - name: eSIM
    description: Provision eSIMs and fetch activation QR / install status.
  - name: Billing
    description: Invoices, credit balance and top-ups.
  - name: Users
    description: End users (employees) on your account that SIMs/devices are assigned to.
  - name: Orders & Transactions
    description: Order history and financial transactions.
  - name: Numbers & Port-in
    description: Check port-in eligibility and port existing numbers into Spenza.
  - name: Messaging
    description: Send outbound SMS from a provisioned number.
  - name: Webhooks
    description: Register endpoints for operator SMS/voice events and inspect deliveries.
  - name: Voice Quality
    description: >-
      Test a line's call quality: call a Spenza test number from a line on your
      account and receive a scored report on your webhook.
  - name: Notifications
    description: In-account notifications and delivery preferences.
  - name: Device Groups
    description: Organize devices into named groups with spend/data limits.
  - name: SIM Groups
    description: Organize SIMs into named groups with spend/data limits.
  - name: User Groups
    description: Organize users (departments) into named groups with spend/data limits.
  - name: Devices
    description: The device catalog (phones/tablets) and their assignment to users.
  - name: Team Members
    description: Admins who manage your Spenza account (distinct from Users/end-users).
  - name: Async Status
    description: Universal status poll for any asynchronous (v3) operation.
paths:
  /api/v1.1/voice-quality/webhooks:
    post:
      tags:
        - Voice Quality
      summary: Register a voice quality webhook
      description: >
        Register an HTTPS endpoint to receive `voice.test.completed` when a line
        on your account calls a Spenza test number. Needs a **read-write** key.


        - The URL must be `https`, on port 443, and publicly reachable —
        private, loopback and link-local addresses are refused.

        - Up to **10** webhooks per account. Every active webhook receives every
        result.

        - The response carries `signingSecret` **once**. Store it to verify
        `X-Spenza-Signature` on deliveries.
      operationId: registerVoiceQualityWebhook
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
              properties:
                url:
                  type: string
                  maxLength: 2048
                  description: HTTPS URL on port 443.
                  example: https://partner.example.com/hooks/voice-quality
                events:
                  type: array
                  description: >-
                    Events to receive. Only `voice.test.completed` exists today,
                    and it is the default.
                  items:
                    type: string
                    enum:
                      - voice.test.completed
                  default:
                    - voice.test.completed
                description:
                  type: string
                  maxLength: 200
                  example: Voice quality demo
            example:
              url: https://partner.example.com/hooks/voice-quality
              events:
                - voice.test.completed
              description: Voice quality demo
      responses:
        '201':
          description: Registered. `signingSecret` is shown only in this response.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/VoiceQualityWebhookCreated'
        '400':
          description: >-
            `VALIDATION_ERROR` — not HTTPS, not port 443, contains credentials,
            not publicly reachable, or an unknown event.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error:
                  code: VALIDATION_ERROR
                  message: Webhook URL must use https.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A read-only API key cannot register or delete webhooks.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error:
                  code: INSUFFICIENT_SCOPE
                  message: >-
                    Your API key doesn't have the required "voice-quality:write"
                    scope for this action.
        '409':
          description: >-
            `WEBHOOK_LIMIT_REACHED` — the account already has 10 voice quality
            webhooks.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error:
                  code: WEBHOOK_LIMIT_REACHED
                  message: Your account can have up to 10 voice quality webhooks.
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >
        Any string unique to this logical request. On a retry with the same key

        and the same request body, the original response is replayed verbatim

        (with an `Idempotency-Replayed: true` response header) instead of the

        action repeating. Reusing the key with a **different** body or on a

        different route returns `409 IDEMPOTENCY_KEY_REUSED`. A replay attempted

        while the original call is still in flight returns `409 CONFLICT`.

        Optional — omit it and the endpoint behaves exactly as it would
        otherwise.
      schema:
        type: string
        example: a1b2c3d4-idem-key-001
  schemas:
    VoiceQualityWebhookCreated:
      allOf:
        - $ref: '#/components/schemas/VoiceQualityWebhook'
        - type: object
          properties:
            signingSecret:
              type: string
              description: >-
                Secret for verifying `X-Spenza-Signature` on deliveries.
                **Returned only in this response** — store it now; it is never
                shown again.
              example: vqwhsec_3oZ1n0Qv5yJ8c2kTq6WfXbR4mA9sLd7eHgPuVi0NxY
    ErrorEnvelope:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              example: SIM_NOT_FOUND
            message:
              type: string
              example: We couldn't find a SIM with that ICCID on your account.
            details:
              description: >-
                Present on some 4xx errors (for example a list of missing
                fields). Always absent on 5xx.
              type: object
    VoiceQualityWebhook:
      type: object
      properties:
        id:
          type: string
          description: Webhook id (`vqwh_` + 16 characters).
          example: vqwh_8Kd2mQ7xLp4Rt9Za
        url:
          type: string
          description: HTTPS URL on port 443 that receives results.
          example: https://partner.example.com/hooks/voice-quality
        description:
          type:
            - string
            - 'null'
          example: Voice quality demo
        events:
          type: array
          items:
            type: string
            enum:
              - voice.test.completed
          example:
            - voice.test.completed
        status:
          type: string
          enum:
            - active
            - disabled
          example: active
        createdAt:
          type: string
          format: date-time
          example: '2026-10-07T12:20:11.000Z'
        updatedAt:
          type: string
          format: date-time
          example: '2026-10-07T12:20:11.000Z'
  responses:
    Unauthorized:
      description: Missing, malformed or expired bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            success: false
            error:
              code: UNAUTHORIZED
              message: Missing or invalid token.
    RateLimited:
      description: >
        Rate limit exceeded for this endpoint (or the account-wide default of
        120 req/min).

        Only `Retry-After` is present on this response — the `X-RateLimit-*`

        headers below are sent on successful (non-429) requests, not on the

        429 itself.
      headers:
        Retry-After:
          description: Seconds until you can retry.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            success: false
            error:
              code: RATE_LIMITED
              message: 'ThrottlerException: Too Many Requests'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Bearer token obtained from `POST /api/v1.1/auth/token`.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.