Skip to content

Resources

Libraries

The official Node.js SDK and MCP server, ready-made integrations, and copy-and-paste clients for other languages. The API is plain JSON over HTTPS.

Official packages#

For TypeScript and JavaScript there is an official SDK, @omnimessage/sdk, and AI agents can use the OmniMessage MCP server. Automation platforms and WordPress have their own integrations.

Other languages#

There are no official packages for other languages yet; PHP and Python SDKs are planned. Every endpoint can be called with the HTTP client your language already has, and the API reference shows each request in cURL, Node.js, Python, PHP and Go.

If you prefer a small wrapper over raw HTTP calls, copy one of the clients below into your project. Each handles authentication, idempotency keys, error objects, retries on 429 and safe retries on server errors, and cursor pagination.

Node.js#

A typed client built on the global fetch of Node.js 18 and later. No dependencies. For a maintained package with every resource, use the Node.js SDK instead.

omnimessage.ts
// omnimessage.ts — a minimal typed client. Requires Node.js 18 or later (global fetch).
const BASE_URL = 'https://api.omnimessage.co/v1';

export interface ApiErrorBody {
  type: string;
  code: string;
  message: string;
  param?: string;
  details?: { param: string; code: string; message: string }[];
  request_id: string;
  doc_url: string;
}

export class OmniMessageError extends Error {
  constructor(
    readonly status: number,
    readonly body: ApiErrorBody,
    readonly retryAfter?: number,
  ) {
    super(`${body.code}: ${body.message} (${body.request_id})`);
    this.name = 'OmniMessageError';
  }
}

export interface Message {
  id: string;
  object: 'message';
  mode: 'live' | 'test';
  channel_id: string;
  channel_type: string;
  direction: 'outbound' | 'inbound';
  to: string;
  from: string;
  type: string;
  content: Record<string, unknown>;
  status: 'queued' | 'sending' | 'sent' | 'delivered' | 'read' | 'failed' | 'received';
  error: { code: string; message: string; provider_code?: string | null } | null;
  reference: string | null;
  metadata: Record<string, string>;
  billing: { source: 'package' | 'wallet' | 'none'; amount_micros: number; package_grant_id: string | null; refunded: boolean };
  created_at: string;
  sent_at: string | null;
  delivered_at: string | null;
  read_at: string | null;
  failed_at: string | null;
}

export interface List<T> {
  object: 'list';
  data: T[];
  has_more: boolean;
  next_cursor: string | null;
}

export type SendMessageParams = {
  channel: string;
  to: string;
  type: string;
  reply_to?: string;
  reference?: string;
  metadata?: Record<string, string>;
  // The content object lives under the key named by `type`.
  [content: string]: unknown;
};

type RequestOptions = {
  query?: Record<string, string | number | undefined>;
  body?: unknown;
  idempotencyKey?: string;
};

export class OmniMessage {
  constructor(
    private readonly apiKey: string,
    private readonly maxRetries = 2,
  ) {}

  async request<T>(method: string, path: string, options: RequestOptions = {}): Promise<T> {
    const url = new URL(BASE_URL + path);
    for (const [key, value] of Object.entries(options.query ?? {})) {
      if (value !== undefined) url.searchParams.set(key, String(value));
    }

    for (let attempt = 0; ; attempt += 1) {
      const response = await fetch(url, {
        method,
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          ...(options.body !== undefined && { 'Content-Type': 'application/json' }),
          ...(options.idempotencyKey && { 'Idempotency-Key': options.idempotencyKey }),
        },
        body: options.body === undefined ? undefined : JSON.stringify(options.body),
        signal: AbortSignal.timeout(15_000),
      });

      const payload = await response.json().catch(() => undefined);
      if (response.ok) return payload as T;

      const retryAfter = Number(response.headers.get('Retry-After')) || undefined;
      // Retry only what is safe: rate limits always, server errors when the request is idempotent.
      const idempotent = method === 'GET' || Boolean(options.idempotencyKey);
      const retryable = response.status === 429 || (response.status >= 500 && idempotent);
      if (retryable && attempt < this.maxRetries) {
        await new Promise((resolve) => setTimeout(resolve, (retryAfter ?? 2 ** attempt) * 1000));
        continue;
      }

      const body: ApiErrorBody = payload?.error ?? {
        type: 'api_error',
        code: 'internal_error',
        message: `Unexpected response with status ${response.status}`,
        request_id: response.headers.get('X-Request-Id') ?? '',
        doc_url: '',
      };
      throw new OmniMessageError(response.status, body, retryAfter);
    }
  }

  sendMessage(params: SendMessageParams, idempotencyKey: string = crypto.randomUUID()) {
    return this.request<Message>('POST', '/messages', { body: params, idempotencyKey });
  }

  getMessage(id: string) {
    return this.request<Message>('GET', `/messages/${encodeURIComponent(id)}`);
  }

  async *listMessages(filters: Record<string, string | number | undefined> = {}): AsyncGenerator<Message> {
    let startingAfter: string | undefined;
    do {
      const page = await this.request<List<Message>>('GET', '/messages', {
        query: { limit: 100, ...filters, starting_after: startingAfter },
      });
      yield* page.data;
      startingAfter = page.has_more ? (page.next_cursor ?? undefined) : undefined;
    } while (startingAfter);
  }
}
Usage
import { OmniMessage, OmniMessageError } from './omnimessage';

