Base URL https://omniflex.co.zw/api
⚡ REST API

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.

📱
Networks
Econet & NetOne
🔒
Auth
JWT & API Keys
Format
JSON / REST

Quick Start — Send your first SMS

Select your language above, then copy the snippet below.

Send SMS
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.

HTTP 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)

POST /api/auth/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');
Response
{
  "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 shape
{
  "error":  "Insufficient credits",
  "detail": "You need 150 credits but only have 42."
}
200OK — request succeeded
201Created — resource created
400Bad Request — invalid payload
401Unauthorized — missing/invalid token
402Payment Required — insufficient credits
403Forbidden — insufficient role
404Not Found
409Conflict — duplicate entry
429Too Many Requests — rate limited
502Bad Gateway — SMS gateway error

Rate Limits

LimitWindowScope
300 requests1 minuteAll endpoints (per IP)
5 OTP sends10 minutesPer identifier (phone/email)
5 wrong verificationsPer codeCode locked after 5 failures

SMS Campaigns

Campaigns organise bulk sends with a shared template, sender ID, and schedule. Recipients are resolved at dispatch time.

GET
/api/campaigns
List all campaigns
🔒 Auth
POST
/api/campaigns
Create a campaign
🔒 Auth + Permission
GET
/api/campaigns/:id
Get campaign details
🔒 Auth
PATCH
/api/campaigns/:id
Update campaign
🔒 Auth + Permission
DELETE
/api/campaigns/:id
Delete campaign
🔒 Auth + Permission

Request body — Create campaign

FieldTypeDescription
name*stringCampaign label
typeoptstring"SMS" (default) or "OTP"
message_templateoptstringMessage body. Use {{name}} for personalisation
sender_idoptstringApproved sender ID e.g. "MYBRAND"
statusoptstringdraft · active · scheduled · completed
scheduled_dateoptISO stringAuto-send at this UTC time (requires status: "scheduled")
recipientsoptarray[{phone, name?, message?}]

Create an SMS Campaign

POST /api/campaigns
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();
Response 201
{
  "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.

POST
/api/campaigns/:id/send-otp
Send an OTP to a phone or email under this campaign
🔒 Auth
FieldTypeDescription
identifier*stringPhone (263771234567) or email address
nameoptstringRecipient name for template personalisation

Send OTP via Campaign

POST /api/campaigns/:id/send-otp
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

POST /api/otp/verify
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

RuleValue
Code length6 digits
Code TTL5 minutes
Send rate limit5 per identifier per 10 minutes
Max failed verifications5 (code invalidated after)
ChannelsSMS (Econet / NetOne) or email

Send SMS

Dispatch to one recipient, an array of numbers, or a full recipient objects array with per-contact personalisation.

POST
/api/sms/send
Send SMS to one or many recipients
🔒 Auth
FieldTypeDescription
phonestringSingle number e.g. 263771234567
phonesstring[]Array of numbers (max 100)
recipientsobject[][{phone, name?, message?}] — enables personalisation
message*stringMessage body. Use {{name}} as placeholder
senderIdoptstringApproved sender ID (Econet)
campaign_idoptstringLink to a campaign for DLR reporting
scheduleoptbooleanSet true to use sendingTime
sendingTimeoptISO stringUTC datetime for scheduled delivery

Single recipient

POST /api/sms/send — single
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

POST /api/sms/send — bulk
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

POST /api/sms/send — personalised
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!'],
        ],
    ]);
Response
{ "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.

POST
/api/otp/send
Send a 6-digit OTP code
Public
POST
/api/otp/verify
Verify the code the user entered
Public
POST /api/otp/send
// 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.

GET
/api/contacts
List all contacts with group memberships
🔒 Auth
POST
/api/contacts
Create a contact
🔒 Auth + Permission
PATCH
/api/contacts/:id
Update contact or group memberships
🔒 Auth + Permission
DELETE
/api/contacts/:id
Delete a contact
🔒 Auth + Permission
POST /api/contacts
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.

GET
/api/contact-groups
List groups with contact counts
🔒 Auth
POST
/api/contact-groups
Create a group — body: { "name": "VIP" }
🔒 Auth + Permission
PATCH
/api/contact-groups/:id
Rename a group
🔒 Auth + Permission
DELETE
/api/contact-groups/:id
Delete group (contacts not deleted)
🔒 Auth + Permission

Sender IDs

Alphanumeric sender names (up to 11 characters) shown instead of a number on Econet. Require network approval before use.

GET
/api/sender-ids
List sender IDs and approval statuses
🔒 Auth
POST
/api/sender-ids
Register a new sender ID (starts pending)
🔒 Auth
GET
/api/sender-ids/:id
Retrieve a single sender ID
🔒 Auth
PATCH
/api/sender-ids/:id
Update description or status fields
🔒 Auth
PATCH
/api/sender-ids/:id/activate
Set as the active default sender ID
🔒 Auth
DELETE
/api/sender-ids/:id
Delete a sender ID
🔒 Auth
StatusMeaning
pendingSubmitted, awaiting network approval
approvedReady to use in campaigns
rejectedRejected by the network

Campaign Files

Upload a parsed recipient list (from CSV/spreadsheet) as JSON. Reference the file ID when building campaigns.

POST
/api/campaign-files
Upload recipient list
🔒 Auth
GET
/api/campaign-files/:id
Retrieve uploaded file
🔒 Auth
POST /api/campaign-files
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.

GET
/api/sms-logs
List delivery logs
🔒 Auth
GET
/api/sms/dlr
Fetch DLR status for a campaign
🔒 Auth
Query paramDescription
campaign_idFilter by campaign
dlr_statusdelivered · failed · pending
limitMax records (default 1000)
exporttrue — remove limit for full export
GET /api/sms-logs
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.

GET
/api/account-balance
Get current credit balance
🔒 Auth
GET
/api/transactions
List credit transaction history
🔒 Auth
GET /api/account-balance
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
PackageCreditsUSDZWG
SMS500500$15.00406.25
SMS10001,000$25.00812.50
SMS25002,500$50.00975.00
SMS50005,000$75.002,437.50

API Keys

Generate programmatic keys from Settings → Developer Keys. Keys are prefixed omf_live_ and shown only once at creation.

GET
/api/api-keys
List API keys
🔒 Admin
POST
/api/api-keys
Generate a new key — body: { "name": "...", "scope"?: "..." }
🔒 Admin
DELETE
/api/api-keys/:id
Revoke a key immediately
🔒 Admin

Request body — Generate key

FieldTypeDescription
name*stringDescriptive label, e.g. "CRM Integration"
scopeoptstringadmin · 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.

POST /api/api-keys
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.

GET
/api/webhooks
List webhook subscriptions
🔒 Admin
POST
/api/webhooks
Create a subscription
🔒 Admin
PATCH
/api/webhooks/:id
Update URL, events, or active state
🔒 Admin
DELETE
/api/webhooks/:id
Remove a subscription
🔒 Admin

Request body — Create subscription

FieldTypeDescription
url*stringMust be https:// — events are POSTed here
events*string[]One or more of message.delivered, message.failed

Register a webhook

POST /api/webhooks
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:

JSON
{
  "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.

Verify X-Omniflex-Signature
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

AttemptDelayTimeout
1Immediate8 seconds
2+2 seconds8 seconds
3+5 seconds8 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.

PermissionAdminManagerOperatorViewer
campaigns.view
campaigns.create
campaigns.edit
campaigns.delete
campaigns.send
contacts.create / edit
settings.billing
settings.devkeys
users.manage
Omniflex API — © 2026 Tyflex Investments
support@omniflex.co.zw omniflex.co.zw