Skip to navigation

Deduplication & Idempotency: Never Double-Book a Meeting Bot

View as Markdown

Retry create_bot safely and guarantee one bot per meeting, even when your workers crash, time out or run twice.

MechanismWhere you send itBest for
Idempotency-KeyHTTP headerRetrying the same HTTP request after network errors or timeouts
deduplication_keyJSON request bodyBinding one bot per logical meeting or event in your own system

Both are optional. If you omit them, every call creates a new bot.

Scope

  • Idempotency-Key: POST /api/v1/bots/create_bot only
  • deduplication_key: POST /api/v1/bots/create_bot and POST /api/v1/calendar/schedule/{event_id}

Platform support

ZoomGoogle MeetMicrosoft Teams
Idempotency-KeyYesYesYes
deduplication_keyYesYesYes
Immediate and scheduled (join_at) botsYesYesYes

Both checks run in the API before the bot is dispatched, so they behave the same on every platform. See the capability matrix for platform-specific features.


At a glance

Idempotency-Key (header)deduplication_key (body)
Scoped to(your account, key)(your account, key)
First callHTTP 201: bot createdHTTP 201: bot created
RepeatHTTP 507 with the original bot: treat as successHTTP 200 with the original bot
Key lifetimeNever expiresNever expires
Request body on repeatIgnored entirelyIgnored, except meeting_link is compared
Different meeting on repeatReturns the original bot anywayHTTP 409 with an explicit error
Typical key sourceOne UUID per logical create, reused across its retriesYour own ID (calendar event occurrence, CRM record, and so on)
Rule of thumb: use Idempotency-Key for transport-level retries; use deduplication_key when your app already has a stable ID for “this meeting should have one bot.”

For every request field, see the create_bot API reference.


deduplication_key (request body)

Pass a string in the JSON body to say: “for this account, this key should always map to the same bot.”

Example: first create

curl -X POST "https://api.meetstream.ai/api/v1/bots/create_bot" \
-H "Authorization: Token <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"meeting_link": "https://meet.google.com/abc-defg-hij",
"bot_name": "Notetaker",
"deduplication_key": "crm-opportunity-88421",
"video_required": false
}'

Response (HTTP 201):

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

Example: repeat with the same key and meeting

curl -X POST "https://api.meetstream.ai/api/v1/bots/create_bot" \
-H "Authorization: Token <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"meeting_link": "https://meet.google.com/abc-defg-hij",
"bot_name": "Notetaker",
"deduplication_key": "crm-opportunity-88421"
}'

Response (HTTP 200):

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

Same bot_id as the first call. No new bot. No extra credits.

The repeat response has the same shape as a fresh create, with two differences: transcript_id is always null, and status is the bot’s current status (for example InMeeting, or Stopped if that meeting is over). Fetch transcript_id from GET /api/v1/bots/{bot_id}/detail if you need it.

Example: same key, different meeting (error)

If you reuse a deduplication_key with a different meeting_link, MeetStream returns HTTP 409:

{
"error": "deduplication_key already bound to a different meeting",
"existing_bot_id": "bot_a1b2c3d4...",
"existing_meeting_url": "https://meet.google.com/abc-defg-hij"
}

This is deliberate: it surfaces a client bug instead of silently joining the wrong meeting. The comparison is an exact string match against the link stored on the bot, so send the link in the same form every time (for example, do not add or drop the ?pwd= part of a Zoom link between calls).

Field rules

RuleValue
RequiredNo
Typestring
Length1 to 2000 characters
CharactersASCII printable only (0x20 to 0x7E)
ScopePer MeetStream account (API key owner)

Good key choices: calendar event ID plus occurrence start time, CRM opportunity ID, internal job ID, user_id + meeting_start_time.

Avoid: timestamps that change on every click, auto-increment integers shared across workers without coordination, and a bare recurring-event ID. Keys never expire, so reusing a series ID for next week’s occurrence returns last week’s finished bot instead of creating a new one.

Calendar scheduling

deduplication_key also works on POST /calendar/schedule/{event_id}. Put it in bot_config:

{
"bot_config": {
"deduplication_key": "google-cal-event-abc123-2026-10-01T15:00Z",
"bot_name": "MeetStream"
}
}

A repeat returns HTTP 200 with the existing scheduled bot (bot_id, schedule_id, scheduled_time). Separately, scheduling an event that already has a scheduled bot returns HTTP 409 with "error": "A bot is already scheduled for this event". See scheduling bots.


Idempotency-Key (HTTP header)

Pass a unique string in the Idempotency-Key header to safely retry the same HTTP request without creating duplicate bots.