const omni = new OmniMessage(process.env.OMNIMESSAGE_API_KEY!);

try {
  const message = await omni.sendMessage(
    {
      channel: 'ch_test_whatsapp',
      to: '+15550100002',
      type: 'text',
      text: { body: 'Hello from test mode' },
      reference: 'order-1042',
    },
    'order-1042-shipped',
  );
  console.log(message.id, message.status);
} catch (error) {
  if (error instanceof OmniMessageError && error.body.code === 'insufficient_balance') {
    // Pause sending and alert whoever tops up the wallet.
  }
  throw error;
}

for await (const message of omni.listMessages({ reference: 'order-1042' })) {
  console.log(message.id, message.status);
}

Python#

A client built on requests. Install it with pip install requests.

omnimessage.py
"""omnimessage.py: a minimal client built on requests. Requires Python 3.9 or later."""
from __future__ import annotations

import time
import uuid
from typing import Any, Iterator

import requests

BASE_URL = "https://api.omnimessage.co/v1"


class OmniMessageError(Exception):
    def __init__(self, status: int, body: dict[str, Any], retry_after: float | None = None):
        super().__init__(f"{body.get('code')}: {body.get('message')} ({body.get('request_id')})")
        self.status = status
        self.type = body.get("type")
        self.code = body.get("code")
        self.param = body.get("param")
        self.request_id = body.get("request_id")
        self.retry_after = retry_after


class OmniMessage:
    def __init__(self, api_key: str, max_retries: int = 2, timeout: float = 15.0):
        self.max_retries = max_retries
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {api_key}"

    def request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json: Any = None,
        idempotency_key: str | None = None,
    ) -> dict[str, Any]:
        headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {}
        attempt = 0
        while True:
            response = self.session.request(
                method, BASE_URL + path, params=params, json=json, headers=headers, timeout=self.timeout
            )
            if response.ok:
                return response.json()

            retry_after = float(response.headers.get("Retry-After") or 0) or None
            # Retry only what is safe: rate limits always, server errors when the request is idempotent.
            idempotent = method == "GET" or idempotency_key is not None
            retryable = response.status_code == 429 or (response.status_code >= 500 and idempotent)
            if retryable and attempt < self.max_retries:
                time.sleep(retry_after or 2**attempt)
                attempt += 1
                continue

            try:
                body = response.json()["error"]
            except (ValueError, KeyError):
                body = {
                    "type": "api_error",
                    "code": "internal_error",
                    "message": f"Unexpected response with status {response.status_code}",
                    "request_id": response.headers.get("X-Request-Id"),
                }
            raise OmniMessageError(response.status_code, body, retry_after)

    def send_message(self, *, idempotency_key: str | None = None, **params: Any) -> dict[str, Any]:
        return self.request("POST", "/messages", json=params, idempotency_key=idempotency_key or str(uuid.uuid4()))

    def get_message(self, message_id: str) -> dict[str, Any]:
        return self.request("GET", f"/messages/{message_id}")

    def list_messages(self, **filters: Any) -> Iterator[dict[str, Any]]:
        params = {"limit": 100, **filters}
        while True:
            page = self.request("GET", "/messages", params=params)
            yield from page["data"]
            if not page["has_more"]:
                return
            params["starting_after"] = page["next_cursor"]
Usage
import os

from omnimessage import OmniMessage, OmniMessageError

omni = OmniMessage(os.environ["OMNIMESSAGE_API_KEY"])

try:
    message = omni.send_message(
        channel="ch_test_whatsapp",
        to="+15550100002",
        type="text",
        text={"body": "Hello from test mode"},
        reference="order-1042",
        idempotency_key="order-1042-shipped",
    )
    print(message["id"], message["status"])
except OmniMessageError as error:
    if error.code == "insufficient_balance":
        pass  # Pause sending and alert whoever tops up the wallet.
    raise

for message in omni.list_messages(reference="order-1042"):
    print(message["id"], message["status"])

Generate a client#

The API is described by an OpenAPI 3.1 document. Use it with the code generator of your choice to produce a typed client for any language, or import it into an API tool such as Postman or Insomnia. A ready-made Postman collection is generated from the same document.

Download the OpenAPI document
curl -o omnimessage-openapi.json https://omnimessage.co/docs/openapi.json
  • The document lists the required API key scope of every operation under x-scopes.
  • Webhook payloads are described in its webhooks section.
  • The same document is served by the API at /openapi.json.

Webhook verification#

Verifying webhook signatures needs only an HMAC-SHA256 function. The webhooks guide has complete handlers for Node.js, Python, PHP and Go.

    Loading