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

# Attach audio to a conversation

> Attach an audio recording to a conversation previously submitted via
`POST /v1/conversations:submit` without audio, and trigger audio-dependent
metrics as a second wave.

**When to use this endpoint**

Use this endpoint when your recording URL is not available at the moment
the call ends (for example, Twilio Programmable Voice recordings that are
only fetchable after an asynchronous job completes ~60 seconds later).
Submit the transcript immediately at call end to get a `conversation_id`
for trace correlation, then PATCH the audio when the recording URL is
ready.

**Two-wave metric timing**

- Text-only metrics fire immediately after `POST /v1/conversations:submit`
  and deliver their results via your configured webhook.
- Audio-dependent metrics fire after this PATCH and deliver a separate,
  later webhook. Configure your consumer to expect both waves.

**One-shot**

Audio can be attached only once per conversation. Overwriting is not
supported in v1 — a second PATCH on a conversation that already has
audio returns `409 ALREADY_EXISTS`.

**Body**

Exactly one of `audio` or `audio_url` is required.




## OpenAPI

````yaml /api-reference/v1/conversations-v1.yaml patch /conversations/{conversation_id}
openapi: 3.0.3
info:
  title: Coval Conversations API
  version: 1.0.0
  description: |

    Submit and manage conversation evaluations.
  contact:
    name: Coval API Support
    email: support@coval.dev
    url: https://docs.coval.ai
  license:
    name: Proprietary
    url: https://coval.dev/terms
servers:
  - url: https://api.coval.dev/v1
    description: Production API
security:
  - ApiKeyAuth: []
tags:
  - name: Conversations
    description: Submit and manage real-world conversation monitoring evaluations
  - name: Audio
    description: Upload audio for evaluation and access conversation audio files
paths:
  /conversations/{conversation_id}:
    patch:
      tags:
        - Conversations
      summary: Attach audio to a conversation
      description: >
        Attach an audio recording to a conversation previously submitted via

        `POST /v1/conversations:submit` without audio, and trigger
        audio-dependent

        metrics as a second wave.


        **When to use this endpoint**


        Use this endpoint when your recording URL is not available at the moment

        the call ends (for example, Twilio Programmable Voice recordings that
        are

        only fetchable after an asynchronous job completes ~60 seconds later).

        Submit the transcript immediately at call end to get a `conversation_id`

        for trace correlation, then PATCH the audio when the recording URL is

        ready.


        **Two-wave metric timing**


        - Text-only metrics fire immediately after `POST
        /v1/conversations:submit`
          and deliver their results via your configured webhook.
        - Audio-dependent metrics fire after this PATCH and deliver a separate,
          later webhook. Configure your consumer to expect both waves.

        **One-shot**


        Audio can be attached only once per conversation. Overwriting is not

        supported in v1 — a second PATCH on a conversation that already has

        audio returns `409 ALREADY_EXISTS`.


        **Body**


        Exactly one of `audio` or `audio_url` is required.
      operationId: patchConversation
      parameters:
        - name: conversation_id
          in: path
          description: Unique conversation identifier (22-26 characters)
          required: true
          schema:
            type: string
            minLength: 22
            maxLength: 26
          example: gk3jK9mPq2xRt5vW8yZaBc
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchConversationRequest'
            examples:
              audioUrl:
                summary: Attach via presigned URL (typical)
                value:
                  audio_url: >-
                    https://recordings.s3.amazonaws.com/calls/2025/11/call-4k2m9p.wav?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...
              audioBase64:
                summary: Attach raw audio (small files only)
                value:
                  audio: UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+AAACABAAZGF0YQAAAAA=
      responses:
        '200':
          description: Audio attached successfully; audio metrics queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetConversationResponse'
              examples:
                attached:
                  summary: Audio attached, metrics queued
                  value:
                    conversation:
                      name: conversations/gk3jK9mPq2xRt5vW8yZaBc
                      conversation_id: gk3jK9mPq2xRt5vW8yZaBc
                      status: IN_PROGRESS
                      create_time: '2025-11-03T14:32:30Z'
                      external_conversation_id: external-call-7x8z9a
                      occurred_at: '2025-11-03T14:32:00Z'
                      has_audio: true
                      agent_id: null
                      persona_id: null
                      metadata:
                        campaign: q4-support
        '400':
          $ref: '#/components/responses/InvalidArgument'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Audio is already attached to this conversation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: ALREADY_EXISTS
                  message: >-
                    Conversation 'gk3jK9mPq2xRt5vW8yZaBc' already has audio
                    attached. Overwriting audio is not supported.
                  details:
                    - field: audio
                      description: Audio already attached
        '500':
          $ref: '#/components/responses/InternalError'
