Developer docs
Retries and idempotency
Networks drop and servers retry. Send an Idempotency-Key and a retried message is answered once and uses one message from your plan, never two.
Idempotency-Key
Add the header to POST /v1/agents/{agentId}/messages. Use one new key per message you want answered, for example a UUID, and reuse the same key only when retrying that same message.
Send with an Idempotency-Key (curl)curl https://api.safecoin.online/v1/agents/support_agent/messages \
-H "Authorization: Bearer $HAVEN_API_KEY" \
-H "Idempotency-Key: 0b8d7c1e-6a54-4f0e-9c2b-3d1f5a7e9b21" \
-H "Content-Type: application/json" \
-d '{"message": "Where is my order?"}'| Situation | What you get |
|---|---|
| First request with a key | Runs normally. |
| Same key, same body, again (a retry) | The first answer again, with the header Idempotent-Replay: true. No second model call and no second message used. |
| Same key, a different body | 422 idempotency_key_reused. Use a new key for a new message. |
| Same key while the first request is still running | 409 request_in_progress with retryAfterSeconds. Wait, then retry with the same key. |
| The first attempt failed (a 5xx, or the AI service was down) | The message is given back and the key is released, so retrying with the same key really tries again. |
| A key that is not 8 to 100 letters, digits, dashes or underscores | 400 invalid_request |
- Keys are private to your account and remembered for 24 hours.
- Protection applies only when you send the header. Without it every call is its own message.
- The streaming
/chatroute takes a key too. A retry replays the first answer as a stream (withIdempotent-Replay: true) and uses no second message. The website chat widget sends a key for every message by itself, so a dropped connection never makes a visitor pay for two answers.
Payments need nothing from you
Payment messages from Stripe, Fapshi and NOWPayments are recorded once per event, and card requests to Stripe carry their own idempotency keys. A repeated payment message never creates a second order, a second payment or a second month of messages.
Rate limits
Each account has a requests-per-minute and a requests-per-day limit (by default 20 a minute and 2,000 a day; your account can differ). Over the limit you get 429 rate_limited with retryAfterSeconds.
Which errors to retry
| Response | Retry? |
|---|---|
429, 409 request_in_progress, 409 conversation_busy | Yes. Wait the suggested time (or a couple of seconds) first. |
5xx, network errors, timeouts | Yes, with the same Idempotency-Key. Back off: 1s, 2s, 4s, 8s, then give up. |
400, 401, 402, 403, 404, 422 | No. Fix the request, the key or the subscription first; retrying the same thing gives the same answer. |
Retry with backoff (Node.js)async function sendWithRetry(message, conversationId) {
const key = crypto.randomUUID();
for (let attempt = 0; attempt < 5; attempt++) {
try {
const res = await fetch('https://api.safecoin.online/v1/agents/support_agent/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HAVEN_API_KEY}`,
'Idempotency-Key': key,
'Content-Type': 'application/json',
},
body: JSON.stringify({ message, conversationId }),
});
if (res.ok) return await res.json();
const body = await res.json();
const retryable = res.status >= 500 || res.status === 429 || body.error === 'request_in_progress' || body.error === 'conversation_busy';
if (!retryable) throw Object.assign(new Error(body.message), { final: true });
await new Promise((r) => setTimeout(r, (body.retryAfterSeconds ?? 2 ** attempt) * 1000));
} catch (err) {
if (err.final) throw err;
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000)); // network error
}
}
throw new Error('Gave up after 5 attempts');
}