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.clickedetc. 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 completedcampaign.open— a subscriber opened the emailcampaign.click— a subscriber clicked a tracked linkcampaign.bounce— a delivery failure (hard / soft)campaign.complaint— a spam complaintcampaign.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 eventsPOST /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 responsesetting_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
2xxwithin 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
- Getting Started with the AcelleMail REST API — for polling the API as an alternative
- REST API Authentication and Endpoints — full endpoint reference
- Extending AcelleMail Source Code — adding custom events / listeners
- Setting Up AcelleMail as a SaaS Platform — webhooks pair with SaaS use case
- Advanced Automation Triggers and Conditions — fire webhooks from automation flows
- Setting Up Queue Workers and Cron Jobs — webhook delivery uses the queue
- Using API Triggers for Custom Automation Workflows — the inverse (your app calls Acelle)
- Configuring Amazon SES with AcelleMail — SNS notification setup for inbound (system 3)
5 bình luận