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

# Video Description

> Produce a timestamped index of what happens on screen — a list of events, each with `start`, `end` (seconds) and a `description` — plus scene boundaries and source metadata. Reads the picture, not the audio track; for speech use POST /api/v2/audio/transcriptions. Accepts either a video URL (YouTube, Twitter/X, Twitch, Kick, TikTok) or a file upload. The result is a JSON document: `{"events": [{"start", "end", "description", "merged?", "edge_distance"}], "scenes": [...], "meta": {"duration", "preset", ...}}`, delivered as `result_url` on the job (and as a serialized JSON string in `result` when `return_result_in_response` is set). Priced on machine time at the model's hourly rate — longer videos and higher presets cost more — or, for TikTok, on a fixed card by length capped at 10 minutes; see POST /api/v2/videos/descriptions/price.

Describe visible events and scene boundaries in a video. Returns a `request_id` for status polling.

<Note>
  **Prerequisite:** Consult the [Model Selection](/api/v2/utilities/models) endpoint to identify a valid model `slug` and check supported presets and duration limits.
</Note>


## OpenAPI

````yaml openapi-v2.json POST /api/v2/videos/descriptions
openapi: 3.1.0
info:
  title: deAPI REST API
  description: >-
    Decentralized AI inference API for image generation, video processing, audio
    transcription, and more.


    ## Rate limits


    Two windows can apply to a request: a per-minute one (RPM) and a per-day one
    (RPD). Every

    throttled response — success or 429 — carries the `X-RateLimit-*` headers,
    so you can pace

    traffic without waiting to be rejected. Each header documents itself: see
    `components/headers`,

    or the headers listed on any response. What follows is only what a header
    cannot say about

    itself.


    Windows open on your first request into a bucket and run their full length
    from that second —

    never to a calendar boundary, in any timezone. That is deliberate: a
    boundary every account

    shares is a boundary every account can double up on.


    **The headers describe the pool *this* request landed in, and
    `X-RateLimit-Source` is what

    identifies it — not `X-RateLimit-Limit` alone.** A per-model limit is keyed
    to the model you

    named, so a request that carried no recognised `model` — rejected at
    validation, an unknown

    slug, an empty body — was counted against your tier pool, and it is the tier
    pool the headers

    then describe. The two pools are fully independent: separate minute window,
    separate day,

    separate `X-RateLimit-Daily-Reset`. This is a consequence of rate limiting
    running *before*

    validation, which is what keeps malformed traffic from reaching the rest of
    the API

    uncounted. Compare headers only across responses carrying the same
    `X-RateLimit-Source`.


    **Buckets are shared in ways worth knowing.** API **v1 and v2 count against
    the same bucket**,

    so migrating grants no fresh allowance and mixed traffic behaves as one
    stream. All `*/price`

    and `*/price-calculation` endpoints share a single quote bucket, separate
    from generation.

    Prompt enhancement (`/v2/prompts/enhancements`) has its own bucket as well,
    so heavy prompt

    work no longer eats into your image throughput.


    **Counting is exact in sequence, best-effort in parallel.** Sent one after
    another, your

    requests are counted precisely: the one that crosses the limit is the one
    that gets 429. Sent

    in parallel, a few beyond the limit can slip through, because the counter is
    read and then

    incremented as two steps and requests already in flight are not yet visible
    to one another.

    The excess is bounded by how many of your own requests are in flight at
    once, and it only

    ever runs in your favour — you are never rejected while below your limit. So
    do not treat

    `X-RateLimit-Remaining` as a reservation: it is a snapshot, and a request
    you sent a

    millisecond ago may already have spent what it shows.


    Requests rejected before authentication (HTTP 401) never reach the limiter
    and carry no

    rate-limit headers at all.
  contact:
    name: deAPI Support
    url: https://deapi.ai
    email: support@deapi.ai
  version: 0.0.1
servers:
  - url: https://api.deapi.ai
    description: Production API Server base URL
security:
  - bearerAuth: []
tags:
  - name: Client API v2
    description: Current client endpoints (OpenAI-aligned noun-based paths)
