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

# Create an upload

> Creates an upload for a fan's voice note or photo, with a link to send the file to. It's the first step in sending one, before Send a message. Send a new Idempotency-Key with each upload, so a retry never creates a second one.



## OpenAPI

````yaml /experiences/api-reference/experiences.openapi.yaml post /fan-voices/{fan_voice_id}/uploads
openapi: 3.1.0
info:
  title: 'Dizplai API: Experiences'
  version: 1.0.0
  summary: Read polls, Fan Voices and collections, and let fans vote and send messages.
  description: >
    Show polls and Fan Voices in your own apps and sites, and let fans take
    part: read experiences and collections, send

    fans' votes and messages, and read back a fan's own vote and messages.


    **Headers.** Send `x-api-key`, your organisation's API key, on every
    request. When a fan takes part, or reads

    their own vote or messages, add their token from your own login as
    `Authorization: Bearer <token>`.


    **Names.** An experience is one thing fans take part in: a poll or a Fan
    Voice. A collection groups experiences for a

    match or show, and an experience can be in many collections. Every ID is a
    UUID: store it in a UUID column or as

    text, and never parse it.


    **Conventions.** snake_case fields; `after` and `limit` paging (50 by
    default, 100 at most) with `{items, next}`;

    errors are RFC 9457 problems with a stable `code` and a `request_id`, and
    every response carries `x-request-id`.

    Types, statuses and codes can gain values within v1, so ignore one you don't
    know.


    **Refreshing.** Read an open poll every 10 seconds and lists every 30
    seconds, and wait as long as `Retry-After`

    says after a `429` or `503`. The Experiences guide has the details.
  contact:
    name: Dizplai
servers:
  - url: https://api.dizplai.com/v1
    description: The Dizplai API.
security:
  - apiKey: []
tags:
  - name: Experiences
    description: >-
      Everything fans can take part in, in one list: build a match or show
      screen from it, drawing each experience by its type.
  - name: Collections
    description: >-
      Groups of experiences, one for each match or show, to organise your
      screens. An experience can be in many collections, in its own order in
      each.
  - name: Polls
    description: >-
      A poll asks fans one question with a short list of answers. Read polls and
      their results, send a fan's vote, and read back the vote they made.
  - name: Fan Voices
    description: >-
      A Fan Voice asks fans to reply in their own words, with text, a voice note
      or a photo. Read a Fan Voice, send a fan's message, and show the fan what
      happened to it. Fans never see each other's messages.
