# Custom metadata

**The problem.** A transcript is useless if you cannot tie it back to the call, the customer or the agent it came
from. `custom_metadata` attaches your identifiers to the session: they come back with every result and callback,
and you can search your history with them.

**Where it works.** Every session.

> **Use the SDK.** The Gladia SDKs stream the audio, keep it until the API has transcribed it, and reconnect after a network drop
without losing a word. Most integration issues come from hand-written WebSocket clients: start from the SDK
examples below.

## Step by step

### 1. Attach your identifiers

```json
{ "custom_metadata": { "call_id": "c-8812", "agent": "lea" } }
```

### 2. Start a session with it

Each program streams a 16-bit WAV file and prints what comes back. Put your API key in the `GLADIA_API_KEY` environment variable, then run `python custom_metadata.py meeting.wav`.

**Python SDK**

```python title="custom_metadata.py"
# pip install gladiaio-sdk
import os
import sys
import threading
import wave

from gladiaio_sdk import GladiaClient, LiveV2InitRequest

API_URL = os.environ.get("GLADIA_API_URL", "https://eu1.api.gladia.io")
API_KEY = os.environ["GLADIA_API_KEY"]

with wave.open(sys.argv[1]) as wav:  # a 16-bit PCM WAV file
    rate, channels = wav.getframerate(), wav.getnchannels()
    audio = wav.readframes(wav.getnframes())

config = {
    "encoding": "wav/pcm",
    "sample_rate": rate,
    "bit_depth": 16,
    "channels": channels,
    "model": "solaria-3",
    "language_config": {
        "languages": [
            "en"
        ]
    },
    "custom_metadata": {
        "call_id": "c-8812",
        "agent": "lea"
    }
}

client = GladiaClient(api_key=API_KEY, api_url=API_URL)
session = client.live_v2().start_session(LiveV2InitRequest.from_dict(config))
ended, errors = threading.Event(), []

@session.on("message")
def on_message(message):
    if message.type == "transcript" and message.data.is_final:
        print(message.data.utterance.text)

session.on("error", errors.append)
session.once("ended", lambda _: ended.set())

step = rate * channels * 2 // 10  # 100 ms of audio per chunk
for start in range(0, len(audio), step):
    session.send_audio(audio[start : start + step])  # live audio: send each chunk as it is captured
session.stop_recording()

ended.wait(timeout=60)
job = client.live_v2().get(session.session_id)  # the session, from your history
print("metadata:", job.custom_metadata)
sys.exit(1 if errors or not ended.is_set() else 0)
```

**JavaScript SDK**

```js title="custom_metadata.mjs"
// npm install @gladiaio/sdk
import { readFileSync } from "node:fs";
import { GladiaClient } from "@gladiaio/sdk";

const { sampleRate, channels, pcm } = readWav(process.argv[2]); // a 16-bit PCM WAV file

const config = {
  encoding: "wav/pcm",
  sample_rate: sampleRate,
  bit_depth: 16,
  channels,
  model: "solaria-3",
  language_config: {
    languages: [
      "en"
    ]
  },
  custom_metadata: {
    call_id: "c-8812",
    agent: "lea"
  }
};

const client = new GladiaClient({
  apiKey: process.env.GLADIA_API_KEY,
  apiUrl: process.env.GLADIA_API_URL ?? "https://eu1.api.gladia.io",
});
const session = client.liveV2().startSession(config);

session.on("message", (message) => {
  if (message.type === "transcript" && message.data.is_final) console.log(message.data.utterance.text);
});
session.on("error", (error) => {
  console.error(error);
  process.exitCode = 1;
});
session.once("ended", async () => {
  const job = await client.liveV2().get(session.sessionId); // the session, from your history
  console.log("metadata:", job.custom_metadata);
  process.exit();
});

const step = (sampleRate * channels * 2) / 10; // 100 ms of audio per chunk
for (let start = 0; start < pcm.length; start += step) {
  session.sendAudio(pcm.subarray(start, start + step)); // live audio: send each chunk as it is captured
}
session.stopRecording();

function readWav(path) {
  const file = readFileSync(path);
  let fmt, pcm;
  for (let pos = 12; pos < file.length; ) {
    const id = file.toString("ascii", pos, pos + 4);
    const size = file.readUInt32LE(pos + 4);
    if (id === "fmt ") fmt = { channels: file.readUInt16LE(pos + 10), sampleRate: file.readUInt32LE(pos + 12) };
    if (id === "data") pcm = file.subarray(pos + 8, pos + 8 + size);
    pos += 8 + size + (size % 2);
  }
  return { ...fmt, pcm };
}
```

**HTTP**

Create the session, then stream its audio to the returned `url` ([Live WebSocket](/api-reference/v2/live/websocket/)):

```bash
curl -X POST https://eu1.api.gladia.io/v2/live \
  -H "x-gladia-key: $GLADIA_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "encoding": "wav/pcm",
    "sample_rate": 16000,
    "bit_depth": 16,
    "channels": 1,
    "model": "solaria-3",
    "language_config": {
      "languages": [
        "en"
      ]
    },
    "custom_metadata": {
      "call_id": "c-8812",
      "agent": "lea"
    }
  }'
```

### 3. Get them back

They are in [`GET /v2/live/{id}`](/api-reference/v2/live/get/), in Gladia Workspace next to the transcript, and in
the history:

```bash
curl -G https://eu1.api.gladia.io/v2/live -H "x-gladia-key: $GLADIA_API_KEY" \
  --data-urlencode 'custom_metadata={"agent": "lea"}'
```

The filter returns the sessions whose metadata **contains** the values given: `{"agent": "lea"}` matches
`{"call_id": "c-8812", "agent": "lea"}`.

## Limits and gotchas

- **A JSON object.** A string or an array is refused (`400`).
- **Small.** The metadata travels with the session's configuration in its WebSocket URL, together with the
  vocabulary and the spelling dictionary: keep it to identifiers. Too big, `POST /v2/live` answers `400` "the
  session configuration is too large…".
- **Not redacted.** [PII redaction](/features/pii-redaction/) applies to transcripts, never to metadata: no names,
  phone numbers or emails in it.
- **Exact values.** The history filter compares values exactly: `"Lea"` does not match `"lea"`.
- **Set once.** It cannot be changed after `POST /v2/live`.
