Omniflex Developer API
Programmable SMS and OTP campaigns for Zimbabwe. Send personalised bulk SMS, run OTP verification flows, and track delivery in real time — from any language or platform.
Quick Start — Send your first SMS
Select your language above, then copy the snippet below.
const res = await fetch('https://omniflex.co.zw/api/sms/send', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
phone: '263771234567',
message: 'Hello from Omniflex!',
senderId: 'MYBRAND',
}),
});
const data = await res.json();
console.log(data); // { ok: true, econet: {...}, netone: {...} }
const axios = require('axios');
const { data } = await axios.post(
'https://omniflex.co.zw/api/sms/send',
{ phone: '263771234567', message: 'Hello from Omniflex!', senderId: 'MYBRAND' },
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } }
);
console.log(data); // { ok: true, econet: {...} }
import requests
resp = requests.post(
'https://omniflex.co.zw/api/sms/send',
headers={'Authorization': 'Bearer YOUR_API_KEY'},
json={
'phone': '263771234567',
'message': 'Hello from Omniflex!',
'senderId': 'MYBRAND',
},
)
print(resp.json()) # {'ok': True, 'econet': {...}}
use Illuminate\Support\Facades\Http;
$response = Http::withToken('YOUR_API_KEY')
->post('https://omniflex.co.zw/api/sms/send', [
'phone' => '263771234567',
'message' => 'Hello from Omniflex!',
'senderId' => 'MYBRAND',
]);
$data = $response->json(); // ['ok' => true, 'econet' => [...]]
Authentication
Pass your API key or JWT token in every request using the Authorization header.
Authorization: Bearer omf_live_<your_api_key>
Your account also has a stable Account ID, visible in Settings → Developer Keys. It isn't a credential — requests are still authenticated with the API key or JWT above — it's just a reference identifier for support requests or when discussing your account with the OmniFlex team.
Obtain a JWT (password login)
const res = await fetch('https://omniflex.co.zw/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier: 'you@example.com', password: 'secret' }),
});
const { token } = await res.json();
const axios = require('axios');
const { data } = await axios.post('https://omniflex.co.zw/api/auth/login', {
identifier: 'you@example.com',
password: 'secret',
});
const { token } = data;
import requests
resp = requests.post('https://omniflex.co.zw/api/auth/login',
json={'identifier': 'you@example.com', 'password': 'secret'})
token = resp.json()['token']
use Illuminate\Support\Facades\Http;
$token = Http::post('https://omniflex.co.zw/api/auth/login', [
'identifier' => 'you@example.com',
'password' => 'secret',
])->json('token');
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"user": { "id": "usr_...", "name": "Tino Moyo", "role": "admin" },
"org": { "id": "org_...", "name": "My Company" }
}
Keep API keys secret. Never embed them in client-side code. Revoke compromised keys immediately from Settings → Developer Keys.
Errors & Status Codes
Errors return JSON with an error field. Successful responses include "ok": true.
{
"error": "Insufficient credits",
"detail": "You need 150 credits but only have 42."
}
Rate Limits
| Limit | Window | Scope |
|---|---|---|
| 300 requests | 1 minute | All endpoints (per IP) |
| 5 OTP sends | 10 minutes | Per identifier (phone/email) |
| 5 wrong verifications | Per code | Code locked after 5 failures |
SMS Campaigns
Campaigns organise bulk sends with a shared template, sender ID, and schedule. Recipients are resolved at dispatch time.
Request body — Create campaign
| Field | Type | Description |
|---|---|---|
| name* | string | Campaign label |
| typeopt | string | "SMS" (default) or "OTP" |
| message_templateopt | string | Message body. Use {{name}} for personalisation |
| sender_idopt | string | Approved sender ID e.g. "MYBRAND" |
| statusopt | string | draft · active · scheduled · completed |
| scheduled_dateopt | ISO string | Auto-send at this UTC time (requires status: "scheduled") |
| recipientsopt | array | [{phone, name?, message?}] |
Create an SMS Campaign
const res = await fetch('https://omniflex.co.zw/api/campaigns', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'June Promo',
type: 'SMS',
message_template: 'Hi {{name}}, enjoy 20% off this June!',
sender_id: 'MYBRAND',
status: 'scheduled',
scheduled_date: '2026-06-25T08:00:00Z',
recipients: [
{ phone: '263771234567', name: 'Tino' },
{ phone: '263712345678', name: 'Rudo' },
],
}),
});
const campaign = await res.json();
const axios = require('axios');
const client = axios.create({
baseURL: 'https://omniflex.co.zw/api',
headers: { Authorization: 'Bearer YOUR_API_KEY' },
});
const { data: campaign } = await client.post('/campaigns', {
name: 'June Promo',
type: 'SMS',
message_template: 'Hi {{name}}, enjoy 20% off this June!',
sender_id: 'MYBRAND',
status: 'scheduled',
scheduled_date: '2026-06-25T08:00:00Z',
recipients: [
{ phone: '263771234567', name: 'Tino' },
{ phone: '263712345678', name: 'Rudo' },
],
});
import requests
resp = requests.post(
'https://omniflex.co.zw/api/campaigns',
headers={'Authorization': 'Bearer YOUR_API_KEY'},
json={
'name': 'June Promo',
'type': 'SMS',
'message_template': 'Hi {{name}}, enjoy 20% off this June!',
'sender_id': 'MYBRAND',
'status': 'scheduled',
'scheduled_date': '2026-06-25T08:00:00Z',
'recipients': [
{'phone': '263771234567', 'name': 'Tino'},
{'phone': '263712345678', 'name': 'Rudo'},
],
},
)
campaign = resp.json()
use Illuminate\Support\Facades\Http;
$campaign = Http::withToken('YOUR_API_KEY')
->post('https://omniflex.co.zw/api/campaigns', [
'name' => 'June Promo',
'type' => 'SMS',
'message_template' => 'Hi {{name}}, enjoy 20% off this June!',
'sender_id' => 'MYBRAND',
'status' => 'scheduled',
'scheduled_date' => '2026-06-25T08:00:00Z',
'recipients' => [
['phone' => '263771234567', 'name' => 'Tino'],
['phone' => '263712345678', 'name' => 'Rudo'],
],
])->json();
{
"id": "cmp_abc123",
"name": "June Promo",
"type": "SMS",
"status": "scheduled",
"total_recipients": 2,
"sent_count": 0,
"cost_estimate": 2,
"scheduled_date": "2026-06-25T08:00:00.000Z",
"created_at": "2026-06-24T10:30:00.000Z"
}
Use {{name}} in message_template — it is replaced with each recipient's name at send time. Set recipients[].message to override per contact.
OTP Campaigns
OTP campaigns track delivery and verified counts for one-time code flows. Create one with "type": "OTP", then call /send-otp per recipient.
| Field | Type | Description |
|---|---|---|
| identifier* | string | Phone (263771234567) or email address |
| nameopt | string | Recipient name for template personalisation |
Send OTP via Campaign
const campaignId = 'cmp_abc123';
const res = await fetch(
`https://omniflex.co.zw/api/campaigns/${campaignId}/send-otp`,
{
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ identifier: '263771234567', name: 'Tino' }),
}
);
// { ok: true }
const axios = require('axios');
const campaignId = 'cmp_abc123';
await axios.post(
`https://omniflex.co.zw/api/campaigns/${campaignId}/send-otp`,
{ identifier: '263771234567', name: 'Tino' },
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } }
);
import requests
campaign_id = 'cmp_abc123'
resp = requests.post(
f'https://omniflex.co.zw/api/campaigns/{campaign_id}/send-otp',
headers={'Authorization': 'Bearer YOUR_API_KEY'},
json={'identifier': '263771234567', 'name': 'Tino'},
)
# resp.json() == {'ok': True}
use Illuminate\Support\Facades\Http;
$campaignId = 'cmp_abc123';
Http::withToken('YOUR_API_KEY')
->post("https://omniflex.co.zw/api/campaigns/{$campaignId}/send-otp", [
'identifier' => '263771234567',
'name' => 'Tino',
]);
Verify the OTP
const res = await fetch('https://omniflex.co.zw/api/otp/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier: '263771234567', code: '483921' }),
});
const { ok } = await res.json(); // true = verified
const axios = require('axios');
const { data } = await axios.post('https://omniflex.co.zw/api/otp/verify', {
identifier: '263771234567',
code: '483921',
});
// data.ok === true
import requests
resp = requests.post(
'https://omniflex.co.zw/api/otp/verify',
json={'identifier': '263771234567', 'code': '483921'},
)
assert resp.json()['ok'] is True
use Illuminate\Support\Facades\Http;
$ok = Http::post('https://omniflex.co.zw/api/otp/verify', [
'identifier' => '263771234567',
'code' => '483921',
])->json('ok'); // true
OTP Rules
| Rule | Value |
|---|---|
| Code length | 6 digits |
| Code TTL | 5 minutes |
| Send rate limit | 5 per identifier per 10 minutes |
| Max failed verifications | 5 (code invalidated after) |
| Channels | SMS (Econet / NetOne) or email |
Send SMS
Dispatch to one recipient, an array of numbers, or a full recipient objects array with per-contact personalisation.
| Field | Type | Description |
|---|---|---|
| phone | string | Single number e.g. 263771234567 |
| phones | string[] | Array of numbers (max 100) |
| recipients | object[] | [{phone, name?, message?}] — enables personalisation |
| message* | string | Message body. Use {{name}} as placeholder |
| senderIdopt | string | Approved sender ID (Econet) |
| campaign_idopt | string | Link to a campaign for DLR reporting |
| scheduleopt | boolean | Set true to use sendingTime |
| sendingTimeopt | ISO string | UTC datetime for scheduled delivery |
Single recipient
const res = await fetch('https://omniflex.co.zw/api/sms/send', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
phone: '263771234567',
message: 'Your order #4821 has been shipped!',
}),
});
const data = await res.json();
const axios = require('axios');
const { data } = await axios.post(
'https://omniflex.co.zw/api/sms/send',
{ phone: '263771234567', message: 'Your order #4821 has been shipped!' },
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } }
);
import requests
resp = requests.post(
'https://omniflex.co.zw/api/sms/send',
headers={'Authorization': 'Bearer YOUR_API_KEY'},
json={'phone': '263771234567', 'message': 'Your order #4821 has been shipped!'},
)
use Illuminate\Support\Facades\Http;
$data = Http::withToken('YOUR_API_KEY')
->post('https://omniflex.co.zw/api/sms/send', [
'phone' => '263771234567',
'message' => 'Your order #4821 has been shipped!',
])->json();
Bulk — multiple numbers
const res = await fetch('https://omniflex.co.zw/api/sms/send', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
phones: ['263771234567', '263712345678', '263782345678'],
message: 'Flash sale: 30% off all products today!',
campaign_id: 'cmp_abc123',
}),
});
const axios = require('axios');
await axios.post(
'https://omniflex.co.zw/api/sms/send',
{
phones: ['263771234567', '263712345678', '263782345678'],
message: 'Flash sale: 30% off all products today!',
campaign_id: 'cmp_abc123',
},
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } }
);
import requests
requests.post(
'https://omniflex.co.zw/api/sms/send',
headers={'Authorization': 'Bearer YOUR_API_KEY'},
json={
'phones': ['263771234567', '263712345678', '263782345678'],
'message': 'Flash sale: 30% off all products today!',
'campaign_id': 'cmp_abc123',
},
)
use Illuminate\Support\Facades\Http;
Http::withToken('YOUR_API_KEY')
->post('https://omniflex.co.zw/api/sms/send', [
'phones' => ['263771234567', '263712345678', '263782345678'],
'message' => 'Flash sale: 30% off all products today!',
'campaign_id' => 'cmp_abc123',
]);
Personalised — per-recipient messages
await fetch('https://omniflex.co.zw/api/sms/send', {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({
message: 'Hi {{name}}, your loyalty reward is waiting!',
recipients: [
{ phone: '263771234567', name: 'Tino' },
{ phone: '263712345678', name: 'Rudo' },
{ phone: '263782345678', name: 'Farai', message: 'Farai — your VIP gift is ready!' },
],
}),
});
await axios.post(
'https://omniflex.co.zw/api/sms/send',
{
message: 'Hi {{name}}, your loyalty reward is waiting!',
recipients: [
{ phone: '263771234567', name: 'Tino' },
{ phone: '263712345678', name: 'Rudo' },
{ phone: '263782345678', name: 'Farai', message: 'Farai — your VIP gift is ready!' },
],
},
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } }
);
requests.post(
'https://omniflex.co.zw/api/sms/send',
headers={'Authorization': 'Bearer YOUR_API_KEY'},
json={
'message': 'Hi {{name}}, your loyalty reward is waiting!',
'recipients': [
{'phone': '263771234567', 'name': 'Tino'},
{'phone': '263712345678', 'name': 'Rudo'},
{'phone': '263782345678', 'name': 'Farai', 'message': 'Farai — your VIP gift is ready!'},
],
},
)
Http::withToken('YOUR_API_KEY')
->post('https://omniflex.co.zw/api/sms/send', [
'message' => 'Hi {{name}}, your loyalty reward is waiting!',
'recipients' => [
['phone' => '263771234567', 'name' => 'Tino'],
['phone' => '263712345678', 'name' => 'Rudo'],
['phone' => '263782345678', 'name' => 'Farai',
'message' => 'Farai — your VIP gift is ready!'],
],
]);
{ "ok": true, "econet": { "sent": 2, "failed": 0 }, "netone": { "sent": 1, "failed": 0 }, "netone_skipped": 0 }
Send OTP (Standalone)
Use these endpoints for OTP flows outside a campaign — phone verification, login, transaction confirmation.
// SMS OTP
await fetch('https://omniflex.co.zw/api/otp/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier: '263771234567', method: 'sms' }),
});
// Email OTP
await fetch('https://omniflex.co.zw/api/otp/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier: 'tino@example.com', method: 'email' }),
});
const axios = require('axios');
// SMS OTP
await axios.post('https://omniflex.co.zw/api/otp/send', {
identifier: '263771234567', method: 'sms',
});
// Email OTP
await axios.post('https://omniflex.co.zw/api/otp/send', {
identifier: 'tino@example.com', method: 'email',
});
import requests
# SMS OTP
requests.post('https://omniflex.co.zw/api/otp/send',
json={'identifier': '263771234567', 'method': 'sms'})
# Email OTP
requests.post('https://omniflex.co.zw/api/otp/send',
json={'identifier': 'tino@example.com', 'method': 'email'})
use Illuminate\Support\Facades\Http;
// SMS OTP
Http::post('https://omniflex.co.zw/api/otp/send', [
'identifier' => '263771234567', 'method' => 'sms',
]);
// Email OTP
Http::post('https://omniflex.co.zw/api/otp/send', [
'identifier' => 'tino@example.com', 'method' => 'email',
]);
Contacts
Store recipients in your address book and assign them to groups for targeted campaigns.
const res = await fetch('https://omniflex.co.zw/api/contacts', {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Tino Moyo', phone: '263771234567', group_ids: ['grp_vip'] }),
});
const contact = await res.json();
const { data: contact } = await axios.post(
'https://omniflex.co.zw/api/contacts',
{ name: 'Tino Moyo', phone: '263771234567', group_ids: ['grp_vip'] },
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } }
);
contact = requests.post(
'https://omniflex.co.zw/api/contacts',
headers={'Authorization': 'Bearer YOUR_API_KEY'},
json={'name': 'Tino Moyo', 'phone': '263771234567', 'group_ids': ['grp_vip']},
).json()
$contact = Http::withToken('YOUR_API_KEY')
->post('https://omniflex.co.zw/api/contacts', [
'name' => 'Tino Moyo',
'phone' => '263771234567',
'group_ids' => ['grp_vip'],
])->json();
Contact Groups
Organise contacts into named groups (e.g. VIP Customers, June Leads) for targeted campaign sends.
{ "name": "VIP" }Sender IDs
Alphanumeric sender names (up to 11 characters) shown instead of a number on Econet. Require network approval before use.
pending)| Status | Meaning |
|---|---|
| pending | Submitted, awaiting network approval |
| approved | Ready to use in campaigns |
| rejected | Rejected by the network |
Campaign Files
Upload a parsed recipient list (from CSV/spreadsheet) as JSON. Reference the file ID when building campaigns.
const res = await fetch('https://omniflex.co.zw/api/campaign-files', {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: 'june_leads.csv',
columns: ['phone', 'name', 'city'],
recipients: [
{ phone: '263771234567', name: 'Tino', city: 'Harare' },
{ phone: '263712345678', name: 'Rudo', city: 'Bulawayo' },
],
}),
});
// { id: 'file_abc', columns: [...], count: 2 }
const { data } = await axios.post(
'https://omniflex.co.zw/api/campaign-files',
{
filename: 'june_leads.csv',
columns: ['phone', 'name', 'city'],
recipients: [
{ phone: '263771234567', name: 'Tino', city: 'Harare' },
{ phone: '263712345678', name: 'Rudo', city: 'Bulawayo' },
],
},
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } }
);
resp = requests.post(
'https://omniflex.co.zw/api/campaign-files',
headers={'Authorization': 'Bearer YOUR_API_KEY'},
json={
'filename': 'june_leads.csv',
'columns': ['phone', 'name', 'city'],
'recipients': [
{'phone': '263771234567', 'name': 'Tino', 'city': 'Harare'},
{'phone': '263712345678', 'name': 'Rudo', 'city': 'Bulawayo'},
],
},
)
# {'id': 'file_abc', 'columns': [...], 'count': 2}
$file = Http::withToken('YOUR_API_KEY')
->post('https://omniflex.co.zw/api/campaign-files', [
'filename' => 'june_leads.csv',
'columns' => ['phone', 'name', 'city'],
'recipients' => [
['phone' => '263771234567', 'name' => 'Tino', 'city' => 'Harare'],
['phone' => '263712345678', 'name' => 'Rudo', 'city' => 'Bulawayo'],
],
])->json();
SMS Logs & DLR
Query per-message delivery logs filtered by campaign, status, or date.
| Query param | Description |
|---|---|
| campaign_id | Filter by campaign |
| dlr_status | delivered · failed · pending |
| limit | Max records (default 1000) |
| export | true — remove limit for full export |
const res = await fetch(
'https://omniflex.co.zw/api/sms-logs?campaign_id=cmp_abc123&dlr_status=delivered',
{ headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
const logs = await res.json();
const { data: logs } = await axios.get(
'https://omniflex.co.zw/api/sms-logs',
{ params: { campaign_id: 'cmp_abc123', dlr_status: 'delivered' },
headers: { Authorization: 'Bearer YOUR_API_KEY' } }
);
logs = requests.get(
'https://omniflex.co.zw/api/sms-logs',
headers={'Authorization': 'Bearer YOUR_API_KEY'},
params={'campaign_id': 'cmp_abc123', 'dlr_status': 'delivered'},
).json()
$logs = Http::withToken('YOUR_API_KEY')
->get('https://omniflex.co.zw/api/sms-logs', [
'campaign_id' => 'cmp_abc123',
'dlr_status' => 'delivered',
])->json();
Balance & Credits
1 credit = 1 SMS part (160 characters). Multi-part messages consume 1 credit per 153-character segment.
const res = await fetch('https://omniflex.co.zw/api/account-balance',
{ headers: { 'Authorization': 'Bearer YOUR_API_KEY' } });
const [balance] = await res.json();
console.log(balance.balance); // 842
const { data } = await axios.get('https://omniflex.co.zw/api/account-balance',
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } });
console.log(data[0].balance); // 842
data = requests.get('https://omniflex.co.zw/api/account-balance',
headers={'Authorization': 'Bearer YOUR_API_KEY'}).json()
balance = data[0]['balance'] # 842
$data = Http::withToken('YOUR_API_KEY')
->get('https://omniflex.co.zw/api/account-balance')->json();
$balance = $data[0]['balance']; // 842
| Package | Credits | USD | ZWG |
|---|---|---|---|
| SMS500 | 500 | $15.00 | 406.25 |
| SMS1000 | 1,000 | $25.00 | 812.50 |
| SMS2500 | 2,500 | $50.00 | 975.00 |
| SMS5000 | 5,000 | $75.00 | 2,437.50 |
API Keys
Generate programmatic keys from Settings → Developer Keys. Keys are prefixed omf_live_ and shown only once at creation.
{ "name": "...", "scope"?: "..." }Request body — Generate key
| Field | Type | Description |
|---|---|---|
| name* | string | Descriptive label, e.g. "CRM Integration" |
| scopeopt | string | admin · manager · operator (default) · viewer — caps the key to that role's permissions, see Roles & Permissions |
Scope a key down for third-party integrations that only need to read data — e.g. scope: "viewer" for a reporting dashboard that should never be able to send campaigns or change billing.
const res = await fetch('https://omniflex.co.zw/api/api-keys', {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'My Integration', scope: 'viewer' }),
});
const { key_value } = await res.json(); // save this — shown once
const { data } = await axios.post(
'https://omniflex.co.zw/api/api-keys',
{ name: 'My Integration', scope: 'viewer' },
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } }
);
const { key_value } = data; // save this — shown once
resp = requests.post(
'https://omniflex.co.zw/api/api-keys',
headers={'Authorization': 'Bearer YOUR_API_KEY'},
json={'name': 'My Integration', 'scope': 'viewer'},
)
key_value = resp.json()['key_value'] # save this — shown once
$keyValue = Http::withToken('YOUR_API_KEY')
->post('https://omniflex.co.zw/api/api-keys', [
'name' => 'My Integration',
'scope' => 'viewer',
])->json('key_value'); // save this — shown once
key_value is returned only once. Store it securely. Revoke and regenerate from the dashboard if lost.
Webhooks
Subscribe an HTTPS endpoint to receive real-time delivery events instead of polling SMS Logs. Manage subscriptions from Settings → Webhooks or the API below.
Request body — Create subscription
| Field | Type | Description |
|---|---|---|
| url* | string | Must be https:// — events are POSTed here |
| events* | string[] | One or more of message.delivered, message.failed |
Register a webhook
const res = await fetch('https://omniflex.co.zw/api/webhooks', {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({
url: 'https://yourapp.com/webhooks/omniflex',
events: ['message.delivered', 'message.failed'],
}),
});
const { secret } = await res.json(); // save this — shown once, used to verify signatures
const { data } = await axios.post(
'https://omniflex.co.zw/api/webhooks',
{ url: 'https://yourapp.com/webhooks/omniflex', events: ['message.delivered', 'message.failed'] },
{ headers: { Authorization: 'Bearer YOUR_API_KEY' } }
);
const { secret } = data; // save this — shown once, used to verify signatures
resp = requests.post(
'https://omniflex.co.zw/api/webhooks',
headers={'Authorization': 'Bearer YOUR_API_KEY'},
json={
'url': 'https://yourapp.com/webhooks/omniflex',
'events': ['message.delivered', 'message.failed'],
},
)
secret = resp.json()['secret'] # save this — shown once, used to verify signatures
$secret = Http::withToken('YOUR_API_KEY')
->post('https://omniflex.co.zw/api/webhooks', [
'url' => 'https://yourapp.com/webhooks/omniflex',
'events' => ['message.delivered', 'message.failed'],
])->json('secret'); // save this — shown once, used to verify signatures
secret is returned only once, at creation. Store it securely — it's required to verify that incoming webhook requests genuinely came from OmniFlex.
Event payload
Every delivery is a POST with this JSON body:
{
"event": "message.delivered",
"data": {
"campaign_id": "cmp_abc123",
"phone": "263771234567",
"log_id": "log_xyz789"
},
"timestamp": "2026-07-08T10:30:00.000Z"
}
Verifying the signature
Each request includes an X-Omniflex-Signature header — a hex-encoded HMAC-SHA256 of the raw request body, signed with your webhook's secret. Recompute it and compare before trusting the payload.
import { createHmac, timingSafeEqual } from 'crypto';
function isValid(rawBody, signatureHeader, secret) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
const { createHmac, timingSafeEqual } = require('crypto');
// rawBody must be the exact, unparsed request body (e.g. express.raw())
function isValid(rawBody, signatureHeader, secret) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
import hmac, hashlib
def is_valid(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)
$expected = hash_hmac('sha256', $rawBody, $secret);
$isValid = hash_equals($expected, $signatureHeader);
Retries
| Attempt | Delay | Timeout |
|---|---|---|
| 1 | Immediate | 8 seconds |
| 2 | +2 seconds | 8 seconds |
| 3 | +5 seconds | 8 seconds |
Your endpoint must return a 2xx status within 8 seconds to count as delivered. After 3 failed attempts the delivery is logged as failed and not retried further — inspect delivery history from the dashboard.
Roles & Permissions
Every team member has one of four roles. API keys inherit the permissions of the admin who created them.
| Permission | Admin | Manager | Operator | Viewer |
|---|---|---|---|---|
| campaigns.view | ✓ | ✓ | ✓ | ✓ |
| campaigns.create | ✓ | ✓ | ✓ | ✗ |
| campaigns.edit | ✓ | ✓ | ✗ | ✗ |
| campaigns.delete | ✓ | ✓ | ✗ | ✗ |
| campaigns.send | ✓ | ✓ | ✓ | ✗ |
| contacts.create / edit | ✓ | ✓ | ✗ | ✗ |
| settings.billing | ✓ | ✗ | ✗ | ✗ |
| settings.devkeys | ✓ | ✗ | ✗ | ✗ |
| users.manage | ✓ | ✗ | ✗ | ✗ |