Idempotency
Retry safely: the Idempotency-Key header makes sure a send, campaign or OTP runs at most once.
Networks fail. If a request times out you can’t know whether the message left. Send Idempotency-Key with every write and retry with the same key: Mursilat returns the first answer instead of doing the work again.
Idempotency-Key: order-1042-shipped| Situation | Answer |
|---|---|
| First request | AnswerRuns normally. |
| Same key + same body (within 24 h) | AnswerThe stored answer, plus Idempotent-Replayed: true. Nothing is sent again. |
| Same key + different body | Answer409 IDEMPOTENCY_KEY_REUSED — use a new key for a new request. |
| Same key while the first is still running | Answer409 IDEMPOTENCY_IN_PROGRESS — wait a moment and retry. |
| Missing on a send / campaign / OTP | Answer400 IDEMPOTENCY_KEY_REQUIRED. |
| Longer than 191 or not visible ASCII | Answer400 IDEMPOTENCY_KEY_INVALID. |
Where it is required
Required on POST /v2/messages, POST /v2/campaigns and POST /v2/otp. Optional but accepted on every other write. Keys are scoped to the API key that used them: another key, even of the same account, never replays an answer.
Choosing keys
- Derive the key from your own business event so retries naturally reuse it:
order-1042-shipped,otp-login-<sessionId>. - Don’t use a random key per attempt — that defeats the purpose.
- Replays return ids and statuses only (never message text, captions or URLs).
A safe retry loop
async function sendWithRetry(body, key, attempts = 4) {
for (let i = 0; i < attempts; i++) {
const res = await fetch('https://mursilat.com/api/v2/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WASLA_API_KEY}`,
'Idempotency-Key': key, // the SAME key on every attempt
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
}).catch(() => null); // network error → retry
if (res && res.ok) return res.json();
if (res && res.status < 500 && res.status !== 409 && res.status !== 429) {
throw new Error((await res.json()).error.code); // won't succeed on retry
}
const wait = Number(res?.headers.get('Retry-After')) || 2 ** i;
await new Promise((r) => setTimeout(r, wait * 1000));
}
throw new Error('gave up');
}