Skip to content

Idempotency

A retry after a network hiccup should never send the same SMS twice or order the same number twice. This page describes how the SIPSTACK API behaves today for write operations, and the pattern to keep your own retries safe.

WriteEndpointRetry safety
Send SMS/MMSPOST /api/v2/messages/sendNot deduped — each accepted call is a distinct send billed against your allowance.
Send / schedule a campaignPOST /api/v2/flare/campaigns/{id}/sendGuarded by campaign state, not by a key — see below.
Order a numberPOST /api/v2/numbers/orderNot deduped — a money-moving carrier order. A blind retry can double-order.

POST /api/v2/flare/campaigns/{id}/send only acts on a campaign in draft or paused status. Once a send is accepted the campaign moves to scheduled / queued, so a second call for the same campaign id is rejected rather than re-blasting your list. That state transition is the natural idempotency key — you don’t need to supply one, but you should read the returned status and not retry a campaign that has already left draft/paused.

POST /api/v2/messages/send and POST /api/v2/numbers/order have no server-side dedupe. Two identical requests produce two messages or two number orders. Because a number order is a real, billed carrier purchase, an accidental double-submit costs money — treat these as the operations that most need the safe retry pattern below.

Until a request-idempotency key exists, make retries safe on the client side with generate-a-key-then-check-before-retry:

  1. Generate a dedupe key of your own (a UUID) for each logical operation, and store it alongside the operation in your system before you call the API.
  2. On a failure where you don’t know the outcome (timeout, dropped connection, 5xx), do not blindly re-POST. First check whether the write already happened, then retry only if it didn’t.
import { randomUUID } from 'crypto';
async function sendOnce(op) {
// op.dedupeKey is YOUR key, persisted before the first attempt
const res = await fetch('https://api.sipstack.com/api/v2/messages/send', {
method: 'POST',
headers: { 'x-api-key': process.env.SIPSTACK_API_KEY, 'content-type': 'application/json' },
body: JSON.stringify({ to: op.to, message: op.message }),
});
if (res.ok) {
const { messageId } = (await res.json()).data ?? {};
await markSent(op.dedupeKey, messageId); // record success in your DB
return messageId;
}
throw new Error(`send failed: ${res.status}`);
}
async function sendWithSafeRetry(op) {
if (await alreadySent(op.dedupeKey)) return; // never re-send
try {
await sendOnce(op);
} catch (err) {
// Unknown outcome? Verify against the API before retrying.
if (await confirmSentViaHistory(op)) { // see "Check before retry"
await markSent(op.dedupeKey);
return;
}
throw err; // genuinely not sent — safe to retry this op later
}
}

To confirm whether a write landed before retrying, query the read side:

  • Sends — list GET /api/v2/messages filtered by contact / number and a tight from_date, and look for a message matching your operation in the seconds around your attempt.
  • Campaign sends — GET /api/v2/flare/campaigns/{id} and inspect status; if it has left draft/paused, the send already took.
  • Number orders — list GET /api/v2/numbers and check whether the number you tried to order is now on your account.

Only retry when the read side confirms the write did not happen.

Idempotency cuts both ways. Just as your retries to the API can duplicate a write, SIPSTACK’s webhook retries can deliver the same event to you more than once. Webhook deliveries carry an idempotency_key (and occurred_at) in the body specifically so you can de-duplicate them — see Webhooks → Idempotency. De-dupe inbound events on that key or on a natural key like messageId + event.