zeevx/lara-termii
LaraTermii integrates the Termii messaging API into Laravel, making it easy to send SMS and OTP tokens, manage sender IDs and channels, and add Termii-powered messaging to Laravel notifications and applications.
Minimum viable setup:
composer require the package, then publish its config (php artisan vendor:publish --tag=termii-config or the tag listed in the README) so config/termii.php exists.TERMII_API_KEY, sender ID (approved alphanumeric, e.g. TERMII), and the base URL — the eSIM client reuses the same key and base URL, so you configure them once.$termii = app(LaraTermii::class); (or the LaraTermii facade alias). Never new it up — the sub-clients are wired in the service provider.Where to look first:
src/Concerns/Engage for the Termii Engage (messaging) methods you actually have. After 1.3.0 the method list lives in traits, not on LaraTermii, so IDE autocompletion on the root class can look thin.src/Concerns/Sotel for the nine eSIM endpoints.TermiiException for the exception vocabulary, including the new esimAuthenticationFailed().First use case — an SMS OTP:
$termii->sendMessage(to: '2348012345678', from: config('termii.sender_id'), sms: 'Your code is 123456');
Then verify the number format, the sender ID approval status, and that you are hitting the correct base URL before building anything else.
Resolve sub-clients, don't reach through the root class. The 1.3.0 split into Concerns\Engage and Concerns\Sotel signals the intended shape: $termii->esim()->dataPlans() for eSIM, the Engage-facing methods for messaging. Treat the root object as a factory and inject the sub-client into the class that needs it — that keeps your own code stable if the package reorganises again.
OTP flow. Send with the messaging API, persist a hashed code plus an expiry and attempt counter, and verify server-side. Do not use the send call's return as proof of delivery — queue it, log the message ID, and let a webhook or a status poll confirm.
eSIM journey. The nine endpoints map cleanly onto a provisioning flow:
$countries = $termii->esim()->countries();
$plans = $termii->esim()->dataPlans();
$esim = $termii->esim()->createEsim(...);
$order = $termii->esim()->purchasePlan(...); // non-idempotent
$qr = $termii->esim()->qrCode($esim->id);
// later: profile(), usage(), planStatus(), esims()
Wrap createEsim() and purchasePlan() in a database transaction plus a local record keyed by your own idempotency reference, and run them from a queued job with tries = 1 (or a failed() handler that flags for manual reconciliation). Everything read-only — countries(), dataPlans(), usage(), planStatus() — is safe to cache and safe to retry.
Queue and rate-limit everything outbound. A ShouldQueue job per send, with backoff() and a retryUntil() window, keeps a bulk campaign from burning your Termii rate limit and from blocking a request cycle.
Testing. It is an HTTP-client wrapper, so Http::fake() in feature tests is enough — assert the outgoing URL and payload, and assert your job handles a TermiiException path. Add one test that fakes esimAuthenticationFailed() so a token-exchange regression fails loudly rather than silently in production.
Error handling. Catch TermiiException at the boundary of each job/controller and branch: esimAuthenticationFailed() is a configuration/credential problem and should alert, not retry; messaging failures are usually per-recipient and belong in a per-row result, not a thrown exception that kills a batch.
sendMessageWithNumber() is gone. It was added in 1.2.0 and removed in 1.3.0 because Termii's live v4 gateway 404s on sms/number/send. Grep your codebase before upgrading — the failure mode was an exception at runtime, not a compile error, so it hides behind untested branches. Fall back to the supported Engage send path or a raw Http::withToken(...) call if you genuinely need that endpoint, and pin the version if you cannot migrate yet.LaraTermii, method_exists() checks against the concrete class, cached reflection lists, or a decorator that proxies __call. If you do any of that, run your suite before shipping.esimAuthenticationFailed(). If messaging works and eSIM does not, the cause is almost always API-key scope or an eSIM/Sotel product not being enabled on the account — not your sender ID, and not the base URL.purchasePlan() is not idempotent. A timeout does not mean the purchase failed. Record your intent locally first, and reconcile with planStatus()/esims() rather than blindly retrying — the same warning applies to createEsim().TERMII_* env values, php artisan config:clear — a stale cached config is the most common "it works locally" report with this package.TermiiException body at the job boundary; Termii's error strings are specific enough to distinguish auth, sender, and balance problems without a support ticket.How can I help you explore Laravel packages today?