# Metadata

> Put your own ids on a text with metadata, and BlueBot hands them back on the message and on every webhook about it, so each delivery, failure and read ties back to your record.

Metadata is your own data on a text. BlueBot never reads it or acts on it; it stores it with the text and hands it back on the message and on every event about that text. Use it to tie a text to the record in your system it was sent for, so a webhook saying “this text failed” tells you which appointment, order or lead it was about.

Dr. Patel’s booking system texts each patient a reminder the day before. It puts the appointment’s id in metadata:

```bash
curl -X POST "https://usebluebot.com/api/v1/messages" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: appt-7712-reminder" \
  -d '{
    "to": "+15555550123",
    "text": "Hi Maria, a reminder that your cleaning with Dr. Patel is tomorrow at 10:30 AM.",
    "metadata": {
      "appointment_id": "7712",
      "office": "north"
    }
  }'
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const message = await client.messages.send({
  to: '+15555550123',
  text: 'Hi Maria, a reminder that your cleaning with Dr. Patel is tomorrow at 10:30 AM.',
  metadata: {
    appointment_id: '7712',
    office: 'north',
  },
}, { idempotencyKey: 'appt-7712-reminder' });
console.log(message);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

message = client.messages.send(
    to="+15555550123",
    text="Hi Maria, a reminder that your cleaning with Dr. Patel is tomorrow at 10:30 AM.",
    metadata={
        "appointment_id": "7712",
        "office": "north",
    },
    idempotency_key="appt-7712-reminder",
)
print(message)
```

The answer, `201`:

```json
{
  "id": "msg_2Bf8QzD4kJmA6nXwEuTr1c",
  "object": "message",
  "direction": "out",
  "state": "queued",
  "to": "+15555550123",
  "metadata": { "appointment_id": "7712", "office": "north" }
}
```

When the text fails, the `message.failed` webhook carries the same metadata, so the booking system can flag appointment 7712 for a phone call without looking anything up.

## What it can hold

|              | Limit                            |
| ------------ | -------------------------------- |
| Keys         | Up to 20                         |
| A key’s name | 1 to 40 characters               |
| A value      | A string of up to 500 characters |

Every value is a string. Send a number as `"7712"`, not `7712`, and a list or an object as a string you encode yourself, such as `"a,b,c"`. A number, a `true`, a `null`, a nested object or anything over a limit makes the whole send `400 invalid_request`, with `details` naming the field, such as `metadata.appointment_id`. Nothing is sent.

Metadata is for ids and short labels. It's sent with every event about the text, so keep it small: the id of your record rather than a copy of it.

## Where it comes back

| Where                                                        | What you get                                                            |
| ------------------------------------------------------------ | ----------------------------------------------------------------------- |
| The send’s answer                                            | The `metadata` you sent.                                                |
| `GET /v1/messages/{id}` and `GET /v1/messages`               | The text’s `metadata`, on every text in a list.                         |
| Every `message.*` event, by webhook or from `GET /v1/events` | The text’s `metadata` inside `data`, as it was when the event happened. |
| Tapback events (`reaction.*`)                                | Not there. They carry `message_id`; read the text for its metadata.     |
| Texts that come in                                           | `{}`. A person’s reply has no metadata of yours.                        |

A text with no metadata has `"metadata": {}`, never `null`, so your code can read a key without checking for the object first.

### In a webhook handler

```ts title="webhooks.ts"
const event = client.webhooks.verify(body, headers, process.env.BLUEBOT_WEBHOOK_SECRET!);

if (event.type === 'message.failed') {
    const appointmentId = event.data.metadata.appointment_id;
    if (appointmentId) await flagForCall(appointmentId, event.data.failure?.reason);
}
```

```python title="webhooks.py"
event = client.webhooks.verify(body, headers, os.environ["BLUEBOT_WEBHOOK_SECRET"])

if event.type == "message.failed":
    appointment_id = event.data.metadata.get("appointment_id")
    if appointment_id:
        flag_for_call(appointment_id, event.data.failure.reason if event.data.failure else None)
```

The SDKs send and return metadata keys exactly as you write them. TypeScript turns the API’s own fields into camelCase (`event.data.contactId`) but leaves the keys inside `metadata` alone, so `appointment_id` stays `appointment_id`.

## Matching a reply to your record

A reply has no metadata, because the person wrote it, not your code. To find which of your records it belongs to, read the last text you sent that person and take its metadata:

```bash
curl "https://usebluebot.com/api/v1/messages?person=%2B15555550123&direction=out&limit=1" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY"
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const page = await client.messages.list({
  person: '+15555550123',
  direction: 'out',
  limit: 1,
});
console.log(page.data);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

page = client.messages.list(person="+15555550123", direction="out", limit=1)
print(page.data)
```

The first text in `data` is the newest you sent them, and its `metadata.appointment_id` says which appointment Maria is answering. When you text the same person about more than one thing at a time, this picks the newest, which may not be the one they mean; read the words of the reply too.

## Changing it

You can change metadata only while a text is scheduled. `PATCH /v1/messages/{id}` with `metadata` replaces all of it, so send every key you want to keep:

```bash
curl -X PATCH "https://usebluebot.com/api/v1/messages/msg_2Bf8QzD4kJmA6nXwEuTr1c" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "metadata": {
      "appointment_id": "7712",
      "office": "south"
    }
  }'
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const message = await client.messages.update('msg_2Bf8QzD4kJmA6nXwEuTr1c', {
  metadata: {
    appointment_id: '7712',
    office: 'south',
  },
});
console.log(message);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

message = client.messages.update(
    "msg_2Bf8QzD4kJmA6nXwEuTr1c",
    metadata={
        "appointment_id": "7712",
        "office": "south",
    },
)
print(message)
```

Once a text is queued, its metadata is fixed. [Change or cancel a text](https://usebluebot.com/docs/sending/change-or-cancel) covers what else can change and when.

Settling an unconfirmed text with `send_again` queues a new text with the same words, files and metadata, so the new text’s events still tie back to your record.

## What you can’t do with it

- **Search by it.** No list filters on metadata. Keep the BlueBot message `id` on your own record when you send, and look the text up by its id later.
- **Use it as a key for retries.** Two sends with the same metadata are two texts. Use an [`Idempotency-Key`](https://usebluebot.com/docs/concepts/idempotency) to keep a retry from sending twice.

## What not to put in it

Metadata goes to every webhook address you’ve subscribed to `message.*` events and to anyone with a key that can read messages. BlueBot deletes the words of a text after 30 days, but its metadata stays with the rest of its record. So put an id in it and keep the details in your own system:

- No passwords, keys or tokens.
- No health, financial or other private details about the person. `"appointment_id": "7712"` is fine; `"reason": "root canal follow-up"` is not.
- Nothing you would want deleted along with the words.

## In no-code apps

The Zapier, Make, n8n and Pipedream **Send Text** actions each take metadata as key and value pairs, and their triggers for a text’s status hand it back, so a Zap can carry a row’s id from the send to the step that updates the row.
