Download the PHP package tritech/threadwire-laravel without Composer
On this page you can find all versions of the php package tritech/threadwire-laravel. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download tritech/threadwire-laravel
More information about tritech/threadwire-laravel
Files in tritech/threadwire-laravel
Package threadwire-laravel
Short Description Official Laravel package for Threadwire: send WhatsApp messages through your own linked numbers with built-in ban protection, verify phone numbers with WhatsApp, and receive signed webhooks as Laravel events.
License MIT
Homepage https://threadwire.tri-tech.net
Informations about the package threadwire-laravel
Threadwire for Laravel
The official Laravel package for Threadwire, the WhatsApp API that keeps your numbers safe.
- Send texts, files, locations, contact cards and polls from your own linked WhatsApp numbers, to a person or a group, in one line.
- Broadcasts: one message to many people who wrote to you, each sent on its own, paced, with an opt-out line, pausing by itself if the number is at risk.
- Verify phone numbers with WhatsApp: a one-tap link where the person sends you the code (the safest message there is), or a classic code.
- Receive webhooks as Laravel events: replies, delivery and read receipts, reactions and more, with the signature checked for you.
- Protected by default: every message goes through Threadwire's pacing, warm-up and daily limits, so a number is far less likely to be banned. A message that would put the number at risk is held or refused, with the reason.
Requires PHP 8.2+ and Laravel 12 or 13, and a Threadwire account (7 days free).
Install
Laravel finds the service provider and the Threadwire facade on its own.
Configure
In .env:
To change more, publish the config: php artisan vendor:publish --tag=threadwire-config.
Send
Each number has an id (instance_id), shown on its page in your panel. Send to a phone in international format, or to a group's or channel's chat_id:
Read and cancel:
Poll for events instead of a webhook
No public address for a webhook (a server behind a firewall, a script on a schedule), or catching up after downtime? Poll: you get the same events, in the same shape, oldest first. Keep next_after and pass it back; ask again at once while has_more is true, otherwise wait a few seconds. An event your webhook also got has the same id, so skip ids you have handled.
WhatsApp OTP
Prove someone holds a WhatsApp number, for a sign-up or a password-free sign-in. Use the link unless you must send the code yourself: the person sends you the code, so your number only replies (the safest message there is, with no night hours or daily limit).
Broadcasts
One message to many people who opted in: people who wrote to your number. Each gets their own message, one at a time, in daytime, with an opt-out line added, and the broadcast pauses by itself if WhatsApp warns the number or too many people reply STOP. A list is filtered to people who wrote; left_out says who was not, and why.
Listen for Threadwire\Events\BroadcastPaused (with reason.code and reason.text) and Threadwire\Events\BroadcastCompleted.
Anything else in the API: Threadwire::json('get', 'instances/'.$numberId.'/groups'), or Threadwire::call(...) for the raw response. Single items come back as the array under data; lists come back whole (data, links, meta). The full reference is at https://threadwire.tri-tech.net/docs/api.
When Threadwire says no
A refusal throws a Threadwire\Exceptions\ThreadwireException. Its message is Threadwire's own reason, in plain words, fit to show or log. A subclass names the common cases:
| Exception | Status | What to do |
|---|---|---|
ValidationException |
422 | Fix the request; $e->errors says what was wrong with each field |
ConflictException |
409 | Not possible right now (the number is not connected, a message can no longer be cancelled); $e->retryAfter when it is worth trying again |
RateLimitedException |
429 | A limit was reached; wait $e->retryAfter seconds. The message says which limit |
AuthenticationException |
401 | The key is missing, wrong or revoked |
ForbiddenException |
403 | The key may not do this, or the account is suspended |
NotFoundException |
404 | No such number or message, or not one this key reaches |
A network failure is Laravel's own Illuminate\Http\Client\ConnectionException. Nothing is retried for you: retry a send only with the same idempotency_key, so it can never go twice.
Protecting your numbers
WhatsApp bans numbers that behave like bots, so Threadwire sends the way a person would, and some messages wait or are refused:
- A send is answered at once (
queued) and goes out paced. A message may wait its turn, or be refused with a reason: it then endsfailedwith the reason inerror, in amessage.statuswebhook and inThreadwire::message($id). - Never send a refused message again in a loop. The reason says what to do instead ("Try again tomorrow", "let them write first"); a loop only makes WhatsApp trust the number less.
- Replies to people who wrote in the last day go within seconds. Messages to people who never wrote are limited each day, so let people write first: each number's
chat_link(inThreadwire::instance()) opens a chat with it. Threadwire::instance($id)['protection']shows where a number stands today.
Receive webhooks
Set your webhook URL in the Threadwire panel (Webhooks → Set webhook URL) to https://your-app.example/threadwire/webhook. The package registers that route, outside the web group (no session, no CSRF token), and lets through only events signed with your secret within the last 5 minutes; anything else gets 401.
Each event is dispatched as a Laravel event: Threadwire\Events\WebhookReceived for every event, and one for its type:
| Webhook | Laravel event |
|---|---|
message.received |
MessageReceived |
message.sent_from_phone |
MessageSentFromPhone |
message.status |
MessageStatusUpdated |
message.edited, message.deleted |
MessageEdited, MessageDeleted |
message.poll_vote, message.reaction |
PollVoted, ReactionReceived |
group.message, group.update |
GroupMessageReceived, GroupUpdated |
call.received, call.rejected |
CallReceived, CallRejected |
instance.status |
InstanceStatusUpdated |
webhook.test |
WebhookTested |
Every one has id, type, createdAt, data (for message events, the message as the API shows it) and payload (the whole event). A type added later reaches WebhookReceived.
- Answer within 15 seconds: make slow listeners queued (
ShouldQueue), as above. Anything but a 2xx is retried, for about a day. - Events can arrive out of order: use
createdAtand the message's own times. - Several secrets: an instance with its own webhook URL has its own secret. List them comma-separated in
THREADWIRE_WEBHOOK_SECRETwhen they post to the same app. After you make a new secret in the panel, events carry both signatures for 24 hours, so either secret verifies meanwhile. - Your own route: set
THREADWIRE_WEBHOOK_PATH=(empty) and put thethreadwire.webhookmiddleware on a route of yours instead. If you publish the config, keep itswebhook.pathkey: without it no route is registered. Threadwire\Webhooks\WebhookSignaturechecks a signature anywhere else:(new WebhookSignature($secret))->verify($id, $timestamp, $signatures, $rawBody).
Testing your app
Fake Threadwire with Laravel's own Http::fake():
Developing the package (maintainers)
The webhook tests sign requests with a copy of Threadwire's own signing code, and Threadwire's test suite verifies its real signatures with this package's WebhookSignature, so the two cannot drift apart.
License
MIT
All versions of threadwire-laravel with dependencies
illuminate/contracts Version ^12.0|^13.0
illuminate/http Version ^12.0|^13.0
illuminate/routing Version ^12.0|^13.0
illuminate/support Version ^12.0|^13.0