> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.meetstream.ai/llms.txt.
> For full documentation content, see https://docs.meetstream.ai/llms-full.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.meetstream.ai/_mcp/server.

# Idempotency Key Guide for Bot Creation in MeetStream

This guide explains how to use the `Idempotency-Key` header on the **Create Bot** endpoint to safely retry requests without creating duplicate bots.

> **Scope:** This feature is currently supported **only on `POST /api/v1/bots/create_bot`** (bot creation). Other write endpoints (PATCH scheduled bots, remove bot, etc.) do not honor `Idempotency-Key` yet.

---

## 1) Why idempotency matters

When you call the Create Bot API, several real-world conditions can cause your client to retry the same request:

- The network drops mid-request and your HTTP client retries.
- API Gateway returns a 5xx and your retry library kicks in.
- Your worker queue redelivers the same job because the previous worker didn't ack in time.
- A user double-clicks the "Join meeting" button in your UI.

Without idempotency, every one of these retries would create a **new bot** — and you'd end up with two, three, or more bots in the same meeting, each consuming credits.

`Idempotency-Key` solves this: MeetStream remembers the key you sent, ties it to the bot that got created, and replays the original bot's details on every subsequent retry — never creating a duplicate.

---

## 2) How it works (one-paragraph summary)

You generate a unique string per logical bot creation (a UUID is ideal) and pass it in the `Idempotency-Key` HTTP header. The first request with that key creates a bot normally and returns `HTTP 201`. Every subsequent request that sends the **same key from the same API key/user** returns `HTTP 507` with the original bot's details — the request body is ignored, no new bot is created, no credits are charged again.

Keys are scoped per user (the API key owner). The same key value sent by two different MeetStream accounts will **not** collide.

---

## 3) Quick example

### First call — creates the bot

```bash
curl -X POST "https://api.meetstream.ai/api/v1/bots/create_bot" \
  -H "Authorization: Token <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9b1c3f4a-7e2b-4d8f-9a1c-2e6f4b8a0c11" \
  -d '{
    "meeting_link": "https://meet.google.com/abc-defg-hij",
    "video_required": false
  }'
```

Response (`HTTP 201`):

```json
{
  "bot_id": "bot_a1b2c3d4...",
  "transcript_id": "t_111...",
  "meeting_url": "https://meet.google.com/abc-defg-hij",
  "status": "Active"
}
```

### Retry with the same key — replays the original

```bash
curl -X POST "https://api.meetstream.ai/api/v1/bots/create_bot" \
  -H "Authorization: Token <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9b1c3f4a-7e2b-4d8f-9a1c-2e6f4b8a0c11" \
  -d '{
    "meeting_link": "https://meet.google.com/abc-defg-hij",
    "video_required": false
  }'
```

Response (`HTTP 507`):

```json
{
  "bot_id": "bot_a1b2c3d4...",
  "transcript_id": null,
  "meeting_url": "https://meet.google.com/abc-defg-hij",
  "status": "Active"
}
```

The bot ID and meeting URL match the original. No new bot was created. No credits were charged. The `status` field reflects the bot's status **at the time of the replay** — so if the bot has progressed from `Joining` to `Active` between calls, you'll see the latest state.

---

## 4) The header

| Header | Required | Format | Description |
|---|---|---|---|
| `Idempotency-Key` | No | Any non-empty string (UUID v4 strongly recommended) | Unique per logical bot-creation request. |

Header rules:

- **Case-insensitive.** Both `Idempotency-Key` and `idempotency-key` are accepted. (REST API Gateway preserves casing, HTTP API lowercases — MeetStream handles both.)
- **Whitespace is trimmed.** Leading/trailing whitespace is removed before lookup.
- **Whitespace-only values are ignored** (treated as if the header was absent).
- **No length cap enforced** — but keep keys reasonably short. UUID v4 (`9b1c3f4a-7e2b-4d8f-9a1c-2e6f4b8a0c11`) is the canonical choice.

If you omit the header, the endpoint behaves exactly as before — every call creates a new bot.

---

## 5) Response codes

