What this is for#
AcelleMail has three distinct webhook systems, and they're often confused. This reference documents each, with the real event names from the Acelle source (per config/webhook_events.php and the webhooks migration) — not the invented event names some older third-party guides list.
| System |
Direction |
Use case |
Configured at |
| Customer-lifecycle webhooks |
Outbound (Acelle → your app) |
SaaS billing events — when a customer signs up, changes plan, cancels |
Admin → Webhooks |
| Per-campaign webhooks |
Outbound (Acelle → your app) |
When a specific campaign sends / errors |
Campaign → Edit → Webhooks tab |
| Sending-provider notifications |
Inbound (sending provider → Acelle) |
Bounce + complaint events from SES / Mailgun / SendGrid |
/api/v1/notification/bounce + /feedback (auto-configured per sending server) |
Pick the right one for your use case before reading further.
System 1 — Customer-lifecycle webhooks (the SaaS billing system)#
This is the system most people mean when they ask about Acelle webhooks. Used by SaaS operators to keep their CRM, analytics, or billing system in sync with Acelle's customer + subscription state.
Configure at Admin → Webhooks → New Webhook. Each webhook subscribes to one event and posts to a URL of your choice.
Available events#
Per config/webhook_events.php — this is the authoritative list:
| Event |
When it fires |
Params posted |
new_subscription |
A customer starts a paid subscription on a plan |
customer_id, plan_id |
cancel_subscription |
A customer cancels (subscription remains active until period end) |
customer_id, plan_id |
terminate_subscription |
A subscription ends (after cancel grace period, or hard end) |
customer_id, plan_id |
change_plan |
A customer moves between plans |
customer_id, old_plan_id, new_plan_id |
new_customer |
A customer account is created |
customer_id |
automation_webhook |
A "webhook" action fires inside an Acelle automation flow |
automation_id |
subscriber.created, campaign.opened, campaign.clicked etc. are NOT events in this system. Older docs sometimes list them; they don't exist. For subscriber + campaign events, use system 2 (per-campaign webhooks) or poll the API.
Webhook delivery shape#
POST https://your-app.example.com/webhooks/acelle
Content-Type: application/x-www-form-urlencoded
customer_id=123&plan_id=45
Plus any auth header you configured (see below).
Webhook configuration#
The webhook form lets you set:
- Name — your label
- Event — one of the six above
- Request method — POST (default), GET, PUT, PATCH, DELETE
- Request URL — your endpoint
- Authentication — None, Bearer Token, Basic Auth, or Custom Header
- Headers — extra headers (e.g.
X-Source: acellemail)
- Body type —
form-urlencoded (default), JSON, raw text
- Body params — the event payload params (auto-populated) or your custom mapping
- Retry on failure — number of retries + delay between attempts
- Status — Active / Inactive
A webhook only fires when its status = active. Disable to pause without deleting.
Triggering an automation_webhook from a flow#
Inside an automation flow, you can add a "Webhook" action. When a subscriber reaches that step, the configured automation_webhook fires with the automation_id (and optionally subscriber merge tags in the body).
This is the bridge from automation flows to external systems — fire a webhook to your CRM when a lead reaches "qualified", or to Slack when a high-value subscriber takes a key action.
System 2 — Per-campaign webhooks#
For events about a specific campaign (sent, opened, clicked, bounced, complained, unsubscribed), use the per-campaign webhook system.
Configure at Campaign → Edit → Webhooks tab (or via the campaign-creation wizard). Each campaign can have multiple webhooks; each subscribes to one event.
Available campaign events#
The per-campaign webhook table (campaign_webhooks) supports:
campaign.sent — campaign send completed
campaign.open — a subscriber opened the email
campaign.click — a subscriber clicked a tracked link
campaign.bounce — a delivery failure (hard / soft)
campaign.complaint — a spam complaint
campaign.unsubscribe — a recipient unsubscribed via the campaign
Payload includes the subscriber email, campaign uid, and event-specific fields (clicked URL, bounce reason, etc.).
When to use this vs polling the API#
| Pattern |
Use when |
| Per-campaign webhook |
You need real-time per-event reaction (push to Slack on bounce; sync to CRM on click) |
Poll /api/v1/campaigns/{uid}/...-log/download |
You need a batch import of all events for a campaign; or your app can't accept inbound HTTP |
For most external-system integrations, webhooks win — they're real-time and lower-cost than polling.
System 3 — Sending-provider inbound notifications#
The third system is for inbound webhooks — your sending provider (Amazon SES via SNS, Mailgun event hooks, SendGrid event webhook) posts bounce + complaint events to AcelleMail.
You usually don't configure these directly — Acelle auto-configures them when you set up the sending server (e.g. SES SNS topic + subscription is created during the SES sending-server wizard).
Routes:
POST /api/v1/notification/bounce — receives bounce events
POST /api/v1/notification/feedback — receives spam-complaint events
If you're building a custom sending provider integration, you'd POST events to these endpoints in the documented format. Otherwise, ignore — Acelle handles it transparently.
Signature verification (where applicable)#
For the System 1 customer-lifecycle webhooks, signature verification depends on what auth method you configured:
- Bearer Token — Acelle sends
Authorization: Bearer <your-token>. Your endpoint checks the header matches the token you set. Simple, recommended.
- Basic Auth — Acelle sends
Authorization: Basic <base64(user:pass)>. Your endpoint validates as usual.
- Custom Header — Acelle sends a custom header with a static value (e.g.
X-Webhook-Secret: my-shared-secret). Verify equality with constant-time comparison.
Acelle's webhook system does NOT use HMAC signature on the body by default (unlike Stripe / GitHub). The auth header is the entire security mechanism. Make sure your endpoint is HTTPS-only so the auth header isn't exposed in transit.
If you need HMAC body signing, implement it via the Custom Header option — configure a header that contains the HMAC of the body computed in a Laravel listener you add via extending source.
Retry behaviour#
The webhook config lets you set:
setting_retry_times — how many times to retry on non-2xx response
setting_retry_after_seconds — wait time between retries
Recommended defaults: 3 retries with 60-second backoff. Past 3 retries, the webhook is marked failed and won't fire again until manually re-enabled.
Your endpoint MUST:
- Return
2xx within 30 seconds to count as success
- Be idempotent — Acelle may retry on transient failures; your endpoint should handle duplicate deliveries gracefully (key on
customer_id + event + timestamp)
- Acknowledge quickly + process async — if your processing takes > 5s, accept the webhook, enqueue a job, return 200 immediately. Slow webhook handlers cause retry storms.
Verifying webhooks work#
# 1. Set up a temporary catch-all endpoint at https://webhook.site/
# (or use ngrok to forward to localhost)
# 2. Configure a webhook in Admin → Webhooks → New
# Event: new_customer
# URL: <your webhook.site URL>
# 3. Trigger the event — create a new test customer in the admin
# 4. Check webhook.site — you should see the POST with customer_id in the body
If nothing arrives:
- Check Admin → Webhooks → Logs for the delivery attempt and the response code Acelle saw
- Verify your endpoint returned
2xx
- Verify webhook status is "Active"
- Check the queue is processing (webhooks are dispatched via the same queue as the rest of Acelle — see Setting Up Queue Workers)
Common issues#
| What you see |
Likely cause |
Fix |
| No webhook fires when expected |
Webhook status = Inactive, or event mismatch |
Verify status is Active; verify the event matches what you actually triggered |
| Endpoint receives one webhook many times |
Endpoint returned non-2xx; Acelle retried |
Make endpoint return 2xx faster; or fix the underlying issue causing the error response |
| Webhook payload missing custom fields |
Customer-lifecycle webhooks only carry customer_id / plan_id. Subscriber data isn't included |
Use customer_id to fetch the customer via API for additional data |
| Webhook arrives without auth header |
Auth method = None |
Reconfigure with Bearer / Basic / Custom Header |
| Endpoint says "signature invalid" |
You enabled HMAC verification but Acelle doesn't ship HMAC |
Use header-based auth instead, OR implement HMAC via a custom listener (extending source) |
| Webhooks back up after a sending storm |
Queue worker pool saturated |
Increase worker numprocs (see scaling guide) |
| Per-campaign webhook fires before "sent" event for transactional sends |
Race condition between send + tracking event |
Process events idempotently; don't assume strict ordering |
| Need a subscriber.created event |
Doesn't exist in customer-lifecycle webhooks |
Use per-campaign webhook for campaign.sent, OR poll the subscribers API for new records |
FAQ#
Why no subscriber.created event? Acelle's customer-lifecycle webhook system was designed for SaaS billing — it covers events the operator cares about (new customer, plan change, cancellation), not events the customer would care about (new subscriber on their list). For subscriber events, use the per-campaign webhook system (system 2) or poll the API.
Can I add custom events? Yes — fire a custom event from a Laravel listener (added via extending source), then add it to config/webhook_events.php. It'll appear in the webhook config UI.
Do webhooks count against API rate limits? No — webhooks are outbound from Acelle, not API calls from your code.
Can the webhook payload be JSON instead of form-urlencoded? Yes — set "Body type" to JSON in the webhook config. The same param keys are posted, just JSON-encoded.
How do I retry a manually-failed webhook? Admin → Webhooks → Logs → click the failed delivery → "Retry". Or trigger the original event again.
Where can I see what URL Acelle is calling? Admin → Webhooks → click the webhook → see configured URL + recent delivery log.
Can I forward webhook payloads to multiple endpoints? Configure multiple webhooks for the same event, each with a different URL. Each fires independently.
Related articles#