API conventions
Idempotency
Send an Idempotency-Key header with POST requests so that a retry after a timeout or network error never sends a message twice.
Why it matters#
When a request times out, you cannot know whether the server processed it. Retrying blindly risks a duplicate message and a duplicate charge; not retrying risks losing the message. An idempotency key removes the dilemma: the server remembers the first result for that key and returns it for every repeat.
How to use it#
Add an Idempotency-Key header with a unique string of up to 255 characters. A random UUID works. A value derived from your own data, such as order-1042-shipped, is often better, because it also protects you from enqueueing the same job twice.
curl https://api.omnimessage.co/v1/messages \
-H "Authorization: Bearer om_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-shipped" \
-d '{
"channel": "ch_7Hq2mN5vB8cX1zL0pK3j",
"to": "+971501234567",
"type": "text",
"text": {
"body": "Your code is 482910"
},
"reference": "order-1042",
"metadata": {
"user_id": "u_17"
}
}'The header is accepted on every POST endpoint:
GET, PATCH and DELETE requests are idempotent by nature and ignore the header.
What the server does#
| Situation | Result |
|---|---|
| First request with a key | Processed normally. The response is stored for 24 hours. |
| Same key, same body, within 24 hours | The stored response is returned with the original status code and the header Idempotent-Replayed: true. Nothing is sent or charged again. |
| Same key, different body | 409 idempotency_key_reused. The request is not processed. |
| Same key while the first request is still running | 409 idempotency_key_in_use. Retry shortly. |
| Same key after 24 hours | Treated as a new request. |
- Keys are scoped to your account. Two accounts can use the same key without conflict.
- The body comparison is exact. Send the identical payload when you retry.
- Only successful responses are stored (2xx, including the
207of a batch). A request that ended in an error left nothing behind, so sending it again with the same key processes it afresh. After fixing the cause, for example topping up after a402, you can reuse the key.
HTTP/1.1 202 Accepted
Idempotent-Replayed: true
X-Request-Id: req_0aB3cD6eF9gH2iJ5kL8mRetry safely#
Generate the key once per logical operation, outside the retry loop, and reuse it for every attempt.
import { randomUUID } from 'node:crypto';
async function sendWithRetry(params) {
// One key per logical send, reused for every attempt.
const idempotencyKey = randomUUID();
for (let attempt = 0; attempt < 4; attempt += 1) {
try {
const response = await fetch('https://api.omnimessage.co/v1/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OMNIMESSAGE_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(params),
signal: AbortSignal.timeout(10_000),
});
const data = await response.json();
if (response.ok) return data;
const retryable = response.status === 429 || response.status >= 500 || data.error.code === 'idempotency_key_in_use';
if (!retryable) throw Object.assign(new Error(data.error.message), { code: data.error.code, fatal: true });
} catch (error) {
if (error.fatal) throw error;
// Network error or timeout: the request may or may not have been processed. Retry with the same key.
}
await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
}
throw new Error('Message could not be sent after 4 attempts');
}Batches#
On POST /v1/messages/batch the key covers the whole batch. A replay returns the stored 207 response with the original per-item results. To resend only the items that were rejected, build a new batch and use a new key.