Skip to content

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:

EndpointOperation
POST /v1/messagesSend a message
POST /v1/messages/batchSend a batch of messages
POST /v1/channelsConnect a channel
POST /v1/webhook_endpointsCreate a webhook endpoint
POST /v1/webhook_endpoints/{id}/roll_secretRoll the signing secret
POST /v1/webhook_endpoints/{id}/testSend a test event
POST /v1/contactsCreate a contact
POST /v1/contacts/upsertCreate or update a contact by phone number
POST /v1/contacts/tagsAdd and remove tags in bulk
POST /v1/contact_listsCreate a contact list
POST /v1/contact_lists/{id}/membersAdd contacts to a list
POST /v1/contact_lists/{id}/members/removeRemove contacts from a list
POST /v1/campaignsCreate a campaign
POST /v1/campaigns/{id}/launchLaunch a campaign
POST /v1/campaigns/{id}/pausePause a campaign
POST /v1/campaigns/{id}/resumeResume a campaign
POST /v1/campaigns/{id}/cancelCancel a campaign
POST /v1/integration_sourcesRegister an integration source
POST /v1/integration_sources/{id}/roll_keyRoll the key of an integration source
POST /v1/automation_eventsPush an event
POST /v1/automation_events/batchPush a batch of events
POST /v1/ingest/{platform}/{source_id}Receive a native platform webhook
POST /v1/test/inbound_messagesSimulate an inbound message

GET, PATCH and DELETE requests are idempotent by nature and ignore the header.

What the server does#

SituationResult
First request with a keyProcessed normally. The response is stored for 24 hours.
Same key, same body, within 24 hoursThe stored response is returned with the original status code and the header Idempotent-Replayed: true. Nothing is sent or charged again.
Same key, different body409 idempotency_key_reused. The request is not processed.
Same key while the first request is still running409 idempotency_key_in_use. Retry shortly.
Same key after 24 hoursTreated 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 207 of 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 a 402, you can reuse the key.
A replayed response
HTTP/1.1 202 Accepted
Idempotent-Replayed: true
X-Request-Id: req_0aB3cD6eF9gH2iJ5kL8m

Retry 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.

    Loading