| Status | When | Meaning |
|---|---|---|
| `201 Created` | First call for a `(user, key)` pair, or any call without the header | Bot was created. Standard create-bot response body. |
| `507` | Subsequent call with a key that was already bound to a bot | Replay of the original bot. **Treat this as a success.** No new bot was created. |
| `400 / 403 / 429 / 500` | Validation, wallet, or platform errors | Standard error responses. The key is **not** cached when the request fails before a bot row is written — your retry will execute fresh. |

### 507 replay response shape

```json
{
  "bot_id":        "<BotID of the originally created bot>",
  "transcript_id": null,
  "meeting_url":   "<MeetingLink of the originally created bot>",
  "status":        "<current Status of the bot at lookup time>"
}
```

> **Why `transcript_id` is `null` on replay:** the transcript ID is generated and returned only at original creation time. If you need it later, fetch it from the bot detail endpoint using the `bot_id`.

---

## 6) Rules and guarantees

### What MeetStream guarantees

1. **No duplicate bots from sequential retries.** Once a `(user_id, idempotency_key)` pair is bound to a bot, every future request with that pair replays the original bot — forever.
2. **Per-user isolation.** The same key value used by two different MeetStream accounts will **not** collide. Keys are scoped to your API key's user.
3. **Request body is ignored on replay.** If you change the `meeting_link`, `bot_name`, or any other field but send the same key, you still get the original bot back. To create a different bot, use a different key.
4. **Failed creates are not cached.** If the original request returned a 4xx/5xx **before** any bot record was written (validation error, wallet rejection, server error), a retry with the same key will be treated as a fresh attempt.

### What MeetStream does **not** guarantee

1. **No in-flight (409) detection.** If you fire two requests with the same key in parallel **before the first one finishes writing to the database**, both may create a bot. Subsequent retries will then deterministically resolve to whichever bot the index surfaces first.
   - **Mitigation:** in your client code, serialize retries (don't fire them in parallel). For UUID-keyed clients this is almost never an issue.
2. **No key expiration.** Keys are bound for the lifetime of the bot record. **Never reuse a key for a different logical create** — treat each new bot creation as a fresh UUID.
3. **Lookup fails open.** If MeetStream's internal lookup encounters a transient database error, the request proceeds to create a new bot rather than reject the call. In that very rare case you may see one duplicate bot. This is intentional: we prefer a rare duplicate over refusing legitimate traffic.

---

## 7) Best practices

### DO

- **Generate a fresh UUID v4 per logical create.** Use your language's standard UUID library — `uuid.uuid4()` (Python), `crypto.randomUUID()` (Node), `UUID.randomUUID()` (Java), etc.
- **Persist the key alongside your job/queue record** before the HTTP call, so a retry uses the same key.
- **Treat `507` as success.** Parse the body and proceed as if you'd just created the bot.
- **Generate the key on the client that owns the retry loop** — typically your worker or the entry point of your background job. The key must survive across retry attempts.
- **Use UUIDs**, not auto-incrementing integers or timestamps. UUIDs eliminate the risk of accidental collisions when scaling out workers.

### DON'T

- **Don't reuse a key across different logical creates.** If a user clicks "Join meeting" twice for two different meetings, generate two different keys. If you reuse the same key, the second click will return the first bot's details (wrong meeting!).
- **Don't fire parallel retries with the same key.** Serialize your retry loop.
- **Don't generate the key inside the retry attempt.** A new UUID per attempt defeats the purpose. Generate it once, outside the retry loop.
- **Don't rely on the key as a unique identifier in your own system.** Use the `bot_id` returned by MeetStream for tracking and lookups.

---

## 8) Reference implementations

### Python (with `requests` and `tenacity`)

```python
import uuid
import requests
from tenacity import retry, stop_after_attempt, wait_exponential

API_URL = "https://api.meetstream.ai/api/v1/bots/create_bot"
API_KEY = "<YOUR_API_KEY>"


def create_bot(meeting_link: str, video_required: bool = False) -> dict:
    idempotency_key = str(uuid.uuid4())
    headers = {
        "Authorization": f"Token {API_KEY}",
        "Content-Type": "application/json",
        "Idempotency-Key": idempotency_key,
    }
    payload = {
        "meeting_link": meeting_link,
        "video_required": video_required,
    }
    return _send_with_retries(headers, payload)


@retry(stop=stop_after_attempt(5), wait=wait_exponential(min=1, max=30))
def _send_with_retries(headers: dict, payload: dict) -> dict:
    response = requests.post(API_URL, json=payload, headers=headers, timeout=10)

    if response.status_code in (201, 507):
        return response.json()

    if response.status_code >= 500:
        response.raise_for_status()

    raise ValueError(f"Bot creation failed: {response.status_code} {response.text}")
```

Key points:
- `idempotency_key` is generated **once**, outside the retry decorator, so all retries share the same key.
- Both `201` and `507` are treated as success.
- Only `5xx` triggers a retry; `4xx` errors fail fast (no point retrying a validation error).

### Node.js (with `axios` and `axios-retry`)

```javascript
import axios from "axios";
import axiosRetry from "axios-retry";
import { randomUUID } from "crypto";

const API_URL = "https://api.meetstream.ai/api/v1/bots/create_bot";
const API_KEY = "<YOUR_API_KEY>";

const client = axios.create({ timeout: 10_000 });

axiosRetry(client, {
  retries: 5,
  retryDelay: axiosRetry.exponentialDelay,
  retryCondition: (err) => err.response?.status >= 500 || !err.response,
});

export async function createBot(meetingLink, videoRequired = false) {
  const idempotencyKey = randomUUID();

  const response = await client.post(
    API_URL,
    { meeting_link: meetingLink, video_required: videoRequired },
    {
      headers: {
        Authorization: `Token ${API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      validateStatus: (status) => status === 201 || status === 507,
    }
  );

  return response.data;
}
```

### Worker queue pattern (recommended)

If you're creating bots in response to a queue message (SQS, Kafka, BullMQ, Sidekiq, etc.), generate and **persist** the idempotency key when the message is first written, not when the worker picks it up:

```python
def enqueue_bot_creation(meeting_link: str, user_event_id: str):
    job = {
        "meeting_link": meeting_link,
        "user_event_id": user_event_id,
        "idempotency_key": str(uuid.uuid4()),
    }
    sqs.send_message(QueueUrl=QUEUE_URL, MessageBody=json.dumps(job))


