Complete your organization profile
Provide your business details, intended markets and messaging use cases. Service and sender approval are required before live traffic.
Integrate messaging, verification, voice and Omini AI through one secure organization API.
Authorization: Bearer ••••Provide your business details, intended markets and messaging use cases. Service and sender approval are required before live traffic.
Choose your organization, credential mode, expiry and allowed IPs. Copy the secret once and store it on your server.
Call GET /api/v1/context with organization:read. Check the organization, mode and scopes before sending.
Use an active service with an approved sender and available funding or approved credit. HTTP 202 means accepted; delivery is asynchronous.
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.
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 keys allow permitted reads and reject live sending or configuration changes. They do not simulate delivery. Production use requires approved services, identities and funding.
https://omini.3pap.africa/api/v1Use 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
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// Server-side JavaScript (Node.js 18+), not a browser bundle.
const base = process.env.OMINI_BASE_URL.replace(/\/$/, '');
const payload = {
sender_id: process.env.OMINI_SENDER_ID,
to: '+233241234567',
body: 'Your delivery is on its way.',
idempotency_key: process.env.OMINI_MESSAGE_UUID // persist before first attempt
};
const response = await fetch(`${base}/sms/messages`, {
method: 'POST',
signal: AbortSignal.timeout(30000),
headers: {
Authorization: `Bearer ${process.env.OMINI_API_KEY}`,
Accept: 'application/json', 'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const supportRequestId = response.headers.get('X-Request-Id');
if (response.status !== 202) {
throw new Error(`Omini HTTP ${response.status}; request ${supportRequestId}`);
}
const accepted = await response.json();
// Persist accepted.data.id (message ID) and accepted.data.request_id (job ID).
// GET /sms/messages/{message ID} to check delivery; 202 is not delivery.# Python 3.10+, standard library only. Run on your server.
import json
import os
import urllib.error
import urllib.request
payload = {
"sender_id": os.environ["OMINI_SENDER_ID"],
"to": "+233241234567",
"body": "Your delivery is on its way.",
"idempotency_key": os.environ["OMINI_MESSAGE_UUID"] # persisted UUID
}
request = urllib.request.Request(
os.environ["OMINI_BASE_URL"].rstrip("/") + "/sms/messages",
data=json.dumps(payload).encode("utf-8"),
headers={"Authorization": "Bearer " + os.environ["OMINI_API_KEY"],
"Accept": "application/json", "Content-Type": "application/json"},
method="POST"
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
if response.status != 202:
raise RuntimeError("Unexpected Omini response")
accepted = json.load(response)
message_id = accepted["data"]["id"] # persist; poll delivery separately
except urllib.error.HTTPError as error:
raise RuntimeError(
f"Omini HTTP {error.code}; request {error.headers.get('X-Request-Id')}"
) from None
# On timeout or 5xx, check the job or retry identical content with the SAME UUID.Examples run on your server. JavaScript uses Node.js 18+; Python uses the standard library. Omini does not send requests from this page.
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.
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.
/contextOrganization contextorganization:read/sms/messagesSend an SMSservices:usesms:send{
"sender_id": "YOUR_APPROVED_SENDER_UUID",
"to": "+233241234567",
"body": "Your delivery is on its way.",
"idempotency_key": "PERSISTED_UUID_FOR_THIS_MESSAGE"
} /sms/messages/{message}SMS delivery statusmessaging:read/otp/challengesCreate a verification challengeservices:useotp:verifysms:send{
"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.
/otp/challenges/{challenge}/verifyCheck a verification codeotp:verify{
"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.
/channels/email/messagesSend a transactional emailservices:useemail:send{
"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"
} /channels/email/messages/{message}Email delivery statusmessaging:read/whatsapp/messagesSend a WhatsApp templateservices:usewhatsapp:send{
"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"
} /whatsapp/templatesApproved WhatsApp templateswhatsapp:send/channels/whatsapp/messages/{message}WhatsApp delivery statusmessaging:read/requestsOmini AI and service requestsservices:useai:use{
"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.
/requests/{serviceRequest}Service request resultservices:read/usageUsage and reserved creditbilling:readOptional 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.
/billing/transactionsBilling transactionsbilling:read/webhooksRegister a webhookwebhooks:manage{
"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.
/voice/numbersVoice numbersvoice:read/voice/calls/{call}Voice call detailsvoice:read/devicesConnected devicesdevices:readNo matching endpoints. Try another service, path or scope.
Download OpenAPIOmini 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>// 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.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_changedAPI 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 status | What to do |
|---|---|
401 / 403 | Check the key, expiry, allowed IPs, organization membership, scopes and service approval. Do not retry with another customer’s key. |
404 | Check the resource ID and organization. Resources from another organization are not accessible. |
409 | Resolve the resource state or idempotency conflict. Never reuse an existing UUID with different content. |
415 / 422 | Send valid JSON, correct the reported fields, and check service limits, approved identities and available funding. |
429 | Pause and use exponential backoff with jitter. Respect Retry-After when present. Limits depend on the API key; some routes also have shared limits. |
5xx / timeout | The 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.
Set up your organization, choose your services and connect with credentials you control.