levilabs / laravel-otpiq
Modern Laravel SDK for OTPIQ: SMS, WhatsApp, and Telegram OTP delivery, delivery webhooks, reports, and WhatsApp campaigns for Iraq.
Requires
- php: ^8.2
- illuminate/console: ^11.0 || ^12.0 || ^13.0
- illuminate/contracts: ^11.0 || ^12.0 || ^13.0
- illuminate/database: ^11.0 || ^12.0 || ^13.0
- illuminate/http: ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^11.0 || ^12.0 || ^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.22
- orchestra/testbench: ^9.0 || ^10.0 || ^11.0
- pestphp/pest: ^3.8 || ^4.4
- pestphp/pest-plugin-laravel: ^3.2 || ^4.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-11-06 03:17:57 UTC
README
A modern Laravel SDK for OTPIQ — SMS, WhatsApp, and Telegram OTP delivery, smart fallback routing, delivery webhooks, CSV reports, and WhatsApp Business campaigns, with typed DTOs, multi-project config, and optional message persistence.
Maintained by Levi Labs — built by Nizam Omer
Official API docs: docs.otpiq.com · Dashboard: app.otpiq.com
Table of contents
- Requirements
- Installation
- Configuration
- Provider routing (
OTPIQ_DEFAULT_PROVIDER) - Multiple OTPIQ projects
- API reference coverage
- Send messages
- WhatsApp campaigns
- Sender IDs, pricing & delivery reports
- Track delivery
- Delivery webhooks
- WhatsApp inbound webhooks
- Facades & services
- Persistence & sync
- Testing
- Security
- License
Requirements
- PHP 8.2+ (Laravel 13.x requires PHP 8.3+ on the application)
- Laravel 11.x, 12.x, or 13.x
- An OTPIQ project API key from app.otpiq.com
Installation
composer require levilabs/laravel-otpiq php artisan vendor:publish --tag=otpiq-config php artisan migrate
Configuration
Publish copies config/otpiq.php. Set credentials in .env:
# Which entry in config('otpiq.projects') to use by default OTPIQ_PROJECT=default # Primary project (maps to projects.default in config) OTPIQ_API_KEY=sk_live_your_key_here OTPIQ_BASE_URL=https://api.otpiq.com/api # Default channel routing when a send DTO omits provider (see table below) OTPIQ_DEFAULT_PROVIDER=whatsapp-telegram-sms # Optional: merged into every send unless you pass DeliveryReportConfig on the DTO OTPIQ_WEBHOOK_URL=https://your-app.com/webhooks/otpiq/delivery OTPIQ_WEBHOOK_SECRET=your_webhook_secret OTPIQ_WEBHOOK_REPORT_TYPE=final # Optional HTTP tuning OTPIQ_HTTP_TIMEOUT=15 OTPIQ_HTTP_RETRY_TIMES=1 OTPIQ_HTTP_RETRY_SLEEP_MS=200
API keys
| Key prefix | Use |
|---|---|
sk_live… |
Production — messages are sent to the phoneNumber you pass in the API. |
sk_dev… |
Development — OTPIQ always delivers to the dev phone number you set in the dashboard; the number in your code is ignored. Never deploy dev keys. |
Guide: How dev keys work
Phone numbers
OTPIQ expects digits only, usually Iraq format without + or spaces (e.g. 9647501234567). The SDK normalizes via PhoneNumber::normalize() when you use the send DTOs.
Provider routing (OTPIQ_DEFAULT_PROVIDER)
The provider field tells OTPIQ which channels to try and in what order. If you omit provider on SendVerificationData / SendCustomMessageData, the SDK uses config('otpiq.defaults.provider'), which comes from OTPIQ_DEFAULT_PROVIDER.
| Value | Behavior |
|---|---|
sms |
SMS only (carrier). |
whatsapp |
WhatsApp only. |
telegram |
Telegram only. |
whatsapp-sms |
WhatsApp first; if that fails, SMS. |
telegram-sms |
Telegram first; if that fails, SMS. |
whatsapp-telegram-sms |
WhatsApp → Telegram → SMS (widest reach; default in config). |
Override per message with the Provider enum:
use LeviLabs\LaravelOtpiq\Enums\Provider; use LeviLabs\LaravelOtpiq\Data\Messaging\SendVerificationData; OtpiqMessage::sendVerification(new SendVerificationData( phoneNumber: '9647501234567', verificationCode: '123456', provider: Provider::WhatsAppSms, // ignores OTPIQ_DEFAULT_PROVIDER for this send ));
Reference: Send SMS API
Multiple OTPIQ projects
One Laravel application can send on behalf of several OTPIQ dashboard projects (production vs staging, white-label apps, etc.). Each project has its own API key and credit balance.
1. Add keys in config/otpiq.php:
'projects' => [ 'default' => [ 'api_key' => env('OTPIQ_API_KEY'), 'base_url' => env('OTPIQ_BASE_URL', 'https://api.otpiq.com/api'), ], 'staging' => [ 'api_key' => env('OTPIQ_STAGING_API_KEY'), 'base_url' => env('OTPIQ_BASE_URL', 'https://api.otpiq.com/api'), ], ],
2. .env:
OTPIQ_PROJECT=default OTPIQ_API_KEY=sk_live_prod_… OTPIQ_STAGING_API_KEY=sk_dev_…
3. Pass the project key on any call:
OtpiqMessage::sendVerification($data, project: 'staging'); OtpiqMessage::track($smsId, project: 'staging'); OtpiqProject::info(project: 'default');
If the project key is missing or has no api_key, the SDK throws OtpiqProjectException.
API reference coverage
This package wraps the REST API documented at docs.otpiq.com/api-reference/introduction. Every endpoint listed in the official reference is supported:
| Docs section | HTTP | OTPIQ path | Laravel facade / method |
|---|---|---|---|
| Project | |||
| Get Project Info | GET |
/info |
OtpiqProject::info() |
| Messaging & SMS | |||
| Send SMS | POST |
/sms |
OtpiqMessage::sendVerification() · sendCustom() · sendWhatsAppTemplate() (same API, different smsType bodies) |
| Track SMS | GET |
/sms/track/{smsId} |
OtpiqMessage::track($smsId) |
| International SMS Pricing | GET |
/international-sms-pricing |
OtpiqPricing::internationalSmsPricing() |
| Sender ID | |||
| Get Sender IDs | GET |
/sender-ids |
OtpiqSenderId::list() |
| Delivery reports | |||
| Generate Delivery Report | POST |
/reports/generate |
OtpiqReport::generate($start, $end, …) |
| List Delivery Reports | GET |
/reports |
OtpiqReport::list() |
| List WhatsApp Resources | GET |
/whatsapp/resources |
OtpiqWhatsApp::list() |
| WhatsApp campaigns | OtpiqWhatsAppCampaign facade |
||
| Create Campaign | POST |
/whatsapp/campaigns |
OtpiqWhatsAppCampaign::create([…]) |
| Get Recipient Schema | GET |
/whatsapp/campaigns/recipient-payload |
OtpiqWhatsAppCampaign::recipientPayload() |
| Attach Recipients | POST |
/whatsapp/campaigns/{id}/recipients |
OtpiqWhatsAppCampaign::attachRecipients($id, […]) |
| Send Sample | POST |
/whatsapp/campaigns/{id}/sample |
OtpiqWhatsAppCampaign::sendSample($id, […]) |
| Start Campaign | POST |
/whatsapp/campaigns/{id}/start |
OtpiqWhatsAppCampaign::start($id) |
| List Campaigns | GET |
/whatsapp/campaigns |
OtpiqWhatsAppCampaign::list() |
| Get Campaign Details | GET |
/whatsapp/campaigns/{id} |
OtpiqWhatsAppCampaign::get($id) |
| Update Campaign | PATCH |
/whatsapp/campaigns/{id} |
OtpiqWhatsAppCampaign::update($id, […]) |
| Delete Campaign | DELETE |
/whatsapp/campaigns/{id} |
OtpiqWhatsAppCampaign::delete($id) |
Not HTTP endpoints (documented separately):
- SMS delivery webhooks — configured on each send via
deliveryReportorOTPIQ_WEBHOOK_*; handled in your app withDeliveryWebhook(how webhooks work). - WhatsApp inbound webhooks — configured in the OTPIQ dashboard; verify with
WhatsAppInboundWebhookVerifier(WhatsApp webhook).
Aliases in composer.json: OtpiqWhatsAppCampaign → LeviLabs\LaravelOtpiq\Facades\OtpiqWhatsAppCampaign.
Send messages
Verification OTP (most common)
use LeviLabs\LaravelOtpiq\Data\Messaging\SendVerificationData; use LeviLabs\LaravelOtpiq\Facades\OtpiqMessage; $response = OtpiqMessage::sendVerification(new SendVerificationData( phoneNumber: '9647501234567', verificationCode: '123456', // provider: optional — defaults to OTPIQ_DEFAULT_PROVIDER // senderId: optional — custom SMS sender ID when supported )); // $response->smsId, $response->cost, $response->remainingCredit, $response->paymentType
Custom SMS / transactional text
Requires appropriate sender ID and OTPIQ product rules for non-OTP SMS:
use LeviLabs\LaravelOtpiq\Data\Messaging\SendCustomMessageData; OtpiqMessage::sendCustom(new SendCustomMessageData( phoneNumber: '9647501234567', customMessage: 'Your order #1042 is confirmed.', senderId: 'MyBrand', ));
WhatsApp template message
use LeviLabs\LaravelOtpiq\Data\Messaging\SendWhatsAppTemplateData; use LeviLabs\LaravelOtpiq\Enums\Provider; OtpiqMessage::sendWhatsAppTemplate(new SendWhatsAppTemplateData( phoneNumber: '9647501234567', templateName: 'verification_template', whatsappAccountId: '68c46fecc509cdcec8fb3ef2', whatsappPhoneId: '68da31fb518ac3db3eb0a0f4', templateParameters: ['body' => ['1' => '123456']], provider: Provider::WhatsApp, ));
List connected WhatsApp assets with OtpiqWhatsApp::list() (maps to List WhatsApp Resources in the API docs).
WhatsApp campaigns
Bulk template messaging uses the WhatsApp Campaigns API (campaign docs). Use the OtpiqWhatsAppCampaign facade (not OtpiqMessage).
Typical flow:
- Resources —
OtpiqWhatsApp::list()fortemplateId,phoneNumberId, etc. - Recipient schema —
OtpiqWhatsAppCampaign::recipientPayload(templateId: '…')orrecipientPayload(campaignId: '…'). - Create —
create()withname,templateId,phoneNumberId,scheduleType(noworschedule). - Attach recipients —
attachRecipients($campaignId, ['recipients' => […]]). - Start —
start($campaignId)(forscheduleType: nowdrafts). Scheduled campaigns start automatically atscheduledFor.
use LeviLabs\LaravelOtpiq\Facades\OtpiqWhatsApp; use LeviLabs\LaravelOtpiq\Facades\OtpiqWhatsAppCampaign; $resources = OtpiqWhatsApp::list(); $campaign = OtpiqWhatsAppCampaign::create([ 'name' => 'Summer promo', 'templateId' => '664abc123def456789012345', 'phoneNumberId' => '664def456abc789012345678', 'scheduleType' => 'now', ]); $id = $campaign['_id']; // MongoDB id from API $schema = OtpiqWhatsAppCampaign::recipientPayload(templateId: '664abc123def456789012345'); OtpiqWhatsAppCampaign::attachRecipients($id, [ 'recipients' => [ // shape must match $schema['exampleRecipient'] / fields from recipient-payload ], ]); OtpiqWhatsAppCampaign::start($id); // Optional: test before full send OtpiqWhatsAppCampaign::sendSample($id, ['recipients' => [/* … */]]); // Monitor OtpiqWhatsAppCampaign::list(status: 'running'); OtpiqWhatsAppCampaign::get($id, page: 1, limit: 20);
Other campaign methods: update($id, …) (draft/scheduled only), delete($id) (draft/scheduled only). Pass ?string $project = null as the last argument on any call for multi-project setups.
Sender IDs, pricing & delivery reports
use LeviLabs\LaravelOtpiq\Facades\OtpiqPricing; use LeviLabs\LaravelOtpiq\Facades\OtpiqReport; use LeviLabs\LaravelOtpiq\Facades\OtpiqSenderId; $senderIds = OtpiqSenderId::list(); $pricing = OtpiqPricing::internationalSmsPricing(page: 1, limit: 100, countryIso: 'IQ'); $reports = OtpiqReport::list(limit: 50, statuses: ['available']); $newReport = OtpiqReport::generate( '2026-01-01T00:00:00.000Z', '2026-01-31T23:59:59.999Z', ); // $newReport['downloadUrl'] — CSV download link from OTPIQ
Track delivery
Poll status when you do not rely solely on webhooks:
$tracking = OtpiqMessage::track($response->smsId); // $tracking->status (pending, sent, delivered, read, failed) // $tracking->isFinalStatus, $tracking->lastChannel, $tracking->channelFlow
Delivery webhooks
Configure OTPIQ_WEBHOOK_* or pass DeliveryReportConfig on a single send. OTPIQ POSTs JSON to your URL and sends X-OTPIQ-Webhook-Secret (docs).
OTPIQ_WEBHOOK_REPORT_TYPE:
| Value | Meaning |
|---|---|
final |
Webhooks only when the message reaches a terminal state (delivered or failed). |
all |
Webhooks for intermediate states too (e.g. sent). |
Example route handler:
use LeviLabs\LaravelOtpiq\Support\DeliveryWebhook; $report = DeliveryWebhook::dispatchIfVerified( payload: $request->all(), receivedSecret: $request->header('X-OTPIQ-Webhook-Secret'), expectedSecret: config('otpiq.delivery_report.webhook_secret'), project: config('otpiq.default'), ); if ($report === null) { abort(401); } return response('OK');
Respond with 2xx within ~10 seconds. Use $report->smsId for idempotency.
WhatsApp inbound webhooks
Separate from SMS delivery webhooks. Configure per phone number in the OTPIQ dashboard. Verify with HMAC (X-OTPIQ-Webhook-Signature, X-OTPIQ-Webhook-Timestamp) using WhatsAppInboundWebhookVerifier — inbound webhook docs.
Facades & services
| Facade | Contract | Covers |
|---|---|---|
OtpiqProject |
OtpiqProjectServiceContract |
Project info |
OtpiqMessage |
OtpiqMessagingServiceContract |
Send SMS API + track |
OtpiqSenderId |
OtpiqSenderIdServiceContract |
Sender IDs |
OtpiqPricing |
OtpiqPricingServiceContract |
International SMS pricing |
OtpiqReport |
OtpiqReportServiceContract |
Delivery report CSVs |
OtpiqWhatsApp |
OtpiqWhatsAppResourceServiceContract |
WhatsApp resources |
OtpiqWhatsAppCampaign |
OtpiqWhatsAppCampaignServiceContract |
All 10 campaign endpoints |
Resolve contracts from the container if you prefer constructor injection.
Persistence & sync
otpiq_messages— upserted on send (MessageSent) and on webhook / track updates (DeliveryStatusUpdated).php artisan otpiq:sync-statuses— re-calls the track API for rows whereis_finalis false (missed webhooks).
php artisan otpiq:sync-statuses --limit=100
Testing
composer test
composer analyse
composer format
Security
See SECURITY.md. Never expose OTPIQ_API_KEY or webhook secrets in client-side code, logs, or public repositories.
License
MIT — see LICENSE.md.
OTPIQ is a commercial messaging service; billing and refunds follow OTPIQ’s terms and privacy policy.