Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Threadwire Laravel Laravel Package

tritech/threadwire-laravel

Laravel integration for Threadwire by Tritech, providing familiar service provider wiring and configuration to add Threadwire functionality to your app. It follows Laravel conventions so setup stays simple and your code remains clean and maintainable.

View on GitHub
Deep Wiki
Context7

Getting Started

Threadwire wraps a chat/messaging provider's HTTP API behind a Laravel facade, so most work is: publish config, set credentials, call the facade.

composer require threadwire/laravel   # adjust to the real package name
php artisan vendor:publish --tag="threadwire-config"

Then in .env:

THREADWIRE_TOKEN=...
THREADWIRE_INSTANCE_ID=...

Look first at config/threadwire.php (base URL, timeout, default instance) and the facade class itself — it's the authoritative list of what's callable. As of v1.1.0 the two entry points worth learning first are:

use Threadwire\Laravel\Facades\Threadwire;

// Poll for webhook events, oldest first
$page = Threadwire::events(['types' => ['message.received']]);

foreach ($page->events as $event) {
    // $event->id, $event->type, $event->instance_id, ...
}

$page->next_after; // pass back as 'after' on the next call
$page->has_more;
// A chat's history from the linked phone, newest first
$history = Threadwire::chatHistory($chatId);

First use case: local development and any deployment without a public webhook URL. Instead of exposing /webhooks/threadwire, run a poller and feed the same event handler you'd have used for the webhook. This also becomes your recovery path the first time a webhook is silently dropped.

Implementation Patterns

Cursor-based catch-up poller. Persist the cursor, loop while has_more, and never assume a page is the whole backlog.

// app/Console/Commands/PollThreadwireEvents.php
public function handle(): int
{
    $cursor = Cache::get('threadwire:cursor'); // nullable on first run
    $guard  = 0;

    do {
        $page = Threadwire::events(array_filter([
            'after'      => $cursor,
            'types'      => config('threadwire.poll_types'),
            'instance_id'=> config('threadwire.instance_id'),
        ]));

        foreach ($page->events as $event) {
            dispatch(new HandleThreadwireEvent($event))->onQueue('threadwire');
        }

        $cursor = $page->next_after ?? $cursor;
        Cache::put('threadwire:cursor', $cursor, now()->addYear());
    } while ($page->has_more && ++$guard < 50);

    return self::SUCCESS;
}

Schedule it every minute (or every five) rather than tight-looping:

$schedule->command('threadwire:poll')->everyMinute()->withoutOverlapping();

One handler, two transports. The strongest pattern here is treating the webhook and the poller as interchangeable producers. Normalise both into a single job keyed by the event id, so webhook delivery and events() retrieval converge:

final class HandleThreadwireEvent implements ShouldQueue
{
    public function __construct(public array $event) {}

    public function handle(): void
    {
        $key = "threadwire:event:{$this->event['id']}";

        if (! Cache::add($key, true, now()->addDays(7))) {
            return; // already processed via webhook (or vice versa)
        }

        match ($this->event['type']) {
            'message.received' => $this->onMessage($this->event),
            default            => null,
        };
    }
}

Backfill after downtime. events() oldest-first with a cursor is exactly the shape you want for replaying an outage window. Store the cursor on a per-instance basis if you run multiple instances, and add a --from option that accepts a known-good after value so operators can rewind deliberately.

Building thread views with chatHistory(). Because history is newest-first and anchored to the linked phone, treat it as a view hydration call, not a sync source: reverse for chronological rendering, and merge it with your own stored messages keyed by provider message id rather than replacing your table.

$messages = collect(Threadwire::chatHistory($chat->provider_id))
    ->sortBy('timestamp')
    ->values();

Multi-instance fan-out. Run a poller per instance_id with its own cursor, or one poller with no instance_id filter and a match on the event's instance. The second is fewer API calls; the first isolates failures and rate-limit budgets.

Test with Http::fake(). Both new methods are plain HTTP underneath, so you can assert cursor handling without touching the network:

Http::fake([
    '*/v1/events*' => Http::sequence()
        ->push(['events' => [/* ... */], 'next_after' => 'c2', 'has_more' => true])
        ->push(['events' => [], 'next_after' => 'c2', 'has_more' => false]),
]);

$this->artisan('threadwire:poll')->assertSuccessful();

Http::assertSent(fn ($r) => str_contains($r->url(), 'after=c1'));

Gotchas and Tips

  • Persist next_after, not after. after is your input cursor; the page tells you where to resume. Storing the value you sent re-fetches the same page forever.
  • has_more is not optional. Always loop on it. A single call is a partial view of the backlog, and code that assumes otherwise will "lose" events that were merely on page 2.
  • Oldest-first is a backfill tool, not a realtime feed. Polling events() every 60s gives you minute-level latency and burns rate limit. Keep the webhook as the primary path where you can expose a URL.
  • Deduplicate across transports. Providers redeliver webhooks, and a poller running alongside a webhook will legitimately see the same event twice. Make the event id your idempotency key and keep the TTL longer than your worst-case replay window.
  • Don't let an exception advance the cursor. If you persist the cursor before dispatching (or before the job commits), a failed batch is gone. Persist after enqueue, and prefer dispatchAfterResponse/queued jobs over inline handling so a slow handler doesn't block the loop.
  • chatHistory() is newest-first and appears unbounded by cursor. Reverse before rendering, and be wary of dumping a very long thread into a single response — paginate in your UI layer or slice defensively rather than assuming the array is small.
  • Filter values are exact strings. types must match the provider's event names verbatim (message.received, not message_received or MessageReceived). Keep the canonical list in config so a typo is a one-line fix, not a hunt.
  • Empty types array ≠ "no filter." Depending on the client's query-string serialisation, an empty array may serialise to nothing (fine) or to types[]= (returns nothing). Wrap optional filters in array_filter().
  • Clock and ordering assumptions. Oldest-first ordering is by the provider's cursor, not your created_at. Don't sort locally on timestamps you generated, and don't compare next_after values numerically — they're opaque.
  • Debugging tip: dd(Threadwire::events(['after' => $cursor])) once at the start of a session to confirm the page shape (events/next_after/has_more as properties vs. array keys) — the response object is the most common source of silent null cursors.
  • Config quirks worth checking: base URL override for sandbox environments, request timeout/retry settings, and whether the default instance_id in config is applied when you omit the argument. A wrong default instance is the classic cause of "the API returns zero events."
  • Extension points: the facade is typically macroable, and the underlying client is usually resolvable from the container — bind a fake or a decorated client in a service provider if you need logging, metrics, or per-tenant credentials rather than editing package code.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core