paths:
  /fan-voices/{fan_voice_id}/uploads:
    parameters:
      - name: fan_voice_id
        in: path
        required: true
        description: The Fan Voice's ID, from a list of experiences or Fan Voices.
        schema:
          $ref: '#/components/schemas/FanVoiceId'
    post:
      tags:
        - Fan Voices
      summary: Create an upload
      description: >-
        Creates an upload for a fan's voice note or photo, with a link to send
        the file to. It's the first step in sending one, before Send a message.
        Send a new Idempotency-Key with each upload, so a retry never creates a
        second one.
      operationId: createFanVoiceUpload
      parameters:
        - $ref: '#/components/parameters/UploadIdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FanVoiceUploadRequest'
            examples:
              example:
                $ref: '#/components/examples/FanVoiceUploadRequest'
      responses:
        '201':
          description: >-
            Upload the file to url, then send the message with this upload_id
            within an hour, or the upload is deleted. If the connection drops,
            ask again with the same Idempotency-Key and body within 5 minutes,
            while the link works: you get the same upload and link. After that
            the key is forgotten, and asking again creates a new upload. An
            upload that's never used is deleted, and only a message the fan
            sends counts towards their limit.
          headers:
            x-request-id:
              $ref: '#/components/headers/RequestId'
            Cache-Control:
              $ref: '#/components/headers/NoStore'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FanVoiceUploadTicket'
              examples:
                example:
                  $ref: '#/components/examples/FanVoiceUploadTicket'
        '400':
          description: >-
            The body is invalid or has no Idempotency-Key (invalid_request), the
            Fan Voice isn't live (not_open), or it doesn't take this kind, or
            the type or size is outside the limits (media_not_accepted).
          headers:
            x-request-id:
              $ref: '#/components/headers/RequestId'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
              examples:
                invalid_request:
                  summary: invalid_request
                  value:
                    type: https://api.dizplai.com/problems/invalid-request
                    code: invalid_request
                    title: Invalid request
                    status: 400
                    detail: content_type is required.
                    request_id: 01JBX8P2R4T6V8X0Z2B4D6F8H0
                not_open:
                  summary: not_open
                  value:
                    type: https://api.dizplai.com/problems/not-open
                    code: not_open
                    title: Not open
                    status: 400
                    detail: 'This Fan Voice isn''t taking messages: it has closed.'
                    request_id: 01JBX8P2R4T6V8X0Z2B4D6F8H0
                media_not_accepted:
                  summary: media_not_accepted
                  value:
                    type: https://api.dizplai.com/problems/media-not-accepted
                    code: media_not_accepted
                    title: Media not accepted
                    status: 400
                    detail: This Fan Voice takes text only.
                    request_id: 01JBX8P2R4T6V8X0Z2B4D6F8H0
        '401':
          $ref: '#/components/responses/FanUnauthorised'
        '403':
          description: >-
            A guest, since voice notes and photos are for signed-in fans
            (sign_in_required); a fan blocked from this Fan Voice (fan_blocked);
            or a fan who has used up this Fan Voice's messages of this kind
            (message_limit_reached). Retrying won't help.
          headers:
            x-request-id:
              $ref: '#/components/headers/RequestId'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
              examples:
                sign_in_required:
                  summary: sign_in_required
                  value:
                    type: https://api.dizplai.com/problems/sign-in-required
                    code: sign_in_required
                    title: Sign-in required
                    status: 403
                    detail: Voice notes and photos are for signed-in fans.
                    request_id: 01JBX8P2R4T6V8X0Z2B4D6F8H0
                fan_blocked:
                  summary: fan_blocked
                  value:
                    type: https://api.dizplai.com/problems/fan-blocked
                    code: fan_blocked
                    title: Fan blocked
                    status: 403
                    detail: This fan can't send messages to this Fan Voice.
                    request_id: 01JBX8P2R4T6V8X0Z2B4D6F8H0
                message_limit_reached:
                  summary: message_limit_reached
                  value:
                    type: https://api.dizplai.com/problems/message-limit-reached
                    code: message_limit_reached
                    title: Message limit reached
                    status: 403
                    detail: This fan has sent 3 of 3 text messages to this Fan Voice.
                    request_id: 01JBX8P2R4T6V8X0Z2B4D6F8H0
        '404':
          $ref: '#/components/responses/FanVoiceNotFound'
        '409':
          $ref: '#/components/responses/IdempotencyConflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          $ref: '#/components/responses/MessagesUnavailable'
      security:
        - apiKey: []
          fanToken: []
