When you need an API-triggered automation#
The standard AcelleMail triggers (subscribed-to-list, email opened, tag added) cover most marketing-side events. The Custom Event trigger is for everything else:
- "Subscriber finished onboarding step 3 in our app" — fires from your backend
- "Customer renewed their subscription in Stripe" — fires from a Stripe webhook bridge
- "Order shipped from warehouse" — fires from your shipping integration
- "Support ticket closed" — fires from Zendesk / Intercom
The pattern is always: your-system → HTTPS POST to AcelleMail's events API → AcelleMail's automation triggered by that event name → email sent (or tag added, or webhook out, or whatever the flow does).
This article walks the AcelleMail-side setup. The developer-side code is in the collapsible.
UI walkthrough — get your AcelleMail side ready#
Step 1: generate an API token#
In AcelleMail's top-right user menu, click API & Webhooks → API tokens → New token.
Give it a descriptive name (crm-bridge-2026 or shopify-webhook-prod). Copy the token immediately — AcelleMail shows it once.
If you scope tokens per integration, you can revoke just that one when the bridge moves or breaks, without affecting other integrations.
Step 2: build the automation that listens#
Open the visual automation builder#
In AcelleMail's left sidebar, click Automation → Automations. The index lists every automation in this account with its current state:

Click New automation in the top-right toolbar. The trigger picker opens:

For the trigger picker, choose Custom event:
- Event name — pick a stable identifier you'll send from your system. Examples:
onboarding_step_3, subscription_renewed, support_ticket_closed. Lowercase + underscores, no spaces.
- Save the automation
The automation is now listening. Add downstream steps (send email, add tag, send webhook):

Step 3: test from the AcelleMail UI#
Inside the automation, click Test → Simulate event → fill in a test subscriber email + payload. AcelleMail runs the flow once with this test data:

The automation log records the test run; verify your downstream email or webhook actions worked correctly before you wire up the real source system.
Step 4: configure your external system to POST#
Now fire the event from outside AcelleMail. The HTTP call:
POST https://acellemail.com/api/v1/events
Headers:
Authorization: Bearer <your_api_token>
Content-Type: application/json
Body:
{
"subscriber_email": "user@example.com",
"event": "onboarding_step_3",
"data": {
"step_name": "added_first_team_member",
"duration_seconds": 45
}
}
The event value must match the event name configured in step 2 above. The subscriber_email must already exist on the list the automation is scoped to (or use upsert: true to create-if-not-exists).
The data object is freeform — payload-shaped JSON your automation steps can read via {{ event.data.step_name }} etc.
Step 5: monitor#
The Automations index shows total runs per automation:

Open the automation detail → run log → see every event received, with timestamp + subscriber + payload, plus the path each subscriber took through the flow:

Failed runs (e.g. subscriber not found, payload invalid) show in red — click to see the error message.
Common UI signals + fixes#
| Symptom |
Likely cause |
UI fix |
| API returns 401 Unauthorized |
Token typo'd or revoked |
Regenerate token from API & Webhooks page; rotate in source system |
| API returns 404 Not Found |
Wrong API path |
Should be https://your-acellemail-domain.com/api/v1/events (not /api/v2) |
| API returns 200 but automation doesn't run |
Event name in payload doesn't match automation's configured event name |
Open automation → confirm the exact event name string |
| Automation runs but the email step skipped |
Subscriber doesn't exist on the automation's scoped list |
Either pre-create the subscriber, or use upsert: true in the API call |
| Same event fires multiple times |
Bridge service retries on timeout, AcelleMail isn't idempotent on event-id |
Include a unique event_id in payload; configure automation step to skip if event_id already processed |
| Payload data missing in email body |
Merge tag uses wrong path |
{{ event.data.step_name }} not {{ step_name }} — payload is nested under event.data |
What event names work well#
Naming events stably is one of the small things that pays back forever:
- Use past tense —
purchase_completed not purchase (clear it already happened)
- One concept per event —
onboarding_step_3_completed is fine; onboarding_or_signup_or_invite is not
- Don't put data in the name —
purchased_yoga_mat is wrong; the product is in data.product, the event is purchase_completed
- Match your internal product vocabulary — if engineering calls it "session_started", call the event "session_started" too. Don't introduce a new vocabulary just for AcelleMail.
Common automation patterns#
| Use case |
Event from your system |
AcelleMail flow does |
| Onboarding nudge after 3 days idle |
onboarding_paused |
Sends "stuck on something?" email |
| Renewal confirmation |
subscription_renewed |
Tags as renewing-customer + thank-you email |
| Cart abandonment |
cart_abandoned |
Wait 1 hour → reminder email; wait 24 hours → discount email |
| Subscription cancel |
subscription_cancelled |
Add tag, suppress marketing sends, send win-back at 30/60/90 days |
| Customer success milestone |
nps_response_recorded |
If score >= 9, send referral CTA; if score <= 6, webhook to CRM ticket |
| Login activity |
user_logged_in |
Update last-active timestamp; trigger re-engagement if 30+ days gap |
Developer: full API reference, signing, retries, batch endpoints, SDK examples
The custom-event API and adjacent endpoints in detail.
Authentication:
Authorization: Bearer <token>
Tokens are issued per-account; rotate them when team members leave. Scope-per-integration is recommended — separate tokens for crm-bridge, shopify-webhook, support-tool-sync — so a leak from one source rotates without cascading.
Fire a custom event:
curl -X POST "https://acellemail.com/api/v1/events" \
-H "Authorization: Bearer $ACELLE_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Event-ID: $(uuidgen)" \
-d '{
"subscriber_email": "user@example.com",
"event": "onboarding_step_3",
"data": {
"step_name": "added_first_team_member",
"duration_seconds": 45,
"completed_at": "2026-05-19T14:30:00Z"
},
"upsert": true
}'
Response (200 OK):
{
"event_id": "evt_a1b2c3...",
"subscriber_uid": "sub_x9y8z7...",
"automation_runs_triggered": ["auto_onboarding_3_nudge"],
"processed_at": "2026-05-19T14:30:01Z"
}
The X-Event-ID header is for idempotency — if your bridge retries on timeout, repeated calls with the same event-id are deduplicated server-side.
Batch events (for high-volume backfill):
curl -X POST "https://acellemail.com/api/v1/events/batch" \
-H "Authorization: Bearer $ACELLE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"events": [
{"subscriber_email": "a@example.com", "event": "purchase", "data": {...}},
{"subscriber_email": "b@example.com", "event": "purchase", "data": {...}},
{"subscriber_email": "c@example.com", "event": "purchase", "data": {...}}
]
}'
Max 1000 events per batch. Useful for backfilling historical events when first integrating a CRM into AcelleMail.
Webhook signing (when AcelleMail webhooks out to your system):
If you configure AcelleMail to Send webhook as an automation step, AcelleMail signs every outbound request with an HMAC-SHA256 header:
X-AcelleMail-Signature: t=1684320000,v1=abc123def456...
Verify on your side:
import hmac, hashlib, time
def verify_webhook(body, signature_header, secret):
parts = dict(p.split('=') for p in signature_header.split(','))
timestamp = parts['t']
sig = parts['v1']
payload = f"{timestamp}.{body}".encode()
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
Reject any request older than 5 minutes (replay protection) and verify the signature matches — this prevents anyone-with-the-URL from forging requests.
Retries:
AcelleMail's outbound webhooks retry with exponential backoff on 5xx responses: 1m, 5m, 30m, 2h, 12h, 24h. After 6 failed attempts the webhook is marked failed-permanent and the automation step's run shows red.
For inbound events FROM your system, you control retries — if AcelleMail returns 5xx, retry with exponential backoff. Don't retry on 4xx (bad payload — retrying won't fix it).
SDK examples:
// Node.js
const fetch = require('node-fetch');
async function fireEvent(email, event, data) {
const res = await fetch('https://acellemail.com/api/v1/events', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ACELLE_TOKEN}`,
'Content-Type': 'application/json',
'X-Event-ID': crypto.randomUUID(),
},
body: JSON.stringify({
subscriber_email: email,
event,
data,
upsert: true,
}),
});
if (!res.ok) throw new Error(`AcelleMail event failed: ${res.status}`);
return res.json();
}
// Usage:
await fireEvent('user@example.com', 'onboarding_step_3', {
step_name: 'added_first_team_member',
});
# Python
import requests, uuid, os
def fire_event(email, event, data):
res = requests.post(
'https://acellemail.com/api/v1/events',
headers={
'Authorization': f"Bearer {os.environ['ACELLE_TOKEN']}",
'Content-Type': 'application/json',
'X-Event-ID': str(uuid.uuid4()),
},
json={
'subscriber_email': email,
'event': event,
'data': data,
'upsert': True,
},
timeout=10,
)
res.raise_for_status()
return res.json()
# Usage:
fire_event('user@example.com', 'onboarding_step_3', {
'step_name': 'added_first_team_member'
})
// PHP / Laravel
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
function fireAcelleEvent(string $email, string $event, array $data): array
{
$response = Http::withToken(config('services.acelle.token'))
->withHeaders(['X-Event-ID' => (string) Str::uuid()])
->timeout(10)
->post('https://acellemail.com/api/v1/events', [
'subscriber_email' => $email,
'event' => $event,
'data' => $data,
'upsert' => true,
]);
$response->throw();
return $response->json();
}
// Usage:
fireAcelleEvent('user@example.com', 'onboarding_step_3', [
'step_name' => 'added_first_team_member',
]);
Local development testing — use webhook.site or ngrok to expose your local dev server to AcelleMail's webhook deliveries while developing:
ngrok http 3000
# AcelleMail webhook URL: https://<ngrok-id>.ngrok.io/your-endpoint
Update the automation's webhook destination during dev; flip back to your production URL when shipping.
Rate limits:
The events API accepts 1000 requests/minute per token. Burst higher than that returns 429 with Retry-After header — back off and retry. For high-volume systems, use the batch endpoint instead.
Schema versioning:
If your event payload schema changes (e.g. you rename data.product to data.sku), bump the event name explicitly: purchase_completed → purchase_completed_v2. AcelleMail automations target one event name; switching is opt-in per-automation, so old data and new data don't crash each other.
Audit trail:
Every API call to /api/v1/events is logged in AcelleMail's audit log (admin → Audit log). Useful for forensics when a bug fires unexpected events — search by token, by event name, by subscriber email.
Related articles#