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

# Create a webhook endpoint

> Registers an HTTPS URL to receive event deliveries. The URL is
ping-verified synchronously; creation fails with `400` if the
receiver does not respond `2xx` within 5 seconds.

The response includes `secret`, a 64-char hex string used for HMAC
signature verification. **It is returned only once.**




## OpenAPI

````yaml https://api.optimaldial.com/openapi/v1/spec.yaml post /api/v1/webhooks
openapi: 3.1.0
info:
  title: OptimalDial API
  version: '2026-08-19'
  summary: Submit phone-number lists, get them processed, receive webhooks.
  description: |
    The OptimalDial REST API lets you programmatically submit lists of phone
    numbers (CSV or JSON, 100–250,000 per request), download processed results,
    and receive HMAC-signed webhooks when uploads change state.

    Map a name column on an upload to enable **OptimalDial Identity**, which
    judges whether each number belongs to the contact named on that row and
    reports it alongside answer intent. Identity is in **public beta** — the
    output format is stable while we tune the thresholds, and feedback is very
    welcome at support@optimaldial.com.

    All endpoints require an API key as `Authorization: Bearer od_live_...`.
    Keys are minted in the in-app developer panel by an organization owner.

    See the human-readable docs at https://docs.optimaldial.com for
    quickstarts, the webhook signature verifier, and the changelog.
  contact:
    name: OptimalDial Support
    email: support@optimaldial.com
    url: https://docs.optimaldial.com
  license:
    name: Proprietary
servers:
  - url: https://api.optimaldial.com
    description: Production
security:
  - bearerAuth: []
tags:
  - name: uploads
    description: Submit, inspect, and download phone-number lists.
  - name: contacts
    description: >
      Single-contact API for per-row enrichment integrations. Available to

      every account with an API key.


      **Use `/api/v2/contacts`** — it mirrors every v1 route and additionally

      reports the nine-tier `OptimalDial_Status` and the OptimalDial Identity

      band.


      `/api/v1/contacts` is **legacy**: supported indefinitely with no removal

      planned, but its `optimaldial_status` reports the Answer Intent value
      only,

      so existing filters keep working untouched. Both versions operate on the

      same contacts — a contact created on either can be read on either, so

      moving to v2 needs no migration.

      Each `standard` contact is charged 1 credit; `max` contacts are billed so

      a batch totals `ceil(1.5 × count)` credits. Result is delivered via

      per-contact callback URL or registered webhook endpoints, and is also

      available via GET.
  - name: webhooks
    description: Register and manage webhook endpoints; inspect delivery history.
  - name: spam
    description: |
      Monitor your outbound numbers for spam/scam-likely flagging across the
      major US carriers (AT&T, T-Mobile, Verizon). Adding a number
      auto-initiates verification — required for the monitoring service
      to place test calls, not a security gate — and once the code is submitted
      OptimalDial re-tests daily, exposing per-carrier status, run history, and
      screenshots. Subscribe to the `spam.detected` and `number.verified`
      webhook events to be notified the moment a number flips to spam.
      Available to every account with an API key and an active subscription.
  - name: credits
    description: |
      Check your organization's current credit balance. Read-only and
      available to every account with an API key.
paths:
  /api/v1/webhooks:
    post:
      tags:
        - webhooks
      summary: Create a webhook endpoint
      description: |
        Registers an HTTPS URL to receive event deliveries. The URL is
        ping-verified synchronously; creation fails with `400` if the
        receiver does not respond `2xx` within 5 seconds.

        The response includes `secret`, a 64-char hex string used for HMAC
        signature verification. **It is returned only once.**
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEndpointCreate'
      responses:
        '200':
          description: Endpoint created and ping-verified.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpointCreateResponse'
        '400':
          description: Invalid URL, unknown event type, or ping verification failed.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ErrorEnvelope'
                  - $ref: '#/components/schemas/PingFailedError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  schemas:
    WebhookEndpointCreate:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          format: uri
          description: HTTPS URL on a publicly resolvable host.
        description:
          type: string
          maxLength: 500
        events:
          type: array
          items:
            type: string
            enum:
              - upload.created
              - upload.completed
              - upload.failed
              - contact.completed
              - contact.failed
              - spam.detected
              - number.verified
          default:
            - upload.completed
            - upload.failed
    WebhookEndpointCreateResponse:
      type: object
      required:
        - webhook
        - secret
      properties:
        webhook:
          $ref: '#/components/schemas/WebhookEndpoint'
        secret:
          type: string
          description: 64-char hex string. Returned only on creation.
          example: 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
    ErrorEnvelope:
      type: object
      required:
        - detail
      properties:
        detail:
          oneOf:
            - type: string
            - type: object
    PingFailedError:
      type: object
      required:
        - detail
      properties:
        detail:
          type: object
          required:
            - error
            - message
          properties:
            error:
              type: string
              enum:
                - ping_failed
            message:
              type: string
            status_code:
              type:
                - integer
                - 'null'
            underlying_error:
              type:
                - string
                - 'null'
    WebhookEndpoint:
      type: object
      required:
        - id
        - user_id
        - organization_id
        - url
        - events
        - is_active
        - consecutive_failures
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        organization_id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        description:
          type:
            - string
            - 'null'
          maxLength: 500
        events:
          type: array
          items:
            type: string
            enum:
              - upload.created
              - upload.completed
              - upload.failed
              - contact.completed
              - contact.failed
              - spam.detected
              - number.verified
        is_active:
          type: boolean
        verified_at:
          type:
            - string
            - 'null'
          format: date-time
        last_success_at:
          type:
            - string
            - 'null'
          format: date-time
        last_failure_at:
          type:
            - string
            - 'null'
          format: date-time
        consecutive_failures:
          type: integer
          minimum: 0
        disabled_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    RateLimitError:
      type: object
      required:
        - detail
      properties:
        detail:
          type: object
          required:
            - error
            - scope
            - limit
            - retry_after_seconds
          properties:
            error:
              type: string
              enum:
                - rate_limit_exceeded
            scope:
              type: string
              enum:
                - per_key
                - per_org
                - global
              description: >-
                Which limit was hit: the per-API-key cap, the per-organization
                cap, or the system-wide ceiling.
            limit:
              type: integer
              description: The per-minute limit for the scope that was exceeded.
            retry_after_seconds:
              type: integer
              description: >-
                Seconds to wait before retrying (also returned as the
                Retry-After header).
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      headers:
        WWW-Authenticate:
          schema:
            type: string
            example: Bearer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    RateLimited:
      description: >-
        A rate-limit bucket was exhausted (per-key, per-org, or the system-wide
        ceiling). See the `scope` field.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying.
        X-RateLimit-Limit:
          schema:
            type: integer
          description: The per-minute limit for the scope that was exceeded.
        X-RateLimit-Remaining:
          schema:
            type: integer
          description: Tokens remaining in that bucket (0 on a 429).
        X-RateLimit-Reset:
          schema:
            type: integer
          description: Seconds until at least one token is available again.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitError'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: od_live_xxxxxxxx
      description: >
        OptimalDial API key, prefixed `od_live_`, sent as `Authorization: Bearer
        ...`.

        Mint keys in the in-app developer panel; they are scoped to a single
        organization.

````