Skip to content

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?"}'

What happens with a key
SituationWhat you get
First request with a keyRuns 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 body422 idempotency_key_reused. Use a new key for a new message.
Same key while the first request is still running409 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 underscores400 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 /chat route takes a key too. A retry replays the first answer as a stream (with Idempotent-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

Retry advice
ResponseRetry?
429, 409 request_in_progress, 409 conversation_busyYes. Wait the suggested time (or a couple of seconds) first.
5xx, network errors, timeoutsYes, with the same Idempotency-Key. Back off: 1s, 2s, 4s, 8s, then give up.
400, 401, 402, 403, 404, 422No. Fix the request, the key or the subscription first; retrying the same thing gives the same answer.
Node.js
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');
}