components:
  schemas:
    PatchConversationRequest:
      type: object
      properties:
        audio_url:
          type: string
          format: uri
          description: |
            Presigned URL to the audio recording. Same validation rules as
            `audio_url` on `POST /v1/conversations:submit` (WAV or MP3, 5 s to
            60 min, 200 MB max). Supports AWS S3, GCP Cloud Storage, Azure Blob
            Storage, or any public/presigned HTTPS URL. The `s3://bucket/key`
            protocol form is also accepted.
          example: >-
            https://recordings.s3.amazonaws.com/calls/2025/11/call-4k2m9p.wav?X-Amz-Algorithm=...
        audio:
          type: string
          description: >
            Base64-encoded audio bytes. Same format and size limits as
            `audio_url`.

            Prefer `audio_url` for anything other than very small clips.
      description: |
        Request to attach audio to an already-submitted conversation.

        **Exactly one** of `audio_url` or `audio` must be provided. Submitting
        both, or neither, returns `400 INVALID_ARGUMENT`.

        **Idempotency:** Audio can be attached only once per conversation. A
        second PATCH on a conversation that already has audio returns
        `409 ALREADY_EXISTS`. Conversations that were submitted with audio via
        `POST /v1/conversations:submit` already have audio attached and cannot
        be PATCHed.
    GetConversationResponse:
      type: object
      required:
        - conversation
      properties:
        conversation:
          $ref: '#/components/schemas/ConversationResource'
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Error code
              enum:
                - INVALID_ARGUMENT
                - UNAUTHENTICATED
                - NOT_FOUND
                - ALREADY_EXISTS
                - PAYLOAD_TOO_LARGE
                - INTERNAL_ERROR
              example: INVALID_ARGUMENT
            message:
              type: string
              description: Human-readable error message
              example: Missing required input data
            details:
              type: array
              items:
                $ref: '#/components/schemas/ErrorDetail'
              description: Additional error details
              default: []
    ConversationResource:
      type: object
      properties:
        name:
          type: string
          description: Resource name in format "conversations/{conversation_id}"
          example: conversations/gk3jK9mPq2xRt5vW8yZaBc
        conversation_id:
          type: string
          description: Unique conversation identifier (22-26 characters)
          minLength: 22
          maxLength: 26
          example: gk3jK9mPq2xRt5vW8yZaBc
        status:
          $ref: '#/components/schemas/ConversationStatus'
        create_time:
          type: string
          format: date-time
          description: When conversation was submitted (ISO 8601)
          example: '2025-11-03T14:32:30Z'
        external_conversation_id:
          type: string
          description: External conversation ID from customer system
          example: external-call-7x8z9a
        occurred_at:
          type: string
          format: date-time
          description: When the conversation actually occurred (ISO 8601)
          example: '2025-11-03T14:32:00Z'
        has_audio:
          type: boolean
          description: |
            Whether audio is available for this conversation.

            Use GET /v1/conversations/{id}/audio to retrieve presigned URL.
          example: true
        agent_id:
          type: string
          nullable: true
          pattern: ^[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{22}$
          description: >-
            Agent resource ID (22-character ShortUUID), if one was provided at
            submission
          example: null
        persona_id:
          type: string
          nullable: true
          description: Reference to persona resource (typically null for monitoring)
          example: null
        source:
          description: >-
            The caller/initiating side of the conversation; null when not
            addressable. Only included in single-resource GET, not LIST.
          type: object
          nullable: true
          oneOf:
            - $ref: '#/components/schemas/PhoneEndpoint'
            - $ref: '#/components/schemas/SipEndpoint'
            - $ref: '#/components/schemas/WebsocketEndpoint'
          discriminator:
            propertyName: type
        destination:
          description: >-
            The agent side of the conversation; null when not addressable. Only
            included in single-resource GET, not LIST.
          type: object
          nullable: true
          oneOf:
            - $ref: '#/components/schemas/PhoneEndpoint'
            - $ref: '#/components/schemas/SipEndpoint'
            - $ref: '#/components/schemas/WebsocketEndpoint'
          discriminator:
            propertyName: type
        progress:
          allOf:
            - $ref: '#/components/schemas/ConversationProgress'
          description: Progress tracking (included when include_progress=true)
        metadata:
          type: object
          additionalProperties: true
          description: Customer-provided metadata
          example:
            campaign: q4-support
            channel: phone
        tags:
          type: array
          items:
            type: string
          description: Tags applied to this conversation
          example:
            - restaurant
            - support-tier-1
        metric_ids:
          type: array
          items:
            type: string
            minLength: 22
            maxLength: 26
          description: >
            List of metric IDs configured for this conversation.


            Use GET /v1/conversations/{id}/metrics to retrieve computed metric
            values.
          example:
            - 29BlkepvvX19ebbLDB0y6Q
            - mymKvEg6ZA65srXbTX5wSM
            - fstokU4ev5UmT8sUBexiwV
        metric_outputs:
          type: array
          nullable: true
          description: >
            Full outputs matching the requested metric_id. Present only when the
            list

            request sets include=metric_outputs; omitted from the default
            summary view.
          items:
            $ref: '#/components/schemas/MetricOutputResource'
        error:
          type: string
          nullable: true
          description: Error message (only present if status=FAILED)
          example: null
      description: |
        Conversation resource object.
    ErrorDetail:
      type: object
      properties:
        field:
          type: string
          description: Field that caused the error
          example: transcript
        description:
          type: string
          description: Detailed description of the error
          example: At least one of transcript, audio, or audio_url must be provided
    ConversationStatus:
      type: string
      enum:
        - PENDING
        - IN_QUEUE
        - IN_PROGRESS
        - COMPLETED
        - FAILED
        - CANCELLED
        - DELETED
      description: |
        Current status of conversation evaluation.

        **Lifecycle:**
        - PENDING: Submitted, awaiting queue
        - IN_QUEUE: In run-setup-queue, awaiting worker
        - IN_PROGRESS: Evaluation running
        - COMPLETED: Successfully completed
        - FAILED: Encountered error
        - CANCELLED: User cancelled (via DELETE on PENDING/IN_PROGRESS)
        - DELETED: Deleted(via DELETE on COMPLETED/FAILED)
    PhoneEndpoint:
      type: object
      required:
        - type
        - value
      properties:
        type:
          type: string
          enum:
            - phone
          description: Endpoint kind discriminator
        value:
          type: string
          description: Phone number in E.164 format
          example: '+15551234567'
    SipEndpoint:
      type: object
      required:
        - type
        - value
      properties:
        type:
          type: string
          enum:
            - sip
          description: Endpoint kind discriminator
        value:
          type: string
          description: SIP URI
          example: sip:agent@voip.example.com
    WebsocketEndpoint:
      type: object
      required:
        - type
        - value
      properties:
        type:
          type: string
          enum:
            - websocket
          description: Endpoint kind discriminator
        value:
          type: string
          description: WebSocket URL
          example: wss://agent.example.com/stream
    ConversationProgress:
      type: object
      properties:
        total_metrics:
          type: integer
          description: Total number of metrics being evaluated
          example: 5
        completed_metrics:
          type: integer
          description: Number of metrics completed successfully
          example: 5
        failed_metrics:
          type: integer
          description: Number of metrics that failed evaluation
          example: 0
        in_progress_metrics:
          type: integer
          description: Number of metrics currently running
          example: 0
    MetricOutputResource:
      type: object
      required:
        - metric_output_id
        - metric_id
        - status
      properties:
        metric_output_id:
          type: string
          description: Unique metric output identifier (26-char ULID)
          minLength: 26
          maxLength: 26
          example: 01JCQR8Z9PQSTNVWXY12345678
        metric_id:
          type: string
          description: Metric definition identifier (22-char ID)
          minLength: 22
          maxLength: 26
          example: 29BlkepvvX19ebbLDB0y6Q
        metric_version_ulid:
          type: string
          nullable: true
          minLength: 26
          maxLength: 26
          description: >-
            ULID of the metric version this output was scored against (null for
            outputs produced before metric versioning landed)
          example: 01JCQR8Z9PQSTNVWXY12345678
        value:
          description: >
            Metric value (can be float, string, or list of strings depending on
            metric type, or null if failed)
          anyOf:
            - type: number
            - type: string
            - type: array
              items:
                type: string
          example: 2.35
        status:
          type: string
          enum:
            - IN QUEUE
            - IN PROGRESS
            - COMPLETED
            - FAILED
          description: Status of metric computation
          example: COMPLETED
        explanation:
          type: string
          nullable: true
          description: >-
            The LLM judge's reasoning for this metric output, as a flat string.
            Null for metrics that produce no explanation (non-judge metrics) or
            when the output is not yet computed. This is a convenience surfacing
            of the reasoning that otherwise lives nested under
            result.llm.answer_explanation (or result.explanation); it is
            populated in both the list and single-output responses so callers do
            not have to request the full result object to read it.
          example: >-
            The agent never confirmed the caller's address before ending the
            call.
        result:
          type: object
          nullable: true
          additionalProperties: true
          description: >-
            Structured metric result. Its keys depend on the metric type — for
            LLM-judge metrics the judge's reasoning is at
            result.llm.answer_explanation and the evaluation prompt at
            result.llm.prompt. Null for metrics that produce no structured
            result.
          example:
            llm:
              answer_explanation: >-
                The agent never confirmed the caller's address before ending the
                call.
              prompt: Did the agent confirm the caller's address?
      description: |
        Simplified metric output for conversations API.
  responses:
    InvalidArgument:
      description: Invalid request arguments
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            missingInput:
              summary: Missing required input
              value:
                error:
                  code: INVALID_ARGUMENT
                  message: Missing required input data
                  details:
                    - field: transcript
                      description: At least one of transcript or audio_url must be provided
            invalidFilter:
              summary: Invalid filter expression
              value:
                error:
                  code: INVALID_ARGUMENT
                  message: Invalid filter expression
                  details:
                    - field: filter
                      description: >-
                        Unknown field 'invalid_field'. Valid fields: status,
                        create_time, occurred_at, external_conversation_id
            invalidPageSize:
              summary: Invalid page_size
              value:
                error:
                  code: INVALID_ARGUMENT
                  message: Invalid page_size
                  details:
                    - field: page_size
                      description: page_size must be between 1 and 250
            audioFormatInvalid:
              summary: Unsupported audio format
              value:
                error:
                  code: INVALID_ARGUMENT
                  message: >-
                    Unsupported audio format: MP4. Only WAV and MP3 files are
                    supported.
                  details:
                    - field: audio_url
                      description: >-
                        Audio format detected as MP4 via magic byte analysis.
                        Convert to WAV or MP3.
            audioTooShort:
              summary: Audio duration too short
              value:
                error:
                  code: INVALID_ARGUMENT
                  message: 'Audio too short: 3.0 seconds. Minimum duration is 5 seconds.'
                  details:
                    - field: audio_url
                      description: >-
                        Audio file duration is below the 5 second minimum
                        requirement
            audioTooLong:
              summary: Audio duration too long
              value:
                error:
                  code: INVALID_ARGUMENT
                  message: >-
                    Audio too long: 75.0 minutes. Maximum duration is 60
                    minutes.
                  details:
                    - field: audio_url
                      description: Audio file duration exceeds the 1 hour maximum limit
            audioTooLarge:
              summary: Audio file size too large
              value:
                error:
                  code: INVALID_ARGUMENT
                  message: 'Audio file too large: 250.0 MB. Maximum size is 200 MB.'
                  details:
                    - field: audio_url
                      description: Audio file size exceeds 200 MB limit
            audioEmpty:
              summary: Empty audio file
              value:
                error:
                  code: INVALID_ARGUMENT
                  message: Audio file is empty.
                  details:
                    - field: audio_url
                      description: Audio file contains no data
            audioCorrupt:
              summary: Corrupt or invalid audio file
              value:
                error:
                  code: INVALID_ARGUMENT
                  message: >-
                    Invalid or corrupt audio file. Unable to read audio
                    metadata.
                  details:
                    - field: audio_url
                      description: Audio file headers could not be read or are corrupted
    Unauthenticated:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: UNAUTHENTICATED
              message: Invalid or missing API key
              details: []
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            conversationNotFound:
              summary: Conversation not found
              value:
                error:
                  code: NOT_FOUND
                  message: Conversation not found
                  details: []
            simulationAccess:
              summary: Attempted to access simulation
              value:
                error:
                  code: NOT_FOUND
                  message: Conversation not found
                  details: []
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: INTERNAL_ERROR
              message: An internal error occurred while processing the request
              details: []
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        API key for authentication.

````