# How to trigger emails from product events (with a curl example)

> Event-triggered emails start when your backend reports what happened, like a failed payment. How to send the event, in curl and JavaScript, and what to write.

Source: https://behavly.io/guides/trigger-emails-from-product-events · Updated October 31, 2026

An event-triggered email is an email sent to one person because your product reported that something happened to them: a card was declined, a trial is about to end, an import finished. To trigger emails from product events, your backend sends the event to your email tool the moment it happens, with the person’s email address and a few details, and a campaign that starts on that event takes the person in. No calendar decides who gets the email or when; the event does.

Events are the right mechanism for moments that happen on your server rather than in a browser. They are one way to do [behavioral email](https://behavly.io/guides/behavioral-email), the practice of writing to people based on what they do. This guide covers which events deserve an email, the request that sends one, with a curl example and a JavaScript one, and what the email should say.

#### Event-triggered vs transactional email: what’s the difference?

Both start with something your code knows. The difference is who asked for the email, and what has to happen after it.

|  | Transactional email | Event-triggered email |
| --- | --- | --- |
| Sent because | The person asked for something | You decided the moment deserves a follow-up |
| Examples | Password reset, receipt, sign-in link, invoice | Payment failed, trial ending, import finished with errors, seat limit reached |
| How many emails | One, every time | Often a short path: an email, a wait, a check, maybe another email |
| When it stops | Once it is sent | When the person does the thing, replies or unsubscribes |
| Unsubscribe link | Usually not: the person needs it | Yes: people can opt out of follow-ups |
| Where it belongs | A transactional provider, called from your code | A lifecycle tool that can wait, branch and stop |

Keep receipts, password resets and sign-in links with a transactional provider. They have to arrive within seconds, every time, and they never need a second email. Event-triggered emails do a different job. What makes them work is what comes after the first one: wait a few days, check whether the person fixed the problem, and stop the moment they do. That logic belongs in a campaign, not in a webhook handler.

#### Which product events are worth an email?

The good ones have three things in common: they happen on your server, they leave the person with something to do or something worth knowing, and you can tell when that something is done. The last part matters most, because the event that says it’s done is what ends the path.

| Event your backend sends | What the email does | What ends the path |
| --- | --- | --- |
| `payment_failed` | Says which charge failed and links straight to updating the card | `payment_succeeded` |
| `trial_ending` | Tells each person what they built during the trial and what happens next | `upgraded` |
| `import_finished` | Reports what came in, what was skipped and how to fix the skipped rows | A visit to the imported records |
| `seat_limit_reached` | Explains the plan with more seats to the person who tried to invite someone | `upgraded` |
| `integration_disconnected` | Names the integration, what stopped syncing and how to reconnect | `integration_connected` |
| `export_ready` | Sends the link to a report that took minutes to build | Nothing: one email is the whole job |

Some moments don’t need your backend at all. Page views and clicks inside your app are already visible to a tracking snippet, and for those you can [trigger on a page visit instead](https://behavly.io/guides/page-visit-follow-up-email). For more ideas, organized by stage, see [20 behavioral email examples](https://behavly.io/guides/behavioral-email-examples).

#### How do you send a product event? A curl example

With Behavly, you name the event once, then your server sends one HTTP request each time it happens. The request carries the event’s key, the person’s email address and any details the email should use. Here is the request for a failed payment, exactly as the Behavly Help Center documents it:

```bash
curl -X POST 'https://behavly.io/api/v1/send_event' \
  -H "Authorization: Bearer $BEHAVLY_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"event":"payment_failed","email":"ana@example.com","properties":{"plan":"Pro","amount":49}}'
```

- **event**: The event’s key, such as `payment_failed`. Required.
- **email**: The person it is about. Required: an event without it is refused.
- **properties**: The details, as JSON: at most 50 keys and 8 KB. If you described the event’s fields, the required ones must be there, with the right types.
- **occurred_at**: When it happened. Optional; it defaults to now and can be up to 30 days ago.
- **idempotency_key**: Your own id for the event. Send it when you retry: the same key twice is one event, counted once.

The same request from a Node server, in the form Behavly shows when you create a trigger, with an idempotency key added:

```javascript
// Node 18+ or any server runtime with fetch.
const res = await fetch("https://behavly.io/api/v1/send_event", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BEHAVLY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    event: "payment_failed",
    email: "ana@example.com",
    properties: { plan: "Pro", amount: 49 },
    idempotency_key: "inv_2041-attempt-1",
  }),
});
const { ok, data, error } = await res.json();
```

The answer names each campaign the event started, and why any did not: the person was already on the path, already went through it, unsubscribed or bounced, or the campaign is paused. To send many events at once, `send_events` takes up to 100 in one request.

##### Keep the key on your server

Create the API key with **Send events only** access. That key can send events and nothing else, so if it leaks it cannot read your contacts or change anything. Behavly refuses an event sent from a browser with a key, because anyone can copy a key out of a web page. In your web app, the tracking snippet’s `behavly.track("payment_failed", { plan: "Pro" })` starts the same campaigns for a visitor your site has identified.

##### Retries and duplicate events

Networks fail, and many webhook senders deliver the same event twice. Give each event an `idempotency_key` that stays the same across retries, such as your own invoice or job id, and Behavly counts it once. Separately, the same event for the same person within 10 minutes counts once, so a burst of identical events can’t start a path twice.

#### What should an event-triggered email say?

- **Name the event in the reader’s words.** “Your import of orders-2025.csv finished” beats “Import job completed”. Use the details the event carried: the file name, the amount, the plan.
- **Lead with what it means for them.** What changed in their account, and whether anything is at risk.
- **Give one next step, linked directly.** The page where they fix it, not your homepage or a help center search.
- **Say what happens if they do nothing.** A retry date, a sync that stays paused, a trial that ends on Friday. Plain facts, no threats.
- **Stop when the problem is solved.** A reminder the day after someone fixed it tells them nobody is paying attention.

This example is for Pinegrove, a made-up sales analytics tool for online shops. The event is `import_finished`, carrying the file name and the number of rows imported and skipped.

Import finished, 18 rows skipped

> Subject: Your order history is in Pinegrove (18 rows to check)
>
> Hi Lena,
> 
> Your import of orders-2025.csv finished: 1,240 orders are in Pinegrove, and your revenue charts now go back to January 2025.
> 
> 18 rows were skipped because their dates were written as text. They’re listed on one page, with the date format Pinegrove expects, and you can re-upload just those rows.
> 
> Fix the 18 skipped rows: [link]
> 
> Tom

#### How to trigger emails from product events in Behavly

[Behavly Triggers](https://behavly.io/features/triggers) turn each event your server sends into the start of a campaign path, and every email on that path can be written for the person from what they did in your product.

1. **Make the trigger.** In **Triggers**, click **New trigger** and give it a **Name**, like Import finished. The **Key** follows it (`import_finished`); it is what your code sends, and it can’t change once the trigger exists. Under **Payload**, list the details each event carries, with a type and whether each is required, or paste an example under **Example JSON** and choose **Apply to fields**.
2. **Copy the request.** **Create trigger** shows the request that sends it, in cURL, JavaScript, Python, Java, C#, Go, PHP or Ruby. Create a key with **Send events only** access in **Settings → API** and keep it in your server’s environment.
3. **Try it without code.** **Send a test event**, on the trigger’s page, sends the event for an address you enter, exactly as your server would. Use your own address: an active campaign that starts on it takes that person in, and its emails are real. Test events don’t count against your plan.
4. **Start the campaign.** **Create an Agent**, on the trigger’s page, opens a new Trigger Campaign that already starts on it. Under **Describe the path**, say what each email should do, choose **Build the path**, and Behavly lays out the emails, waits and branches for you to check.
5. **Use the event’s details.** In a template, `{{event.file_name}}` puts a payload field into the subject or the email, and `{{event.file_name|your file}}` says what it reads when the field is empty. A **Behavly Agent** step instead writes each email for the person from their activity, in your sender’s voice.
6. **Decide what a repeat means.** **If it happens again** is **Once per person** by default. Choose **A new case each time** for problems that can come back, like a failed payment, or **Again after a break** for interest that returns, like viewing pricing.
7. **End it when it’s done.** Set the **Goal** to the event that means the job is finished and turn on **They reach the goal** under **Leave when**. People already on a path who wait for, branch on or aim for an event move on the moment it arrives.
8. **Read before it sends.** **Preview** writes a step for a real person, **Send the path to me** delivers every email to your own inbox, and **Hold Behavly-written emails for approval** keeps written emails until you release them.

The usual rules apply to every path: the sending window, the frequency cap, review, unsubscribes and bounces. An event that arrives at night starts the path, and the first email waits for the window, which is Monday to Friday, 9:00 AM to 6:00 PM in each person’s timezone by default.

> Note: Trigger Campaigns work on every plan, Free included, with no plan limit on how many are active. Events from your server count toward the plan’s monthly product events, from 1,000 on Free to 1,000,000 on Scale; events from the website snippet don’t.

_**Triggers** lists every event your product sends and the campaign each one starts._

#### How Behavly does this for its own users

Behavly’s own lifecycle email runs on Behavly Triggers. On October 31, 2026, five Trigger Campaigns started from events Behavly’s product sends, all switched on October 20, 2026 and signed by the Behavly team. Each one’s repeat setting follows what its event means:

| Event | The path | If it happens again | Goal (reaching it ends the path) |
| --- | --- | --- | --- |
| `signed_up` | **Onboarding · New workspaces**: five emails over about 12 days, each setup email skipped once that step is done | **Once per person** | `campaign_activated` |
| `payment_failed` | **Billing · Payment failed**: three emails sent as designed, 3 and then 4 days apart, the second and third as replies in the first one’s thread | **A new case each time**, at **Urgent** priority | `payment_recovered` |
| `plan_limit_reached` | **Expansion · Plan limit reached**: an email sent as designed 1 hour after the event, then 3 days later an email edited by Behavly about which plan fits | **Again after a break**, 30 days | `plan_upgraded` |
| `subscription_cancelled` | **Retention · Cancellation & win-back**: one plain question 2 hours after cancelling, then 30 days later an overview of what Behavly does now | **Again after a break**, 90 days | `plan_upgraded` |
| `plan_upgraded` | **Customers · Welcome to paid**: a welcome to the plan, a week later one thing to set up next, two weeks after that a request for a review | **Again after a break**, 90 days | None |

Three choices in that table are worth copying. Only the failed payment is **A new case each time** and **Urgent**, because a card can fail again next month and the person has to act. The two paths that can end in an upgrade use `plan_upgraded` as their goal, so anyone who upgrades leaves them at once. And the first email after a cancellation uses the event’s own details: its opening line is written as `Your {{event.previous_plan|paid}} plan has ended`, so it names the plan the person had, or says “paid” when the event didn’t carry one.

The paths that don’t start from your server, such as people who have gone quiet for 14 days, run as Pattern Campaigns instead; the [win-back guide](https://behavly.io/guides/win-back-inactive-users) shows Behavly’s own.

If you are still choosing a tool for event-triggered email, our comparison of [Customer.io alternatives](https://behavly.io/compare/customer-io-alternatives) covers the options SaaS teams usually weigh.

## Questions

### What is an event-triggered email?

An email sent to one person because your product reported that something happened to them, such as a failed payment, an ending trial or a finished import. The event decides who gets it and when, instead of a send date.

### Are event-triggered emails the same as transactional emails?

No. Transactional emails deliver something the person asked for, like a receipt or a password reset, and belong with a transactional provider. Event-triggered emails are follow-ups you choose to send, usually a short path that stops when the person acts.

### Can I send product events from the browser?

Not with an API key: anyone can copy a key out of a web page, so Behavly refuses events sent that way. Send them from your server, or use the tracking snippet’s `behavly.track` in your web app.

### What happens if my server sends the same event twice?

Send an `idempotency_key` with each event and keep it the same across retries: the same key twice is one event, counted once. The same event for the same person within 10 minutes also counts once.

### What if the event arrives in the middle of the night?

The person enters the path right away, and the first email waits until the sending window opens, Monday to Friday, 9:00 AM to 6:00 PM in their timezone by default.

## Related

- [Event-triggered emails from your product’s API](https://behavly.io/features/triggers)
- [Behavioral email for SaaS: the practical guide](https://behavly.io/guides/behavioral-email)
- [Payment failed emails for SaaS: what to send, and how to trigger them](https://behavly.io/guides/payment-failed-emails)
- [Start A Campaign From A Trigger](https://behavly.io/help-center/campaigns/agent-triggers)
