Idempotency
Idempotency
Section titled “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.
Current Behavior by Endpoint
Section titled “Current Behavior by Endpoint”| Write | Endpoint | Retry safety |
|---|---|---|
| Send SMS/MMS | POST /api/v2/messages/send | Not deduped — each accepted call is a distinct send billed against your allowance. |
| Send / schedule a campaign | POST /api/v2/flare/campaigns/{id}/send | Guarded by campaign state, not by a key — see below. |
| Order a number | POST /api/v2/numbers/order | Not deduped — a money-moving carrier order. A blind retry can double-order. |
Campaign send has a natural state guard
Section titled “Campaign send has a natural state guard”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.
Send and number order are not deduped
Section titled “Send and number order are not deduped”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.
The Safe Retry Pattern
Section titled “The Safe Retry Pattern”Until a request-idempotency key exists, make retries safe on the client side with generate-a-key-then-check-before-retry:
- 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.
- 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 }}Check before retry
Section titled “Check before retry”To confirm whether a write landed before retrying, query the read side:
- Sends — list
GET /api/v2/messagesfiltered bycontact/numberand a tightfrom_date, and look for a message matching your operation in the seconds around your attempt. - Campaign sends —
GET /api/v2/flare/campaigns/{id}and inspectstatus; if it has leftdraft/paused, the send already took. - Number orders — list
GET /api/v2/numbersand 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.
Inbound Webhooks Are the Mirror Image
Section titled “Inbound Webhooks Are the Mirror Image”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.