paths:
  /api/v2/videos/descriptions:
    post:
      tags:
        - Client API v2
      summary: Describe what happens in a video
      description: >-
        Produce a timestamped index of what happens on screen — a list of
        events, each with `start`, `end` (seconds) and a `description` — plus
        scene boundaries and source metadata. Reads the picture, not the audio
        track; for speech use POST /api/v2/audio/transcriptions. Accepts either
        a video URL (YouTube, Twitter/X, Twitch, Kick, TikTok) or a file upload.
        The result is a JSON document: `{"events": [{"start", "end",
        "description", "merged?", "edge_distance"}], "scenes": [...], "meta":
        {"duration", "preset", ...}}`, delivered as `result_url` on the job (and
        as a serialized JSON string in `result` when `return_result_in_response`
        is set). Priced on machine time at the model's hourly rate — longer
        videos and higher presets cost more — or, for TikTok, on a fixed card by
        length capped at 10 minutes; see POST /api/v2/videos/descriptions/price.
      operationId: createVideoDescription
      parameters:
        - $ref: '#/components/parameters/AcceptHeader'
      requestBody:
        description: >-
          Video description parameters. Provide exactly one of video_url or
          video_file.
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/VideoDescriptionRequestBody'
      responses:
        '200':
          description: ID of the inference request.
          headers:
            X-RateLimit-Policy:
              $ref: '#/components/headers/X-RateLimit-Policy'
            X-RateLimit-Source:
              $ref: '#/components/headers/X-RateLimit-Source'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/X-RateLimit-Daily-Limit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/X-RateLimit-Daily-Remaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/X-RateLimit-Daily-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobRequestResponseResource'
        '401':
          description: Unauthorized user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/response_error_default'
        '403':
          $ref: '#/components/responses/AccountSuspended'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
      security:
        - bearerAuth: []
