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

# Stream a recording

> Stream a recording.

The Call Recording API provides read-only access to server-side call recordings associated with a conversation session. It returns recording metadata and media URLs for available audio files, supporting automated workflows such as compliance archiving, quality assurance, and analytics.

A session can produce multiple recordings, such as when a call is transferred. Each recording can contain multiple files, such as separate caller and agent channels. The API does not return transcripts or other conversation text.

Authenticate when retrieving session recordings using a project-bound platform API key (`abl_...`) with the `recordings.read` scope. Send the key in either the `x-api-key` header or the `Authorization: Bearer` header. Studio user JWTs, JWT App assertions, and public `pk_` keys are not accepted.

## Stream recording media

Use this endpoint to stream a recording when the session recordings endpoint returns a file with `urlKind: proxied`. Prefix the returned relative URL with the API host and send a `GET` request to the complete URL. Don't construct the URL on your own.

The `mediaToken` embedded in the URL is the credential for this request. **Do not send an `x-api-key` or `Authorization` header.** The token is short-lived and bound to the tenant, project, recording, file index, disposition, and issuing API key.

This endpoint supports HTTP range requests for seeking or retrieving part of a file. Send a `Range` header, such as `Range: bytes=0-65535`, to request a byte range.

## Response behavior

* `200 OK`: Returns the full media file.
* `206 Partial Content`: Returns the requested byte range.
* `302 Found`: The file is hosted at an external URL; follow the redirect.

## Errors

* `400`: Invalid file index or media disposition.
* `401`: The media token is invalid, expired, or does not match the request.
* `404`: The recording or requested file is not found, or the stored media object is missing.
* `409`: The recording is not yet complete (`RECORDING_NOT_READY`).
* `416`: The requested byte range cannot be satisfied.
* `504`: Streaming did not finish within five minutes.


## OpenAPI

