African roots. Global ambition.Meet Omini, a product of 3PAP
OMINI FOR DEVELOPERS

One API. More possibilities.

Integrate messaging, verification, voice and Omini AI through one secure organization API.

REST / JSONScoped credentialsSigned webhooks
omini / api / v1
YOUR APPLICATION
Your serverAuthorization: Bearer ••••
Omini APIOrganization-scoped access
SMSOTPEmailWhatsAppVoiceAI
Build securely. Track every request.
FROM ACCOUNT TO FIRST REQUEST

A clear path to integration.

2

Create a scoped key

Choose your organization, credential mode, expiry and allowed IPs. Copy the secret once and store it on your server.

3

Confirm your context

Call GET /api/v1/context with organization:read. Check the organization, mode and scopes before sending.

4

Send, then track delivery

Use an active service with an approved sender and available funding or approved credit. HTTP 202 means accepted; delivery is asynchronous.

Authentication

Every API key belongs to one organization. The credential determines tenant access; a browser session or an organization ID in the payload cannot select another customer.

Keep keys on your server

Use a secret manager or environment variables. Never place API keys in browser code, mobile apps, source control or logs. Rotate or revoke a key from your workspace when needed.

Sandbox and production

Sandbox keys allow permitted reads and reject live sending or configuration changes. They do not simulate delivery. Production use requires approved services, identities and funding.

API base URLhttps://omini.3pap.africa/api/v1

Use the HTTPS base URL of your deployed Omini workspace. Local HTTP is for development only. Send Accept: application/json and Content-Type: application/json for JSON writes.

Authorization: Bearer $OMINI_API_KEY
Accept: application/json
Content-Type: application/json
YOUR FIRST SMS

Real requests. Familiar tools.

Replace every placeholder with your organization’s approved identifiers. Generate and persist a UUID for each business action before the first request; use the same UUID and content when retrying.

# Server environment: OMINI_BASE_URL=https://YOUR_OMINI_HOST/api/v1
# Keep OMINI_API_KEY in your server's secret manager.
# Save one UUID with your business event before its first attempt.
cat > message.json <<'JSON'
{
  "sender_id": "YOUR_APPROVED_SENDER_UUID",
  "to": "+233241234567",
  "body": "Your delivery is on its way.",
  "idempotency_key": "PERSISTED_UUID_FOR_THIS_MESSAGE"
}
JSON
# Replace placeholders in message.json; keep identical content on retry.
curl --fail-with-body --include --max-time 30 \
  "$OMINI_BASE_URL/sms/messages" \
  -H "Authorization: Bearer $OMINI_API_KEY" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  --data-binary @message.json

Examples run on your server. JavaScript uses Node.js 18+; Python uses the standard library. Omini does not send requests from this page.

Accepted is not delivered

Store data.id as the message ID and data.request_id as the service job ID. Poll the message endpoint or consume a signed delivery webhook. Keep X-Request-Id for support; it identifies the HTTP request, not the message.

REST / JSON

Endpoint reference

18 endpoints

Paths below are relative to the API base URL. Required scopes are cumulative. Use your own resource UUIDs; template variables must match the approved template exactly.

GET/contextOrganization context

Required scopes

organization:read
POST/sms/messagesSend an SMS

Required scopes

services:usesms:send

Request body example

{
    "sender_id": "YOUR_APPROVED_SENDER_UUID",
    "to": "+233241234567",
    "body": "Your delivery is on its way.",
    "idempotency_key": "PERSISTED_UUID_FOR_THIS_MESSAGE"
}
GET/sms/messages/{message}SMS delivery status

Required scopes

messaging:read
POST/otp/challengesCreate a verification challenge

Required scopes

services:useotp:verifysms:send

Request body example

{
    "channel": "sms",
    "sender_id": "YOUR_APPROVED_SENDER_UUID",
    "to": "+233241234567",
    "purpose": "customer-sign-in",
    "idempotency_key": "PERSISTED_UUID_FOR_THIS_CHALLENGE"
}

For email OTP, use channel email, an approved email identity as sender_id, and the email:send scope instead of sms:send. The response includes id, status, channel and expires_at. Never log verification codes.

POST/otp/challenges/{challenge}/verifyCheck a verification code

Required scopes

otp:verify

Request body example

{
    "code": "USER_ENTERED_SIX_DIGIT_CODE"
}

Submit the recipient’s six-digit code. Check the verified boolean in the response. Expired, consumed or invalid codes return verified false.

POST/channels/email/messagesSend a transactional email

Required scopes

services:useemail:send

Request body example

{
    "identity_id": "YOUR_APPROVED_EMAIL_IDENTITY_UUID",
    "template_id": "YOUR_APPROVED_TEMPLATE_UUID",
    "to": "recipient@example.com",
    "variables": {
        "name": "Sam"
    },
    "idempotency_key": "PERSISTED_UUID_FOR_THIS_MESSAGE"
}
GET/channels/email/messages/{message}Email delivery status

Required scopes

messaging:read
POST/whatsapp/messagesSend a WhatsApp template

Required scopes

services:usewhatsapp:send

Request body example