components:
  parameters:
    AcceptHeader:
      name: Accept
      in: header
      required: true
      schema:
        type: string
        default: application/json
        enum:
          - application/json
  schemas:
    VideoDescriptionRequestBody:
      description: >-
        A source is MANDATORY and comes in exactly one of two shapes —
        `video_url` for a hosted video, or `video_file` for an upload. Sending
        neither is a 422, and sending both is a 422 as well; the `oneOf` below
        is that choice, not a pair of optional fields. Everything else applies
        to both. The `/price` twin of this endpoint accepts a third shape,
        `duration_seconds`, which this one does not.
      required:
        - model
      properties:
        video_url:
          description: >-
            URL of the video to describe (YouTube, Twitter/X, Twitch, Kick,
            TikTok — the same platforms as transcription). Audio-only sources
            such as Twitter Spaces are rejected. Live streams are rejected.
            Mutually exclusive with video_file.
          type: string
          example: https://www.youtube.com/watch?v=jNQXAC9IVRw
        video_file:
          description: >-
            Video file to describe. Supported: mp4, mpeg, quicktime, avi, wmv,
            ogg, webm, mkv. Must contain a video stream. Mutually exclusive with
            video_url.
          type: string
          format: binary
        model:
          description: >-
            The model to use. Available models can be retrieved via the GET
            /api/v2/models endpoint; a model lists `video2desc` (URL sources)
            and/or `video_file2desc` (uploads) in its `inference_types`. Its
            `info.limits.max_video_duration_seconds`, when present, is the
            longest source it accepts.
          type: string
          example: Marlin_2B
        preset:
          description: >-
            Detail level. A preset is the per-frame pixel budget the model reads
            the video at: `fast` reads a small frame over long windows, `detail`
            a large frame over short windows, so higher presets read more of
            what is on screen (small text, brand names) and cost more — roughly
            ×1 / ×3 / ×7 machine time for `fast` / `balanced` / `detail` (×0.75
            / ×1 / ×1.5 on the TikTok card). Omit for the default (`balanced`,
            or the cheapest preset the model offers if it does not offer
            `balanced`). Only honoured on a model whose
            `info.features.supports_presets` is true; such a model publishes the
            presets it offers as `info.limits.available_presets` and rejects any
            other. On a model without the flag the field is ignored.
          type:
            - string
            - 'null'
          example: balanced
          enum:
            - fast
            - balanced
            - detail
            - null
        include_metadata:
          description: >-
            If true, the job-status response carries a `metadata` object
            describing the source — title, channel, uploader, upload date and
            engagement counts such as views, likes and comments — exactly as on
            POST /api/v2/audio/transcriptions. Only URL sources have any; an
            uploaded file returns null and is never charged for the flag.
            Carries the same flat surcharge as transcription metadata.
          type:
            - boolean
            - 'null'
          example: false
          default: false
        return_result_in_response:
          description: >-
            If true, the completed job-status response includes the description
            as a serialized JSON string in `data.result`, in addition to
            `data.result_url`. Parse the string as JSON to read `events`,
            `scenes` and `meta`.
          type:
            - boolean
            - 'null'
          example: false
          default: false
        webhook_url:
          description: >-
            Optional HTTPS URL to receive webhook notifications for job status
            changes (processing, completed, failed). Must be HTTPS. Max 2048
            characters. A per-request URL is self-contained: it is delivered
            even when the account has no webhook configuration, or the account
            webhook is disabled. See the `webhooks` block at the top level of
            this document for the event payloads and the headers each delivery
            carries.
          type:
            - string
            - 'null'
          format: uri
          example: https://your-server.com/webhooks/deapi
          maxLength: 2048
        webhook_secret:
          description: >-
            Optional per-request HMAC secret (min. 32 chars) used to sign the
            webhook callback. Overrides the account-default secret for this job
            only. If omitted, the account-default secret signs the callback; if
            no secret exists at all, the callback is still delivered but
            UNSIGNED (empty X-DeAPI-Signature header). Requires `webhook_url` to
            also be set.
          type:
            - string
            - 'null'
          example: a1b2c3d4e5f60708091a2b3c4d5e6f7081920a1b2c3d4e5f60708091a2b3c4d5
          maxLength: 255
          minLength: 32
      type: object
      oneOf:
        - title: From a video URL
          required:
            - video_url
          type: object
        - title: From an uploaded file
          required:
            - video_file
          type: object
    JobRequestResponseResource:
      required:
        - data
      properties:
        data:
          description: Information from success endpoint
          required:
            - request_id
          properties:
            request_id:
              description: >-
                UUID identifying the created inference request. Pass it to `GET
                /api/v2/jobs/{job_request}` to poll status and collect the
                result.
              type: string
              format: uuid
              example: c08a339c-73e5-4d67-a4d5-231302fbff9a
          type: object
      type: object
    response_error_default:
      properties:
        data:
          description: Information from success endpoint
          type: object
        message:
          description: Error general message
          type: string
        errors:
          description: Information about errors
          type: array
          items: {}
        statusCode:
          description: Status code
          type: integer
      type: object
    response_error_rate_limit:
      description: Rate limit exceeded response
      properties:
        message:
          description: Error message
          type: string
          example: Too Many Attempts.
      type: object
  headers:
    X-RateLimit-Policy:
      description: >-
        Every window that applies, as `<limit>;w=<seconds>` elements. Always
        present on a throttled response, so a single element means there is no
        daily limit — do not infer that from the absence of the Daily-* headers.
        `300;w=60` is minute-only; `3;w=60, 100;w=86400` is both.
      schema:
        type: string
        example: 3;w=60, 100;w=86400
    X-RateLimit-Source:
      description: >-
        Where the limit came from, so you know what would change it:

        `tier` — your account tier's rate for this endpoint;

        `fallback` — no rate configured for this tier and endpoint, degraded
        default in force;

        `model` — a per-model override for the model in the request, unaffected
        by your tier

        (only when the request carried a recognised `model`, otherwise the tier
        pool applies);

        `static` — a flat limit outside the tier system, identical for every
        account.


        If you ever see `fallback`, tell us: it means no rate is configured for
        that tier and endpoint.
      schema:
        type: string
        example: tier
        enum:
          - tier
          - fallback
          - model
          - static
    X-RateLimit-Limit:
      description: Maximum requests allowed per minute (RPM)
      schema:
        type: integer
        example: 3
    X-RateLimit-Remaining:
      description: Remaining requests in current minute window
      schema:
        type: integer
        example: 2
    X-RateLimit-Reset:
      description: >-
        Epoch second at which the minute (RPM) window refills. Fixed window —
        the value does not drift as you spend the quota. Describes the same
        window as X-RateLimit-Limit, including on a daily rejection.
      schema:
        type: integer
        example: 1755800460
    X-RateLimit-Daily-Limit:
      description: Maximum requests allowed per day (RPD)
      schema:
        type: integer
        example: 100
    X-RateLimit-Daily-Remaining:
      description: Remaining requests in current day window
      schema:
        type: integer
        example: 95
    X-RateLimit-Daily-Reset:
      description: >-
        Epoch second at which the daily (RPD) window refills — 86400 s after the
        first request that opened this bucket's window, not a calendar midnight.
        Present only when a daily limit applies.
      schema:
        type: integer
        example: 1755846000
    X-RateLimit-Type:
      description: >-
        Which window rejected the request: `minute` (RPM) or `daily` (RPD). Sent
        only on 429.
      schema:
        type: string
        example: minute
        enum:
          - minute
          - daily
    Retry-After:
      description: >-
        Seconds until the window that rejected you frees up (60 for minute, up
        to 86400 for daily). Counted from when that window opened, not to a
        calendar boundary. Sent only on 429.
      schema:
        type: integer
        example: 60
  responses:
    AccountSuspended:
      description: >-
        The account is suspended. Applies to every endpoint that spends credits
        — generation, transcription and prompt enhancement — and is returned
        before any validation runs, so a suspended account sees this rather than
        a 422 about the body. Price-estimate endpoints are not gated and keep
        working.
      content:
        application/json:
          schema:
            properties:
              message:
                description: >-
                  Human-readable explanation. No `errors` object accompanies
                  this response.
                type: string
                example: >-
                  Client account is suspended. If you believe this is a mistake,
                  contact us.
            type: object
    ValidationError:
      description: >-
        Validation failed. Common errors include: model does not exist, model
        does not support the inference type for this endpoint, the model is not
        available on your plan (it requires a higher tier than your account has
        — upgrade to use it), or invalid request parameters.
      content:
        application/json:
          schema:
            properties:
              message:
                description: General error message
                type: string
                example: The selected model does not support Text To Image.
              errors:
                description: >-
                  Detailed validation errors by field. A model your plan does
                  not cover is reported on the "model" field with the message
                  "This model is not available on your plan. Please upgrade to
                  access it." Such a model is also omitted from GET /models.
                type: object
                example:
                  model:
                    - >-
                      This model is not available on your plan. Please upgrade
                      to access it.
            type: object
    RateLimitExceeded:
      description: >-
        Rate limit exceeded. Check X-RateLimit-Type header to determine if
        minute (RPM) or daily (RPD) limit was hit.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Daily-Limit:
          $ref: '#/components/headers/X-RateLimit-Daily-Limit'
        X-RateLimit-Daily-Remaining:
          $ref: '#/components/headers/X-RateLimit-Daily-Remaining'
        X-RateLimit-Type:
          $ref: '#/components/headers/X-RateLimit-Type'
        Retry-After:
          $ref: '#/components/headers/Retry-After'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
        X-RateLimit-Daily-Reset:
          $ref: '#/components/headers/X-RateLimit-Daily-Reset'
        X-RateLimit-Source:
          $ref: '#/components/headers/X-RateLimit-Source'
        X-RateLimit-Policy:
          $ref: '#/components/headers/X-RateLimit-Policy'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/response_error_rate_limit'
  securitySchemes:
    bearerAuth:
      type: http
      description: >-
        Sanctum personal access token, sent as `Authorization: Bearer <token>`.
        The token is opaque — it carries no claims and no embedded expiry, so do
        not attempt to decode it. Issue and revoke tokens from your account
        dashboard.
      scheme: bearer

````