> ## 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.

# Add Metadata After a Call

> Attach metadata to a conversation after it has been submitted — for values produced by post-call analysis, so you can segment and filter conversations on outcomes that were not known at call end.

## Overview

Some of the most useful things you can know about a call are not known when the call ends. A post-call classifier finishes a few minutes later. A quality workflow assigns a disposition. A downstream system records whether the customer called back.

`PATCH /v1/conversations/{conversation_id}` lets you add those values to a conversation that already exists. Once a key is added, it is immediately available as a filter on the Conversations page and in the `filter` parameter of `GET /v1/conversations`, so you can compare metric results between segments — for example, how your agent scored on calls where `called_back = 1` against calls where it is `0`.

This is the same endpoint used to [attach audio after a call](/concepts/conversations/async-audio-attach), and it follows the same shape: submit what you have at call end, add the rest when it arrives.

## The two-call flow

<Steps>
  <Step title="Submit the conversation at call end">
    `POST /v1/conversations:submit` returns a `conversation_id` synchronously. Send whatever metadata you already have.

    ```bash theme={null}
    curl -X POST https://api.coval.dev/v1/conversations:submit \
      -H "x-api-key: $COVAL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "transcript": [
          {"role": "agent", "content": "Hello, how can I help you today?"},
          {"role": "user", "content": "I need to check my order status."}
        ],
        "agent_id": "your-agent-id",
        "external_conversation_id": "CA1234567890abcdef",
        "metadata": {
          "channel": "twilio-pstn",
          "queue": "order-support"
        }
      }'
    ```

    Store the returned `conversation_id`.
  </Step>

  <Step title="Add the new values when your analysis completes">
    `PATCH /v1/conversations/{conversation_id}` with a `metadata` object. The keys are merged into the metadata already on the conversation.

    ```bash theme={null}
    curl -X PATCH "https://api.coval.dev/v1/conversations/$CONVERSATION_ID" \
      -H "x-api-key: $COVAL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "metadata": {
          "called_back": 1,
          "csat_bucket": "promoter",
          "resolved_intent": "order_status"
        }
      }'
    ```

    The conversation now carries `channel`, `queue`, `called_back`, `csat_bucket` and `resolved_intent`, and every one of them is filterable.
  </Step>
</Steps>

## Filtering on what you added

Added keys behave exactly like metadata sent at submit time.

* On the **Conversations** page, the key appears in the metadata filter and can be combined with metric, agent and date filters.
* Through the API, filter with `metadata.<key>`:

  ```bash theme={null}
  curl -G "https://api.coval.dev/v1/conversations" \
    -H "x-api-key: $COVAL_API_KEY" \
    --data-urlencode 'filter=metadata.called_back="1"'
  ```

## Metrics are not re-run

Adding metadata does **not** run or re-run any metric. This is deliberate: the common case is enrichment for filtering, and re-running evaluation on every metadata write would be surprising and expensive.

If you have a metric that depends on a key you added — for example, an LLM judge whose prompt references `{{customer_metadata.resolved_intent}}` — rerun it explicitly after the PATCH lands:

```bash theme={null}
curl -X POST "https://api.coval.dev/v1/simulations:rerunMetrics" \
  -H "x-api-key: $COVAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "simulation_ids": ["<simulation_output_id>"],
    "metric_ids": ["<metric_id>"]
  }'
```

<Note>
  A metric whose prompt references a metadata key that is not present fails for that conversation. If a metric depends on a delayed value, expect it to fail on the first pass and succeed on the rerun after the metadata is added.
</Note>

## Additive only

Metadata is **write-once per key**. You can keep adding new keys to a conversation, but you cannot change a key that already has a value.

* Adding a key that is not yet present succeeds.
* Patching a key that already holds a value returns `409 ALREADY_EXISTS`, and nothing is written.
* A stored `0`, `""` or `null` counts as a value. It is a set key, not an empty slot.

This keeps a metric score reproducibly tied to the inputs it was scored against. If you need to correct a value, submit a new conversation with a new `external_conversation_id`.

### Keys you cannot set

Some metadata keys identify the conversation rather than describe it, and are rejected with `400 INVALID_ARGUMENT`:

`conversation_id`, `external_conversation_id`, `call_id`, `trace_id`, `langfuse_trace_id`, `observation_id`, `langfuse_observation_id`, `project_id`, `environment`, `occurred_at`

Set these when you submit the conversation.

## Limits

* One patch target per request. Send `metadata` **or** an audio field, not both. Use two calls if you need to do both.
* At most 100 keys per request; key names are limited to 200 characters.
* Values may be strings, numbers, booleans, arrays or objects.