Example

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",
"bot_name": "Notetaker",
"video_required": false
}'
  • First call: HTTP 201
  • Retry with the same header: HTTP 507 with the original bot (treat as success)

The 507 body:

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

status is the original bot’s current status. A repeat returns the original bot even if the rest of the body changed (the body still has to pass basic request validation).

Generate one UUID per logical create attempt, persist it across retries, and accept both 201 and 507 as success.


Using both together

You may send both on the same request. MeetStream checks Idempotency-Key first, then deduplication_key.

Typical pattern:

{
"meeting_link": "https://meet.google.com/abc-defg-hij",
"deduplication_key": "crm-opportunity-88421"
}
Idempotency-Key: 9b1c3f4a-7e2b-4d8f-9a1c-2e6f4b8a0c11
  • deduplication_key: a stable business ID for this meeting (survives across days and re-schedules)
  • Idempotency-Key: one UUID for this logical create, reused by every retry of it

Which one should I use?

ScenarioRecommendation
HTTP client retry after timeout / 5xxIdempotency-Key
Queue worker redelivery (SQS, Kafka, etc.)Idempotency-Key (persist key with the job)
“One bot per calendar event”deduplication_key set to your event ID
”One bot per CRM record”deduplication_key set to your record ID
Maximum safetyBoth: business ID in the body, UUID in the header

Best practices

DO

  • Generate one UUID v4 per logical create: uuid.uuid4() (Python), crypto.randomUUID() (Node), UUID.randomUUID() (Java).
  • Persist the key alongside your job or queue record before the HTTP call, so a retry reuses the same key.
  • Treat 507 (and 200 for deduplication_key) as success: parse the body and proceed as if you just created the bot.
  • Generate the key on the client that owns the retry loop (your worker, or the entry point of your background job) so it survives across attempts.

DON’T

  • Don’t reuse a key across different logical creates. Two different meetings → two different keys, or the second call replays the first bot (wrong meeting).
  • Don’t fire parallel retries with the same key. Serialize your retry loop (there is no in-flight race protection).
  • Don’t generate the key inside the retry attempt: a new UUID per attempt defeats the purpose. Generate it once, outside the loop.
  • Don’t rely on the key as an identifier in your own system. Track by the bot_id MeetStream returns.

Reference implementations

Python (requests + tenacity)

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()) # once, outside the retry loop
headers = {
"Authorization": f"Token {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
}
payload = {"meeting_link": meeting_link, "bot_name": "Notetaker", "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): # both are success
return response.json()
if response.status_code >= 500: # retry only on 5xx
response.raise_for_status()
raise ValueError(f"Bot creation failed: {response.status_code} {response.text}")

Node.js (axios + axios-retry)

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(); // once, before the retry loop
const response = await client.post(
API_URL,
{ meeting_link: meetingLink, bot_name: "Notetaker", video_required: videoRequired },
{
headers: {
Authorization: `Token ${API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
validateStatus: (status) => status === 201 || status === 507,
}
);
return response.data;
}

Generate and persist the key when the job is enqueued, not when the worker picks it up, so a redelivered message reuses the same key:

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"], "bot_name": "Notetaker"},
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 queue redelivers the job, and the same key returns the original bot. No duplicate.


Shared guarantees and limits

Both mechanisms share these behaviors:

  1. Per-account isolation: the same key value used by two different MeetStream accounts does not collide.
  2. No protection against parallel first calls: two simultaneous first creates with the same key may both succeed, and there is no 409 for a request still in flight. Serialize retries in your client.
  3. Failed creates are not remembered: if the original request failed before the bot was created (validation error, low balance, and so on), a retry runs as a fresh create.
  4. The lookup fails open: on a rare internal lookup error, MeetStream creates the bot rather than blocking the request.
  5. Keys never expire: a key stays bound to its bot for good. Generate a new key for each distinct logical create.

FAQ

Q: Does a replay charge credits? A: No. Only the original successful create charges credits.

Q: Can I look up a bot by deduplication_key or Idempotency-Key later? A: Not via a dedicated API endpoint. Use the bot_id returned in the response.

Q: What HTTP codes should my client treat as success? A:

  • With Idempotency-Key only: 201 and 507
  • With deduplication_key only: 201 and 200
  • With both: 201, 200, and 507

Q: Why 507 for a repeat? A: It is MeetStream’s signal that nothing new was created. Many HTTP clients treat 5xx as an error or retry it automatically, so allow 507 explicitly (as in the examples above) and do not retry on it.

Q: Does deduplication_key work for scheduled bots (join_at)? A: Yes, on POST /api/v1/bots/create_bot with a future join_at, and on calendar schedule endpoints.

Q: What if I omit both? A: Every call creates a new bot, same as before these features existed.