DocumentationRetry safely
How to retry API calls without posting twice
A request can time out after BuzzTake has already queued the post, and a busy script can hit the rate limit. Make every call safe to repeat: send an Idempotency-Key, wait for the result instead of sending again, and follow Retry-After.
You need a BuzzTake API key. The examples post through one account.
1. Send an Idempotency-Key with every write
POST/v1/accounts/{id}/actions
The Idempotency-Key header is 1 to 255 visible ASCII characters, with no spaces. Make it from your own record, such as post-row-1842, so the same job always sends the same key.
curl -i https://api.buzztake.com/v1/accounts/$ACCOUNT_ID/actions \
-H "Authorization: Bearer $BUZZTAKE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: post-row-1842" \
-d '{ "type": "post", "text": "Dark mode is live.", "wait": 60 }'A repeat with the same key, the same type and the same parameters creates nothing. It returns the first action as it is now, with the header idempotent-replayed: true, and charges nothing more.
HTTP/2 200
content-type: application/json
idempotent-replayed: true
{ "id": "0199e5f6-...", "type": "post", "status": "succeeded", "result": { "type": "created", "url": "..." } }- A key belongs to one account. The same key on another account starts a new action.
- Keys do not expire. A key used last month still returns last month's action.
- On the batch route, one key covers every account in the batch, and each account gets its own action under it.
The same key with a different body is refused, and nothing runs:
{
"type": "https://buzztake.com/problems/idempotency-conflict",
"title": "Idempotency-Key reused",
"status": 409,
"code": "idempotency_conflict",
"detail": "This Idempotency-Key was already used for a different request on this account. Send a new key."
}2. Wait for the result instead of sending again
wait holds the request for up to 120 seconds. 200 means the action finished; 202 means it is still queued or running. A 202 is not a failure: read the action again, and do not send the write again.
curl "https://api.buzztake.com/v1/actions/$ACTION_ID?wait=60" \
-H "Authorization: Bearer $BUZZTAKE_API_KEY"Set your HTTP client's timeout above wait: the server gives these routes 130 seconds. If the connection drops before you see the action's id, send the same request with the same key. You get the action that was already queued.
3. Back off on 429 and 503
Each API key may send 600 requests a minute, and each client network 300: one IPv4 /24 or one IPv6 /64. Over either limit, the answer is 429:
HTTP/2 429
content-type: application/problem+json
retry-after: 60
{
"type": "https://buzztake.com/problems/rate-limit",
"title": "Too many requests",
"status": 429,
"detail": "This route's rate limit is spent. Try again after retryAfterSeconds, or at retryAt.",
"retryAfterSeconds": 60,
"retryAt": "2026-10-30T09:01:00Z"
}Wait for the Retry-After seconds, then send the same request with the same key. A 503, "A required service is temporarily unavailable.", gets the same treatment. A request that fails this way creates no action and costs nothing.
429: the action runs and
fails with error.code: "rate_limited". Step 4 covers it.
4. Put it together
This helper sends one action with a key, retries on network errors, 429 and 503 with the same key, and reads the action until it stops running. Other 4xx answers stop it, because the request itself needs a fix.
JavaScript
import { randomUUID } from "node:crypto";
const api = "https://api.buzztake.com/v1";
const auth = { Authorization: `Bearer ${process.env.BUZZTAKE_API_KEY}` };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Runs one action and returns it once it stops running. Safe to call again with the same key.
async function runAction(accountId, body, key = randomUUID()) {
const headers = { ...auth, "Content-Type": "application/json", "Idempotency-Key": key };
let action;
for (let attempt = 1; !action; attempt++) {
let res;
try {
res = await fetch(`${api}/accounts/${accountId}/actions`, {
method: "POST",
headers,
body: JSON.stringify({ ...body, wait: 60 }),
signal: AbortSignal.timeout(130_000),
});
} catch (error) {
if (attempt === 5) throw error;
await sleep(2 ** attempt * 1000); // No answer: send the same request and key again.
continue;
}
if (res.status === 429 || res.status === 503) {
if (attempt === 5) throw new Error(`${res.status} after 5 attempts`);
await sleep(Number(res.headers.get("retry-after") ?? 2 ** attempt) * 1000);
continue;
}
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); // Fix the request.
action = await res.json();
}
while (action.status === "queued" || action.status === "running") {
const res = await fetch(`${api}/actions/${action.id}?wait=60`, {
headers: auth,
signal: AbortSignal.timeout(130_000),
});
if (res.status === 429) {
await sleep(Number(res.headers.get("retry-after") ?? 60) * 1000);
continue;
}
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
action = await res.json();
}
return action; // succeeded, failed, canceled or needs_input
}
const action = await runAction(
process.env.ACCOUNT_ID,
{ type: "post", text: "Dark mode is live." },
"post-row-1842",
);
console.log(action.status, action.result?.url ?? action.error?.code ?? "");Python
import os
import time
import uuid
import requests
API = "https://api.buzztake.com/v1"
AUTH = {"Authorization": f"Bearer {os.environ['BUZZTAKE_API_KEY']}"}
def run_action(account_id, body, key=None):
"""Runs one action and returns it once it stops running. Safe to call again with the same key."""
headers = {**AUTH, "Idempotency-Key": key or str(uuid.uuid4())}
action = None
for attempt in range(1, 6):
try:
resp = requests.post(
f"{API}/accounts/{account_id}/actions",
headers=headers,
json={**body, "wait": 60},
timeout=130,
)
except requests.RequestException:
if attempt == 5:
raise
time.sleep(2 ** attempt) # No answer: send the same request and key again.
continue
if resp.status_code in (429, 503):
if attempt == 5:
raise SystemExit(f"{resp.status_code} after 5 attempts")
time.sleep(int(resp.headers.get("Retry-After", 2 ** attempt)))
continue
if not resp.ok:
raise SystemExit(f"{resp.status_code}: {resp.text}") # Fix the request.
action = resp.json()
break
while action["status"] in ("queued", "running"):
resp = requests.get(
f"{API}/actions/{action['id']}", params={"wait": 60}, headers=AUTH, timeout=130
)
if resp.status_code == 429:
time.sleep(int(resp.headers.get("Retry-After", 60)))
continue
if not resp.ok:
raise SystemExit(f"{resp.status_code}: {resp.text}")
action = resp.json()
return action # succeeded, failed, canceled or needs_input
action = run_action(os.environ["ACCOUNT_ID"], {"type": "post", "text": "Dark mode is live."}, "post-row-1842")
print(action["status"], (action.get("result") or {}).get("url") or (action.get("error") or {}).get("code"))5. Decide which failed actions to run again
A key returns its first action whatever its status, so the same key never runs a failed action again. When an action failed and you want another attempt, send a new key. Read error.code first:
| error.code | Means | Next |
|---|---|---|
browser_unavailable browser_error | No browser could start, or it failed during the run. | Run it again with a new key. If browser_error repeats, send feedback. |
rate_limited ui_changed | The platform asked the account to slow down, or the page looked different. | Run it again later with a new key. |
signed_out login_failed account_blocked challenge_timeout | The account cannot act until someone signs it in. | Fix the account first; see the guide on signed-out accounts. Then use a new key. |
invalid_input unsupported not_found rejected | The request or its target is wrong for this platform. | Change the request. Sending it again unchanged fails the same way. |
insufficient_credits | The balance could not pay when the action started. | Add credits, then run it again with a new key. |
A write that failed before the page changed is refunded, so a second attempt does not pay twice for the first. A write that failed after it changed the page is not refunded: check the account before you post again.
Common errors
- 422 invalid_input
- "
Idempotency-Keyis 1 to 255 visible ASCII characters." - 422 invalid_input
- "
waitis a whole number of seconds from 0 to 120." - 409 idempotency_conflict
- The key was used for a different request on this account. Send a new key.
- 409 without a code
- Two requests with the same key arrived at the same moment. Read the account's actions, or send the request again after a second: the repeat returns the action that won.
See also
- Request errors
- Action error codes
- How to post to many social media accounts at once
- How to monitor mentions of your brand and reply
- How to automate a task that has no action, with browser steps
To run this on your own accounts, create a BuzzTake account, then add an account and an API key.