Skip to the page

API basics

Idempotency

Send an Idempotency-Key with every POST so a retry after a dropped connection gets the first answer instead of sending a second text.

A network can drop a connection after BlueBot accepted your text but before the answer reached you. Your code sees an error and sends again, and without protection the person gets the text twice. An Idempotency-Key header prevents that: BlueBot keeps the first answer under the key for 24 hours, and a retry with the same key gets that answer back instead of doing the work again.

POST/v1/messagesSend a text →
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 is tomorrow at 10:30 AM.",
    "metadata": {
      "appointment_id": "7712"
    }
  }'

Send it again, byte for byte the same, within 24 hours, and the answer is the same 201 with the same message, plus the header Idempotent-Replayed: true. Maria gets one text.

Which requests take one

Every POST: sending a text, uploading a file, canceling or settling a text, adding to the stop list, and making, testing, rotating or replaying webhooks. BlueBot ignores the header on GET, PATCH and DELETE, which are safe to repeat as they are: reading twice changes nothing, and a PATCH sent twice sets the same values twice.

Send one on every POST, not only on sends. A retried send_again without a key can queue two new texts.

Choosing a key

A key is any string of 1 to 255 characters. What matters is that one key stands for one thing you mean to do:

  • Build it from your own record and intent. appt-7712-reminder, order-10293-shipped, lead-48213-welcome. A retry of the same job makes the same key, even from a different process or after a restart.
  • Put the date in a key for something that repeats. A daily summary to the same person needs daily-summary-48213-2026-11-13, or tomorrow’s text is answered with today’s.
  • Or make a random UUID when there’s no record to name, and keep it with the job so a retry reuses it rather than making a new one.

Keys belong to the account, not to one API key, so two programs on the same account must not make the same key for different things. Starting each with the program’s name (booking-, crm-) keeps them apart.

What a retry gets

What happened the first timeWhat a retry with the same key gets
SuccessThe same status and body, with Idempotent-Replayed: true. Nothing is done again.
A refusal after BlueBot read the key, such as a number it can’t read or a file that isn’t thereThe same refusal, replayed. Fix the request and send it with a new key.
A refusal before BlueBot read the key: a malformed body, a missing field, a bad key, a missing scope, a rate limitThe request runs fresh. These refusals don’t use the key.
A server error (5xx)The request runs fresh. A server error lets the key go so a retry can succeed.
Still running409 conflict with Retry-After: 1. Wait a second and send the same request again.

So the rule for a loop is simple: on a network error, a timeout or a 5xx, send the same request with the same key until you get an answer. On a 4xx, read the error, fix the request, and use a new key.

Reusing a key for a different request

A key stands for one request. Using it in the last 24 hours with a different request answers 409 idempotency_conflict:

Response409A key used for something else
{
  "error": {
    "code": "idempotency_conflict",
    "message": "This Idempotency-Key was already used for a different request in the last 24 hours.",
    "hint": "This Idempotency-Key was used in the last 24 hours with a different request. Use a new key for a new request.",
    "doc_url": "https://usebluebot.com/docs/errors/idempotency_conflict",
    "request_id": "req_1Hq2LxW9cVbN3mKpRtYs0a"
  }
}

BlueBot compares the method, the path and the exact bytes of the body. Two bodies that mean the same thing but differ in spacing or key order count as different requests. Build the body once and send those same bytes on every retry; the SDKs do this for you.

A file upload (POST /v1/files) is the exception, because every multipart body carries a boundary your HTTP library picks at random. For an upload BlueBot compares the parts instead: each field’s name and value, and each file’s field name, file name, type and contents. The order of the fields doesn’t count. Sending the same file again with the same key replays the first answer, whatever boundary the retry used.

This is also what keeps a key from being reused by mistake. If a job meant to text Maria and a job meant to text her husband both made the key appt-7712-reminder, the second is refused rather than answered with Maria’s message.

You gotIt meansDo next
201The first time with this key. It ran.Keep going.
201 with Idempotent-Replayed: trueA retry. This is the first answer, and nothing was sent again.Treat it as the answer you missed.
409 idempotency_conflictThe key was used in the last 24 hours with a different request.Use a new key. If you meant the same request, send the same bytes (for an upload, the same fields and file).
409 conflict with Retry-After: 1The first request with this key hasn’t finished.Wait a second and retry with the same key.
400 invalid_request naming Idempotency-KeyThe key is empty or over 255 characters.Send 1 to 255 characters.

How long a key lasts

24 hours from its first use. After that the key is free again, and a request with it runs as new. Retries belong in minutes and hours, not days; a job that may retry after a day should check whether its text went (by its metadata or the message id it stored) before sending again.

In the SDKs

The TypeScript and Python SDKs put an Idempotency-Key on every POST for you: a new random key for each call, reused for that call’s own retries. That covers the SDK retrying after a dropped connection. It doesn’t cover your own program running a job twice, because the second run makes a new call with a new key. Pass your own key for that:

send.ts
const message = await client.messages.send(
    { to: '+15555550123', text: 'Your order 10293 has shipped.' },
    { idempotencyKey: 'order-10293-shipped' },
);
send.py
message = client.messages.send(
    to="+15555550123",
    text="Your order 10293 has shipped.",
    idempotency_key="order-10293-shipped",
)

The Zapier, Make, n8n and Pipedream apps build a key from the run and the step, so a step that’s retried doesn’t send twice. The CLI’s bluebot send makes one for each command you run.