def worker_handler(message: dict):
    job = json.loads(message["Body"])
    response = requests.post(
        API_URL,
        json={"meeting_link": job["meeting_link"]},
        headers={
            "Authorization": f"Token {API_KEY}",
            "Idempotency-Key": job["idempotency_key"],
        },
    )
    assert response.status_code in (201, 507)
```

If the worker crashes after the HTTP call but before acking the message, the queue redelivers the same job — but the same key replays the original bot. No duplicate.

---

## 9) FAQ

**Q: Does the idempotency key work for scheduled bots (`join_at` in the future)?**
A: Yes. `Idempotency-Key` is honored on every call to `POST /api/v1/bots/create_bot`, including scheduled-bot creations. A retry returns the same scheduled bot's details.

**Q: What happens if I send `Idempotency-Key` but no `Authorization` header?**
A: The request is rejected at the auth layer before idempotency is evaluated. No key is bound.

**Q: Can I look up a bot by its idempotency key later?**
A: Not via a dedicated endpoint. The intended use is retry safety, not as a public lookup key. Use the `bot_id` returned in the response to fetch bot details.

**Q: Are keys deleted when a bot is deleted?**
A: The key lives on the bot record. If the bot record is deleted, the key is gone with it and the same key could be reused. **Best practice is still to never reuse keys** — always generate a fresh UUID.

**Q: Does idempotency-key affect billing?**
A: A `507` replay does **not** charge credits — only the original `201` creation did. Failed creates (4xx/5xx) also don't charge.

**Q: What if my client sends the header with a value of `null` or an empty string?**
A: That's treated as if the header was absent. Every call creates a new bot.

**Q: Is there a maximum number of bots I can bind to one key?**
A: One. A key maps to exactly one bot. After that, every future request with that key replays that one bot.

---

## 10) Related docs

- [Create Bot Payload Reference](./MeetStream_Create_Bot_Payload_Reference.md) — full list of body fields accepted by `POST /api/v1/bots/create_bot`.
- [First Bot Quickstart](/guides/get-started/create-your-first-bot) — end-to-end walkthrough of creating your first bot.
- [Bot Lifecycle Webhook Events](/guides/webhooks/webhooks-and-events) — webhook events you'll receive after the bot is created.
- API reference: https://docs.meetstream.ai/api-reference/api-reference/bot-endpoints/create-bot