components:
  schemas:
    FanVoiceId:
      type: string
      description: A Fan Voice's ID, a UUID.
      format: uuid
      examples:
        - 0192fa7b-a1a5-7ae6-8820-782ce84e0f49
    FanVoiceUploadRequest:
      description: >-
        What the fan wants to upload for a Fan Voice message: its kind, file
        type and size. Fixed limits: a voice note is audio/mp4 (AAC, what phones
        record) or audio/webm (Opus), up to 5 MB and 60 seconds; a photo is
        image/jpeg, image/png, image/heic or image/webp, up to 10 MB.
      oneOf:
        - title: Voice note
          type: object
          required:
            - kind
            - content_type
            - bytes
          additionalProperties: false
          properties:
            kind:
              type: string
              enum:
                - voice
            content_type:
              type: string
              description: The file's type. Upload it with the same Content-Type.
              enum:
                - audio/mp4
                - audio/webm
            bytes:
              type: integer
              minimum: 1
              maximum: 5242880
              description: The file's size in bytes, up to 5 MB.
        - title: Photo
          type: object
          required:
            - kind
            - content_type
            - bytes
          additionalProperties: false
          properties:
            kind:
              type: string
              enum:
                - image
            content_type:
              type: string
              description: The file's type. Upload it with the same Content-Type.
              enum:
                - image/jpeg
                - image/png
                - image/heic
                - image/webp
            bytes:
              type: integer
              minimum: 1
              maximum: 10485760
              description: The file's size in bytes, up to 10 MB.
    FanVoiceUploadTicket:
      type: object
      required:
        - upload_id
        - url
        - expires_at
      description: >-
        Where to upload the file, and the upload_id to send with the message.
        PUT the file to url before expires_at, with Content-Type set to the
        content_type and Content-Length to the bytes you asked for; anything
        else is rejected. Then send the message with this upload_id within an
        hour, or the upload is deleted. Nothing serves the file until a
        moderator approves it.
      properties:
        upload_id:
          $ref: '#/components/schemas/UploadId'
        url:
          type: string
          format: uri
          description: >-
            Already signed. PUT the file here with only Content-Type and
            Content-Length, no x-api-key or Authorization. Don't log it.
        expires_at:
          $ref: '#/components/schemas/Timestamp'
          description: 5 minutes after the request.
    Problem:
      type: object
      required:
        - type
        - code
        - title
        - status
        - request_id
      description: >-
        What went wrong, in the same shape for every error: an RFC 9457 problem
        with a stable code. Act on code, which never changes meaning within a
        version, and on the HTTP status for a code you don't know. Quote
        request_id to support.
      properties:
        type:
          type: string
          format: uri-reference
          description: >-
            One URL per code: https://api.dizplai.com/problems/ followed by the
            code with hyphens, such as
            https://api.dizplai.com/problems/unknown-option for unknown_option.
        code:
          type: string
          x-extensible-enum:
            - invalid_request
            - unknown_option
            - not_open
            - media_not_accepted
            - invalid_upload
            - invalid_api_key
            - invalid_fan_token
            - forbidden
            - fan_blocked
            - sign_in_required
            - message_limit_reached
            - not_found
            - idempotency_conflict
            - rate_limited
            - unavailable
          description: >
            More codes may be added: for one you don't know, act on the HTTP
            status.


            | code | Status | Retry? | Meaning |

            |---|---|---|---|

            | invalid_request | 400 | No | The body or a parameter is invalid,
            or a required header such as Idempotency-Key is missing. |

            | unknown_option | 400 | No; read the poll again | An option ID the
            poll doesn't have. |

            | not_open | 400 | No; read the experience again | The experience
            isn't taking votes or messages: it isn't live yet, or votes have
            stopped. |

            | media_not_accepted | 400 | No | The Fan Voice doesn't take this
            kind of message, or the file's type or size is outside the limits. |

            | invalid_upload | 400 | No; upload again | The upload_id is
            unknown, already used or another fan's, or the file was never
            uploaded or its type or size differs from what you asked for. |

            | invalid_api_key | 401 | No | A missing or invalid x-api-key. |

            | invalid_fan_token | 401 | Once, with a fresh token | A missing,
            invalid or expired fan token, or one from another organisation's
            login. It carries WWW-Authenticate. |

            | forbidden | 403 | No | Your API key isn't allowed to make this
            request. |

            | fan_blocked | 403 | No | The fan is blocked from this Fan Voice. |

            | sign_in_required | 403 | No; ask the fan to sign in | A guest on
            an experience that needs a signed-in fan, or a guest's voice note or
            photo. |

            | message_limit_reached | 403 | No | The fan has used up this Fan
            Voice's messages of this kind. |

            | not_found | 404 | No | Not found, or not yours. |

            | idempotency_conflict | 409 | No | An Idempotency-Key reused with a
            different body while it's kept: 24 hours, or 5 minutes for an
            upload. |

            | rate_limited | 429 | After Retry-After | Too many requests. |

            | unavailable | 503 | After Retry-After | Votes or messages can't be
            taken right now. |
        title:
          type: string
          description: A short title for the code, such as Not open. It doesn't change.
        status:
          type: integer
          description: The HTTP status code.
          minimum: 400
          maximum: 599
        detail:
          type: string
          description: >-
            What went wrong, in plain English. It can change, so don't act on
            it.
        request_id:
          type: string
          description: Matches the x-request-id response header.
        errors:
          type: array
          description: Field-level validation errors, with invalid_request.
          items:
            type: object
            required:
              - pointer
              - message
            properties:
              pointer:
                type: string
                description: A JSON Pointer to the field.
              message:
                type: string
                description: What's wrong with that field.
    UploadId:
      type: string
      description: >-
        A fan's voice note or photo, uploaded for a Fan Voice message. Created
        when the fan asks to upload, used in one message only, and deleted if no
        message uses it within an hour.
      format: uuid
      examples:
        - 0192fa8a-9346-78b7-9664-3cb0e5caf78d
    Timestamp:
      type: string
      format: date-time
      description: A date and time, RFC 3339, in UTC.
  parameters:
    UploadIdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        format: uuid
      example: 9a5c0f37-e8d2-4b6a-91f4-7c3e2b8d5a10
      description: >-
        Required. A new UUID for each upload. Keys are kept for each fan for 5
        minutes, as long as the link: the same key and body from the same fan
        within that time answers as the first time, with the same upload and
        link, so a retry never creates a second upload; with a different body,
        409 (idempotency_conflict). After 5 minutes, the same key creates a new
        upload.
  examples:
    FanVoiceUploadRequest:
      value:
        kind: voice
        content_type: audio/mp4
        bytes: 1843200
    FanVoiceUploadTicket:
      value:
        upload_id: 0192fa8a-9346-78b7-9664-3cb0e5caf78d
        url: >-
          https://uploads.example.com/0192fa8a-9346-78b7-9664-3cb0e5caf78d?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=EXAMPLEKEY%2F20261121%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20261121T204140Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-length%3Bcontent-type%3Bhost&X-Amz-Security-Token=EXAMPLE-SESSION-TOKEN&X-Amz-Signature=3f1c0d2e9b8a7f6e5d4c3b2a19087f6e5d4c3b2a19087f6e5d4c3b2a19087f6e
        expires_at: '2026-11-21T20:46:40Z'
  headers:
    RequestId:
      description: The request's ID. Quote it to support.
      schema:
        type: string
    NoStore:
      description: 'no-store: the answer belongs to one fan, so no cache may keep it.'
      schema:
        type: string
        const: no-store
    WwwAuthenticate:
      description: >-
        With invalid_fan_token only: Bearer error="invalid_token", with an
        error_description.
      schema:
        type: string
    RetryAfter:
      description: >-
        How long to wait before retrying, as a whole number of seconds. Sent
        with every 429 and 503.
      schema:
        type: integer
        minimum: 0
  responses:
    FanUnauthorised:
      description: >-
        A missing or invalid x-api-key (invalid_api_key), or a missing, invalid
        or expired fan token, or one from another organisation's login
        (invalid_fan_token). Only invalid_fan_token carries WWW-Authenticate:
        refresh the fan's token and retry once.
      headers:
        x-request-id:
          $ref: '#/components/headers/RequestId'
        WWW-Authenticate:
          $ref: '#/components/headers/WwwAuthenticate'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            invalid_fan_token:
              summary: invalid_fan_token
              value:
                type: https://api.dizplai.com/problems/invalid-fan-token
                code: invalid_fan_token
                title: Invalid fan token
                status: 401
                detail: The fan's token has expired.
                request_id: 01JBX8P2R4T6V8X0Z2B4D6F8H0
            invalid_api_key:
              summary: invalid_api_key
              value:
                type: https://api.dizplai.com/problems/invalid-api-key
                code: invalid_api_key
                title: Invalid API key
                status: 401
                detail: The x-api-key isn't recognised.
                request_id: 01JBX8P2R4T6V8X0Z2B4D6F8H0
    FanVoiceNotFound:
      description: >-
        The Fan Voice doesn't exist, isn't yours, or was taken down or deleted
        (not_found).
      headers:
        x-request-id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            not_found:
              summary: not_found
              value:
                type: https://api.dizplai.com/problems/not-found
                code: not_found
                title: Not found
                status: 404
                detail: No such Fan Voice.
                request_id: 01JBX8P2R4T6V8X0Z2B4D6F8H0
    IdempotencyConflict:
      description: >-
        The Idempotency-Key was used with a different body while it was kept, 24
        hours or 5 minutes for an upload (idempotency_conflict).
      headers:
        x-request-id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            idempotency_conflict:
              summary: idempotency_conflict
              value:
                type: https://api.dizplai.com/problems/idempotency-conflict
                code: idempotency_conflict
                title: Idempotency conflict
                status: 409
                detail: This Idempotency-Key was used with a different body.
                request_id: 01JBX8P2R4T6V8X0Z2B4D6F8H0
    TooManyRequests:
      description: >-
        Too many requests (rate_limited). Wait the number of seconds in
        Retry-After, then retry.
      headers:
        x-request-id:
          $ref: '#/components/headers/RequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            rate_limited:
              summary: rate_limited
              value:
                type: https://api.dizplai.com/problems/rate-limited
                code: rate_limited
                title: Rate limited
                status: 429
                detail: Too many requests. Retry after 2 seconds.
                request_id: 01JBX8P2R4T6V8X0Z2B4D6F8H0
    MessagesUnavailable:
      description: >-
        Messages can't be taken right now (unavailable). Wait the number of
        seconds in Retry-After, then retry.
      headers:
        x-request-id:
          $ref: '#/components/headers/RequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            unavailable:
              summary: unavailable
              value:
                type: https://api.dizplai.com/problems/unavailable
                code: unavailable
                title: Unavailable
                status: 503
                detail: Messages can't be taken right now. Retry after 5 seconds.
                request_id: 01JBX8P2R4T6V8X0Z2B4D6F8H0
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Your organisation's API key, supplied by Dizplai. Send it on every
        request. It isn't a secret, so it can ship in your app: what a fan does
        is protected by the fan's token.
    fanToken:
      type: http
      scheme: bearer
      description: >-
        The fan's token from your own login, sent as `Authorization: Bearer
        <token>`: a signed-in fan's or a guest's. Dizplai checks it against your
        login's details, agreed when you're set up, and a guest's token carries
        the guest claim agreed at the same time. Needed to vote, upload, send a
        message, and read the fan's own vote or messages.

````

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