> ## 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 an upload

> Submit a list of phone numbers. Use `multipart/form-data` to upload a
CSV file (max 100 MB), or `application/json` to send phone numbers
directly. Either way, the upload requires at least 100 valid US/CA
phone numbers and at most 250,000.

Map a name column to enable **OptimalDial Identity**, which additionally
judges whether each number belongs to the contact named on that row.
Identity does not change what an upload costs.




## OpenAPI

````yaml https://api.optimaldial.com/openapi/v1/spec.yaml post /api/v1/uploads
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/uploads:
    post:
      tags:
        - uploads
      summary: Create an upload
      description: |
        Submit a list of phone numbers. Use `multipart/form-data` to upload a
        CSV file (max 100 MB), or `application/json` to send phone numbers
        directly. Either way, the upload requires at least 100 valid US/CA
        phone numbers and at most 250,000.

        Map a name column to enable **OptimalDial Identity**, which additionally
        judges whether each number belongs to the contact named on that row.
        Identity does not change what an upload costs.
      operationId: createUpload
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
                - phone_column
              properties:
                file:
                  type: string
                  format: binary
                  description: CSV file (≤100 MB, must include a header row).
                phone_column:
                  type: string
                  description: Name of the column containing phone numbers.
                skip_mobile_lookup:
                  type: boolean
                  default: false
                validation_tier:
                  type: string
                  enum:
                    - standard
                    - max
                  default: standard
                  description: >-
                    Validation tier. `standard` charges 1 credit/contact; `max`
                    is more thorough and charges 1.5 credits/contact (billed as
                    ceil(valid_count × 1.5)).
                filename_override:
                  type: string
                  description: Override the file's original name.
                full_name_column:
                  type: string
                  description: >-
                    Enables OptimalDial Identity. Name of the column holding the
                    contact's full name. Supply this OR both `first_name_column`
                    and `last_name_column`.
                first_name_column:
                  type: string
                  description: >-
                    Enables OptimalDial Identity when supplied together with
                    `last_name_column`. Supplying only one half returns 400.
                last_name_column:
                  type: string
                  description: >-
                    Enables OptimalDial Identity when supplied together with
                    `first_name_column`. Supplying only one half returns 400.
          application/json:
            schema:
              $ref: '#/components/schemas/UploadCreateJson'
            examples:
              phoneNumbers:
                summary: Flat phone-number list
                value:
                  filename: april-cohort.csv
                  phone_numbers:
                    - '+15551234567'
                    - '+15551234568'
              contacts:
                summary: Rich contact objects
                value:
                  filename: april-cohort.csv
                  contacts:
                    - phone: '+15551234567'
                      first_name: Alex
                      list: april
                    - phone: '+15551234568'
                      first_name: Bea
                      list: april
                  phone_column: phone
      responses:
        '200':
          description: Upload created, credits charged, ready for processing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Upload'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Insufficient credits.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: No active subscription on the organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '415':
          description: >-
            Unsupported Content-Type. Use multipart/form-data or
            application/json.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: Fewer than 100 valid phone numbers after server validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MinContactsError'
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  schemas:
    UploadCreateJson:
      type: object
      description: |
        Provide exactly one of `phone_numbers` (flat string array) OR
        `contacts` (rich objects). When using `contacts`, `phone_column`
        is required.
      properties:
        filename:
          type: string
          description: Used as `original_filename`. Defaults to a synthesized name.
        skip_mobile_lookup:
          type: boolean
          default: false
        validation_tier:
          type: string
          enum:
            - standard
            - max
          default: standard
          description: >-
            Validation tier. `standard` charges 1 credit/contact; `max` charges
            1.5 credits/contact (billed as ceil(valid_count × 1.5)).
        phone_numbers:
          type: array
          items:
            type: string
          minItems: 100
          maxItems: 250000
        contacts:
          type: array
          items:
            type: object
            additionalProperties: true
          minItems: 100
          maxItems: 250000
        phone_column:
          type: string
          description: Required when using `contacts`.
        full_name_column:
          type: string
          description: >-
            Enables OptimalDial Identity. The key on each contact object holding
            the contact's full name. Requires `contacts` — a flat
            `phone_numbers` array carries no name to match against.
        first_name_column:
          type: string
          description: Enables OptimalDial Identity together with `last_name_column`.
        last_name_column:
          type: string
          description: Enables OptimalDial Identity together with `first_name_column`.
    Upload:
      type: object
      required:
        - id
        - user_id
        - original_filename
        - status
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        organization_id:
          type:
            - string
            - 'null'
          format: uuid
        original_filename:
          type: string
        file_size_bytes:
          type:
            - integer
            - 'null'
        status:
          type: string
          description: |
            `cancelled` is a legacy value retained so historical uploads still
            parse — no new upload can enter it.
          enum:
            - pending_mapping
            - validating
            - validation_failed
            - awaiting_confirmation
            - ready_for_processing
            - processing
            - completed
            - failed
            - cancelled
        valid_row_count:
          type:
            - integer
            - 'null'
        invalid_row_count:
          type:
            - integer
            - 'null'
        credits_required:
          type:
            - integer
            - 'null'
        credits_charged:
          type:
            - integer
            - 'null'
        credits_refunded:
          type:
            - integer
            - 'null'
          description: >-
            Credits returned to the organization when OptimalDial refunds an
            upload.
        source:
          type:
            - string
            - 'null'
          enum:
            - web
            - api
            - api_pool
            - null
          description: |
            `web` for in-app uploads, `api` for direct `/api/v1/uploads` calls,
            `api_pool` for uploads synthesized from the Contacts API.
        api_key_id:
          type:
            - string
            - 'null'
          format: uuid
        skip_mobile_lookup:
          type:
            - boolean
            - 'null'
        validation_tier:
          type:
            - string
            - 'null'
          enum:
            - standard
            - max
            - null
          description: Validation tier used for this upload (`standard` or `max`).
        identity_enabled:
          type:
            - boolean
            - 'null'
          description: >-
            Whether OptimalDial Identity was requested for this upload, i.e. a
            name column was mapped at creation.
        result_summary:
          oneOf:
            - $ref: '#/components/schemas/ResultSummary'
            - type: 'null'
          description: >-
            Distribution of the results, populated once `status == "completed"`.
            Lets you see the shape of a list without downloading it.
        export_count:
          type: integer
          description: >-
            How many times these results have been downloaded, counting both the
            web app and this API.
        last_exported_at:
          type:
            - string
            - 'null'
          format: date-time
        processed_available:
          type: boolean
          description: >-
            `true` once the processed file can be downloaded. Storage keys are
            deliberately not exposed — use the download endpoints.
        processed_filtered_available:
          type: boolean
          description: >-
            `true` once the deprecated `download/processed-filtered` endpoint
            will succeed, whether from a stored file or generated on demand.
        error_message:
          type:
            - string
            - 'null'
        exceeded_daily_limit:
          type:
            - boolean
            - 'null'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ErrorEnvelope:
      type: object
      required:
        - detail
      properties:
        detail:
          oneOf:
            - type: string
            - type: object
    MinContactsError:
      type: object
      required:
        - detail
      properties:
        detail:
          type: object
          required:
            - error
            - min
            - got
            - message
          properties:
            error:
              type: string
              enum:
                - min_contacts_required
            min:
              type: integer
            got:
              type: integer
            message:
              type: string
    ResultSummary:
      type: object
      description: |
        Parsed distribution of a completed upload's results.

        `format` tells you which vocabulary keys `status_counts`, and therefore
        which values the `statuses` download filter accepts: `identity` for the
        nine-tier `OptimalDial_Status` vocabulary, `answer_intent` for the
        five-value one used when no name columns were mapped.
      properties:
        format:
          type: string
          enum:
            - identity
            - answer_intent
        total_rows:
          type: integer
        status_counts:
          type: object
          additionalProperties:
            type: integer
          example:
            1 - Likely Right-Party Answer: 303
            5 - Likely Right-Party Voicemail: 891
            8 - Likely Wrong Number: 463
        identity_counts:
          type: object
          additionalProperties:
            type: integer
          example:
            Very High: 344
            High: 846
            Low: 463
        has_answer_intent_column:
          type: boolean
        has_identity_column:
          type: boolean
    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:
    BadRequest:
      description: Invalid request.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Unauthorized:
      description: Missing or invalid API key.
      headers:
        WWW-Authenticate:
          schema:
            type: string
            example: Bearer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    PayloadTooLarge:
      description: CSV >100 MB or array >250,000 entries.
      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.

````