automateflow / laravel
Connect a Laravel application to an AutomateFlow workspace: sync contacts, send mail through the transactional API, embed subscription forms, trigger automations, and receive signed webhooks.
Requires
- php: ^8.2
- illuminate/bus: ^11.0|^12.0
- illuminate/http: ^11.0|^12.0
- illuminate/queue: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
- symfony/mailer: ^7.0
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^11.0
README
Connect a Laravel application to an AutomateFlow workspace. The Laravel counterpart of
wordpress/automateflow, and like it, a client of the public /api/v1 contract with no
privileged access — the same surface any third party gets.
composer require automateflow/laravel php artisan vendor:publish --tag=automateflow-config
AUTOMATEFLOW_URL=https://app.example.com AUTOMATEFLOW_KEY=your-workspace-api-key
php artisan automateflow:ping
Every feature is off until switched on. Installing a package should not start shipping your user table to a third party or rewriting how your application sends mail.
What it does
| Feature | Switch | Entry point |
|---|---|---|
| Contact sync | AUTOMATEFLOW_SYNC_CONTACTS |
SyncsToAutomateFlow trait, Registered listener, automateflow:sync-contacts |
| Transactional mail | config/mail.php | automateflow mail transport |
| Subscription forms | AUTOMATEFLOW_FORMS |
<x-automateflow-subscribe-form uuid="…" /> |
| Automation triggers | — | AutomateFlow::triggerAutomation(), TriggerAutomation job |
| Campaigns | — | automateflow:campaigns |
| Incoming webhooks | AUTOMATEFLOW_WEBHOOKS |
WebhookReceived event |
Contact sync
Add the trait to your user model and sync where it makes sense:
use AutomateFlow\Laravel\Concerns\SyncsToAutomateFlow; class User extends Authenticatable { use SyncsToAutomateFlow; } $user->syncToAutomateFlow(); $user->triggerAutomateFlow('trial_expiring', ['days_left' => 3]);
Nothing hooks model events, deliberately. A saved observer fires for every factory in
your test suite, every seeder row and every unrelated column update — a queued network call
each time, and in tests against an API that is not there. The package wires up only the one
moment it can be sure of, Laravel's Registered event; everything else is your call.
Map extra fields in config/automateflow.php. Values may be an attribute name or a callable,
so a computed field needs no column:
'attributes' => [ 'plan' => 'subscription_plan', 'lifetime_value' => fn ($user) => $user->orders()->sum('total'), ],
Backfill existing users with php artisan automateflow:sync-contacts. It queues rather than
sends — see Rate limiting below.
Point a mailer at the transport:
'mailers' => [ 'automateflow' => ['transport' => 'automateflow'], ],
For a fallback, use Laravel's own failover transport, not something in this package:
'default' => env('MAIL_MAILER', 'failover'), 'mailers' => [ 'failover' => [ 'transport' => 'failover', 'mailers' => ['automateflow', 'smtp'], ], ],
The WordPress plugin hand-rolls a fallback because WordPress has no such primitive. Laravel
does, and it is better than a reimplementation would be: it remembers which transport failed
and prefers the next for a cooldown, rather than paying the timeout on every message. The
transport throws TransportException precisely so failover can catch it.
Webhooks
Register https://your-app.test/automateflow/webhook as an endpoint in AutomateFlow, put the
generated secret in AUTOMATEFLOW_WEBHOOK_SECRET, then listen:
Event::listen(WebhookReceived::class, function (WebhookReceived $event) { if ($event->is('contact.bounced') && $email = $event->email()) { // your policy here } });
The controller dispatches an event and does nothing else. What a bounce means for an application — suspend the account, flag the row, ignore it — is a decision this package should not make for you.
With no secret configured the endpoint returns 503 rather than trusting the caller, matching the platform's own posture on an unset SNS secret.
Contract details that are easy to get wrong
- The webhook signature is the bare hex digest.
hash_hmac('sha256', $body, $secret)inX-Webhook-Signature, with nosha256=prefix. A prefix is tolerated in case one is added later, but a receiver that strips one unconditionally rejects every real delivery. Pinned by a test. - The signature covers the raw body. Verified against
$request->getContent(); re-encoding the decoded array reorders keys and breaks every signature. POST /contactsupserts on(workspace, email), so sync is "send current state"./transactional/sendtakes one recipient. A message to N people is N requests. Cc and Bcc are expanded into ordinary recipients — that loses the appearance of a copy, not the copy.POST /campaigns/{id}/sendaccepts onlydraftandscheduledand 422s otherwise.- A zone-less
scheduled_atmeans the workspace's timezone, not UTC and not yours. Send an ISO-8601 string with an offset if you mean a specific instant.
Rate limiting shapes the design
A key is limited per minute (60 by default). Everything bulk is therefore a queued job that releases itself on a 429 rather than a loop that fails partway through with no record of where. Throttling costs latency; it must never cost a record.
ThrottledException is the one exception callers should handle rather than surface.
Testing
composer install composer test # 27 tests composer format
The suite runs on Orchestra Testbench with Http::fake() — no network, no AutomateFlow
install required.
License
MIT.