````yaml agent-platform/api-specs/call-recordings.yaml get /api/projects/{projectId}/recordings/{recordingId}/media/content/{mediaToken}
openapi: 3.1.0
info:
  title: Call Recording API
  version: 1.0.0
  summary: >-
    Project-scoped, read-only API for fetching a session's server-side call
    recordings with ready-to-play media URLs.
  description: >
    Fetches every server-side call recording a voice session produced, in one
    request, with a

    media URL for each recorded file. Built for unattended jobs — compliance
    archiving, QA

    sampling, analytics pipelines — that need call audio without a human's
    credentials.

    This is the abl-platform equivalent of Kore XO **Fetch Call Recording v2**.


    The API is read-only. Writes, deletes, legal holds and retention changes
    stay on the

    internal, permission-checked routes. Transcripts and other conversation text
    are not

    returned here.


    ### Authentication


    Send a **platform API key** (`abl_...`) that is bound to the project and
    holds the

    `recordings.read` scope, either as `x-api-key: abl_...` or as

    `Authorization: Bearer abl_...`.


    `recordings.read` expands only to the `recording:read` permission. It grants
    no transcript,

    session-content or write access.


    Other credentials are refused with `403 PLATFORM_API_KEY_REQUIRED`, even
    when they carry

    `recordings.read`:

    - a Studio user JWT

    - a **JWT App** assertion (`auth` header)

    - a public `pk_` key


    ### One request, every recording


    A session can produce more than one recording (for example a transfer), and
    a recording

    can have more than one file (for example separate `caller` / `agent`
    channels). Both levels

    are always arrays: `data.recordings[].files[]`. Each file carries its own
    `channel`.


    ### Availability model — "not yet" vs "never"


    Every file entry always includes the `url` key:

    - **`url` set:** `availability: available`, and `unavailableReason` is
    `null`.

    - **`url` null:** `unavailableReason` says why, and `retryable` says whether
    calling again
      can succeed.

    | `unavailableReason`    |
    Meaning                                                         |
    `retryable` |

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

    | `call_in_progress`     | The session has not ended
    yet                                   | **true**    |

    | `media_in_progress`    | The call ended; the media is still being fetched
    or stored     | **true**    |

    | `url_signing_failed`   | The file exists, but issuing its URL failed this
    time           | **true**    |

    | `recording_disabled`   | The session was never recorded because recording
    was switched off (which setting is not disclosed) | false |

    | `media_not_available`  | The recording server had no media for this
    call                 | false       |

    | `auth_failed`          | The platform could not authenticate to the
    recording server     | false       |

    | `storage_write_failed` | The media could not be written to
    storage                       | false       |

    | `media_object_missing` | The recording is marked complete but its stored
    object is gone  | false       |

    | `retention_expired`    | The recording passed its retention
    period                       | false       |

    | `deleted`              | The recording was
    deleted                                       | false       |


    `data.status` sums up the whole session:

    - **`success`:** every file has a URL.

    - **`partial`:** at least one file has a URL and at least one does not.
    Still **HTTP 200**.

    - **`call_in_progress`:** no file has a URL, and at least one is waiting on
    a call that
      hasn't ended.
    - **`media_in_progress`:** no file has a URL, and at least one is still
    being fetched or
      stored.
    - **`media_unavailable`:** nothing is available, and nothing is still
    pending.


    When the session has **no recording at all**:

    - `recordings` is an empty array.

    - The top-level `unavailableReason` and `retryable` explain why.

    - In practice that's `call_in_progress` (retry later), `recording_disabled`,
    or
      `media_not_available`.

    **Retry rule:** retry with backoff while `data.retryable` is `true`, and
    stop when it is

    `false`.


    ### Media URLs


    `urlKind` tells you how to use `url`:


    | `urlKind`  | What it is | How to fetch it | `expiresIn` |

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

    | `signed`   | A pre-signed object-storage URL (absolute) | `GET` it
    directly, no auth header | Seconds until expiry |

    | `proxied`  | A **relative** path on this host that carries a short-lived
    media token | Prefix it with the API host, then `GET` it with no auth header
    (see `getRecordingMediaContent`) | Seconds until expiry |

    | `external` | A customer-owned recording URL (absolute) | `GET` it directly
    | `null` (the platform doesn't control expiry; never `0`) |


    - **Lifetime:** issued URLs live for
    `RECORDING_PUBLIC_MEDIA_URL_TTL_SECONDS`, which
      defaults to **900 s** and is capped at **3600 s**.
    - **Treat every URL as a bearer secret.** Responses are sent with
    `Cache-Control: no-store`,
      `Pragma: no-cache` and `Referrer-Policy: no-referrer`.
    - **Download vs playback:** `disposition` chooses the `Content-Disposition`
    of the media
      response. Use `attachment` (the default) to download, or `inline` to play in a browser.

    ### Audit


    Every response that issues at least one URL writes an audit record:

    - **Action:** `recording:public_media_urls_issued`.

    - **Principal:** attributed to the calling API key, not a shared user.

    - **Content:** the recording IDs, file indexes, URL kinds and disposition.


    Streaming through a `proxied` URL writes a second record,

    `recording:media_content_streamed`, for each request.


    ### Limits


    | Limit | Default | Over the limit |

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

    | Recordings per session | 64 | `413 RECORDING_QUERY_LIMIT_EXCEEDED` |

    | Stored files across a session's recordings | 32
    (`RECORDING_PUBLIC_RECORDING_OBJECT_LIMIT`) | `413
    RECORDING_OBJECT_LIMIT_EXCEEDED` |

    | Requests | Tenant request rate limit | `429 RATE_LIMIT_EXCEEDED` |


    ### Isolation and gating


    - **Other project:** a session in a project the key isn't bound to returns
      `404 PROJECT_NOT_FOUND`, never `403`, so the API doesn't reveal whether it exists.
    - **Feature not enabled:** for a tenant without the call-recording feature,
    the request
      fails before any lookup.

    ### Migrating from XO Fetch Call Recording v2


    | XO | This API |

    |---|---|

    | `/api/public/bot/{botId}/...` |
    `/api/public/recordings/projects/{projectId}/sessions/{sessionId}/recordings`
    |

    | App JWT (`auth` header) | Platform API key (`x-api-key` or `Authorization:
    Bearer abl_...`) with `recordings.read` |

    | Lookup by call ID, time window or contact | **Session ID only** (v1).
    `callId` is returned on each recording. |

    | One URL per call | List of recordings, each with a list of files and its
    own `channel` |
servers:
  - url: https://{host}
    description: Base URL of your platform.
    variables:
      host:
        default: runtime.example.com
security:
  - ApiKeyAuth: []
  - BearerPlatformKey: []
tags:
  - name: Call Recordings
    description: Fetch a session's call recordings and stream their media.
paths:
  /api/projects/{projectId}/recordings/{recordingId}/media/content/{mediaToken}:
    get:
      tags:
        - Call Recordings
      summary: Stream recording media (target of a `proxied` URL)
      description: Stream a recording.
      operationId: getRecordingMediaContent
      parameters:
        - $ref: '#/components/parameters/ProjectId'
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
            minLength: 1
        - name: mediaToken
          in: path
          required: true
          description: Signed media token issued inside a `proxied` URL.
          schema:
            type: string
            minLength: 1
        - name: objectIndex
          in: query
          required: false
          description: File index within the recording. Must match the token.
          schema:
            type: integer
            minimum: 0
            default: 0
        - $ref: '#/components/parameters/Disposition'
        - name: Range
          in: header
          required: false
          description: Single byte range, e.g. `bytes=0-65535`.
          schema:
            type: string
      responses:
        '200':
          description: The full media file.
          headers:
            Content-Type:
              description: Media type, e.g. `audio/flac`.
              schema:
                type: string
            Content-Length:
              schema:
                type: integer
            Content-Disposition:
              description: '`inline` or `attachment` with the file name.'
              schema:
                type: string
          content:
            audio/flac:
              schema:
                type: string
                format: binary
            application/octet-stream:
              schema:
                type: string
                format: binary
        '206':
          description: Partial content for a `Range` request.
          headers:
            Content-Range:
              schema:
                type: string
              description: e.g. `bytes 0-65535/242323`.
            Accept-Ranges:
              schema:
                type: string
                const: bytes
          content:
            audio/flac:
              schema:
                type: string
                format: binary
        '302':
          description: The file is an external URL; follow the redirect.
          headers:
            Location:
              schema:
                type: string
                format: uri
        '400':
          description: '`INVALID_OBJECT_INDEX` or `INVALID_MEDIA_DISPOSITION`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: >-
            Token invalid, expired, or not matching the path, `objectIndex` or
            `disposition`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error:
                  code: INVALID_RECORDING_MEDIA_TOKEN
                  message: Invalid media token
        '404':
          description: >-
            `RECORDING_NOT_FOUND` (no such recording or file index) or
            `RECORDING_MEDIA_NOT_FOUND` (stored object missing).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: The recording is not `completed` yet.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error:
                  code: RECORDING_NOT_READY
                  message: Recording media is not ready
        '416':
          description: Unsatisfiable `Range` header.
          headers:
            Content-Range:
              schema:
                type: string
              description: '`bytes */*`'
        '504':
          description: The stream did not finish within 5 minutes.
      security: []
components:
  parameters:
    ProjectId:
      name: projectId
      in: path
      required: true
      description: Project ID (not the slug). The key must be bound to this project.
      schema:
        type: string
        minLength: 1
    Disposition:
      name: disposition
      in: query
      required: false
      description: >
        `Content-Disposition` for the media response: `attachment` (download,
        the default) or

        `inline` (play in a browser). It is built into each issued URL.
      schema:
        type: string
        enum:
          - inline
          - attachment
        default: attachment
  schemas:
    ErrorEnvelope:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          const: false
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Stable machine-readable error code.
            message:
              type: string
              description: Human-readable detail.
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Platform API key (`abl_...`) bound to the project, with the
        `recordings.read` scope.
    BearerPlatformKey:
      type: http
      scheme: bearer
      bearerFormat: Platform API key (abl_...)
      description: >-
        The same platform API key sent as `Authorization: Bearer abl_...`. This
        is not a JWT.

````

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