Events Hook
Use this hook to react when something happens: a new subscriber, a bounce, a finished broadcast.
How it works
- Something happens in a channel.
- Broadcast checks which plugins are on in that channel and list that event.
- A background job calls your
onEventfunction.
The call happens after the fact. Your plugin cannot stop or change what happened.
Full example: Slack alert
This plugin posts to Slack when someone subscribes.
manifest.json:
{ "id": "local.signup-alert", "name": "Signup alert", "version": "1.0.0", "requires": { "broadcast": ">=3.0.0 <4.0.0", "pluginApi": "1" }, "permissions": { "network": [{ "fromSetting": "webhook_url" }] }, "hooks": { "events": ["subscriber.created"] }, "settings": { "webhook_url": { "type": "url", "label": "Slack webhook URL", "secret": true, "required": true } } }
dist/main.js:
export default definePlugin({ async onEvent(event, ctx) { // Broadcast can deliver one event more than once. Handle it only once. const doneKey = `done:${event.id}`; if (await ctx.store.get(doneKey)) return; const webhookUrl = await ctx.settings.secret("webhook_url"); const response = await fetch(webhookUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text: "A new person subscribed." }), }); // A throw makes Broadcast try this event again later. if (!response.ok) throw new Error(`Slack answered ${response.status}`); await ctx.store.set(doneKey, true); ctx.log.info("Posted one signup to Slack."); }, });
Pack and upload it as in Your First Plugin. Then paste your Slack webhook URL in the setting and turn the plugin on.
The manifest uses fromSetting: the plugin may reach the host of the URL the admin typed in the setting, and no other host.
What you get
The first value is the event. What data holds depends on your permissions. With a subscribers permission, you get all fields:
{ "id": "evt_005c298f-0ac5-49b9-923b-f6eda80e22e8", "type": "subscriber.created", "occurred_at": "2026-11-01T18:51:15.829Z", "data": { "subscriber_id": 12, "email": "ada@example.com", "first_name": "Ada", "last_name": null, "subscribed_at": "2026-11-01T18:51:15.736Z", "unsubscribed_at": null, "confirmed_at": null, "is_active": true, "custom_data": {}, "source": "opt_in_form", "change_source": "opt_in_form" } }
data then holds the same fields as the webhook for that event. The Webhooks page lists every event and its data. A plugin can listen to all of them, except test.webhook and message.attempt.exhausted.
data holds signup_ref only when the person came through a form address with ?ref=. See Build a Referral Program.
Without a subscribers permission
A plugin with no subscribers permission gets only a fixed list of fields that are not personal. Broadcast removes all other fields before the job is queued. For the same event, data is:
{ "subscriber_id": 12, "subscribed_at": "2026-11-01T18:51:15.736Z", "unsubscribed_at": null, "confirmed_at": null, "is_active": true }
| Events | Fields you get |
|---|---|
subscriber.* |
subscriber_id, is_active, subscribed_at, unsubscribed_at, confirmed_at, changed_at |
subscribers.* |
Counts and times of the purge |
email.* |
IDs, status, sent_at, and the flags and times of delivered, opened, clicked, bounced, complaint and unsubscribed |
broadcast.* |
IDs, name, subject, status, tags, counts and times. Never the list of recipients |
sequence.* |
IDs, sequence and step names, status, reason and times |
You never get an email address, a name, custom_data, signup_ref, source, the changes of subscriber.updated, a bounce reason or an error message. This list only grows by a decision in Broadcast. A new field stays out until it is added.
To read the person, add "subscribers": "read" to the manifest and call ctx.broadcast.subscribers.get(event.data.subscriber_id).
Warning
With a subscribers permission, event data holds personal data. Treat it with care. Do not log it, and do not send it to a host for a reason the admin does not expect.
Common mistakes
You handle an event twice. Delivery is “at least once”. A retry or a restart can bring the same event again, with the same event.id. If your work sends messages or adds up numbers, check event.id first, as the example does. If your work is safe to repeat, such as adding a tag, you do not need this.
You expect an email address with no permission. event.data.email is undefined unless the plugin has a subscribers permission.
You catch every error. If you hide an error, Broadcast does not try again. Let the error go up, as the example does with throw.
You listen to too much. email.opened can fire thousands of times for one broadcast. Each one is a call. Listen only to what you need.
You expect subscriber.created to mean “confirmed”. On a double opt-in form, this event comes before the person confirms, and confirmed_at is null. The confirmation arrives later as subscriber.updated.
You expect subscriber.created to run for an existing address. It runs only when a new subscriber record is made.
You expect an event right after you turn the plugin on. Broadcast can take up to 30 seconds to notice.