{
    "identity_id": "YOUR_APPROVED_WHATSAPP_IDENTITY_UUID",
    "template_id": "YOUR_APPROVED_TEMPLATE_UUID",
    "to": "+233241234567",
    "variables": {
        "1": "Sam"
    },
    "consent_reference": "YOUR_RECORDED_RECIPIENT_CONSENT",
    "idempotency_key": "PERSISTED_UUID_FOR_THIS_MESSAGE"
}
GET/whatsapp/templatesApproved WhatsApp templates

Required scopes

whatsapp:send
GET/channels/whatsapp/messages/{message}WhatsApp delivery status

Required scopes

messaging:read
POST/requestsOmini AI and service requests

Required scopes

services:useai:use

Request body example

{
    "subscription_id": "YOUR_ACTIVE_AI_SUBSCRIPTION_UUID",
    "idempotency_key": "PERSISTED_UUID_FOR_THIS_REQUEST",
    "parameters": {
        "input": "Write a short delivery update.",
        "max_output_tokens": 128
    }
}

Your active Omini AI subscription selects its model automatically. You can optionally pass the Omini model identifier shown in your workspace. Other operations require their own service scope and approved parameters. Poll GET /requests/{serviceRequest} for the result.

GET/requests/{serviceRequest}Service request result

Required scopes

services:read
GET/usageUsage and reserved credit

Required scopes

billing:read

Optional from and to dates use YYYY-MM-DD in UTC, inclusively, with a maximum range of 366 days. Amounts are integer micro-units returned as strings: 1,000,000 micros equals one currency unit. Keep different currencies separate.

GET/billing/transactionsBilling transactions

Required scopes

billing:read
POST/webhooksRegister a webhook

Required scopes

webhooks:manage

Request body example

{
    "name": "Delivery updates",
    "url": "https://hooks.example.com/omini",
    "events": [
        "messaging.delivery",
        "channels.delivery",
        "otp.verified",
        "service.state_changed"
    ]
}

Use a public HTTPS destination. Save signing_secret securely from the creation response; list responses omit it. An organization can register up to ten destinations.

GET/voice/numbersVoice numbers

Required scopes

voice:read
GET/voice/calls/{call}Voice call details

Required scopes

voice:read
GET/devicesConnected devices

Required scopes

devices:read

Download OpenAPI
TRUST EVERY EVENT

Webhook verification

Omini signs the timestamp, a dot and the exact raw request body using HMAC-SHA256 and your endpoint signing secret. Verify Omini-Signature before parsing JSON or acting on an event.

Omini-Signature: t=<unix_seconds>,v1=<hex_hmac>Omini-Event-Id: <event_uuid>Omini-Delivery-Id: <delivery_uuid>
JavaScript · Node.js
// Node.js: rawBody must be the unmodified request Buffer, before JSON parsing.
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyOminiWebhook(rawBody, signature, secret, now = Date.now()) {
  if (!Buffer.isBuffer(rawBody) || typeof signature !== 'string') return false;
  const match = /^t=([0-9]{1,12}),v1=([a-f0-9]{64})$/.exec(signature);
  if (!match || Math.abs(Math.floor(now / 1000) - Number(match[1])) > 300) {
    return false;
  }
  const expected = createHmac('sha256', secret)
    .update(match[1] + '.').update(rawBody).digest();
  return timingSafeEqual(expected, Buffer.from(match[2], 'hex'));
}

// Read header Omini-Signature; secret = process.env.OMINI_WEBHOOK_SECRET.
// After verification, parse rawBody and check organization_id and event type.
// Atomically store body.id with the durable work item; a unique constraint
// makes duplicates return 2xx without repeating a business action.
// Return 2xx only after durable acceptance; return non-2xx for temporary failure.
// Deduplicate on body.id, not the delivery attempt or timestamp.

Verify, persist, acknowledge

The example accepts timestamps within five minutes; keep your server clock synchronized. Validate the signed organization_id and event type. Store the event ID atomically with durable processing, then return 2xx. Duplicates must not repeat a business action.

Non-2xx responses and temporary failures are retried with exponential backoff, up to twelve automatic attempts. A replay may have a new timestamp. Deduplicate by the signed body.id, and allow valid previously recorded events to return 2xx.

messaging.deliverychannels.deliveryotp.verifiedservice.state_changed

Errors and retries

API errors normally include error and message; validation failures also include errors by field. X-Request-Id is returned by API authentication. Do not rely on every response body having the same wrapper.

HTTP statusWhat to do
401 / 403Check the key, expiry, allowed IPs, organization membership, scopes and service approval. Do not retry with another customer’s key.
404Check the resource ID and organization. Resources from another organization are not accessible.
409Resolve the resource state or idempotency conflict. Never reuse an existing UUID with different content.
415 / 422Send valid JSON, correct the reported fields, and check service limits, approved identities and available funding.
429Pause and use exponential backoff with jitter. Respect Retry-After when present. Limits depend on the API key; some routes also have shared limits.
5xx / timeoutThe outcome may be unknown. Query the existing resource or retry the identical action with its original UUID. Keep retries bounded.

Paginated endpoints expose data, page and total; some include pages. Keep polling bounded, and never treat a transport timeout as proof that nothing was created.

BUILD WITH OMINI

Your next connection starts here.

Set up your organization, choose your services and connect with credentials you control.

Get started