Public Actions Hook
Use this hook when a person clicks a link in an email and your code should run. There is no sign-in and no API token. Polls, ratings and “yes, I am coming” links all work this way.
This is a sensitive permission. The admin sees Links anyone can click in emails.
How it works
- You declare an action in the manifest.
- You put a merge tag in an email. Broadcast makes a signed link for each subscriber.
- A person clicks the link.
- A small page opens and submits itself.
- Your function runs and returns a title and a message.
- The page shows them to the person.
The page in step 4 is there for a reason. Many mail systems open every link in an email to scan it. A scanner only fetches the address, and fetching the address runs nothing. Your code runs only after the page submits. A person without JavaScript sees a Continue button.
Some scanners run scripts too, so they can submit the page. They click every link in a few seconds. If a click must come from a person, look at the timing, as Polls does: it drops answers that come within 10 seconds of each other.
Full example: quick rating
manifest.json:
{ "id": "local.quick-rating", "name": "Quick rating", "version": "1.0.0", "requires": { "broadcast": ">=3.0.0 <4.0.0", "pluginApi": "1" }, "hooks": { "publicActions": [{ "name": "rate" }] } }
dist/main.js:
const ALLOWED = ["good", "bad"]; export default definePlugin({ publicActions: { async rate({ value, subscriber_id }, ctx) { // The value is not signed. Anyone can change it in the address bar. if (!ALLOWED.includes(value)) { return { title: "Sorry", message: "We do not know that answer." }; } // This overwrites the old answer, so a second click does no harm. await ctx.store.forSubscriber(subscriber_id).set("rating", value); return { title: "Thanks", message: "Your answer is saved." }; }, }, });
In the email, write the links like this:
<a href="{{ plugins.local_quick_rating.actions.rate }}?value=good">Good</a> <a href="{{ plugins.local_quick_rating.actions.rate }}?value=bad">Bad</a>
The namespace is your plugin ID, with . and - written as _. Then comes actions, then the action name.
What you get
{ "name": "rate", "value": "good", "subscriber_id": 12, "subscribed": true }
What is proven and what is not
| Field | Can you trust it? |
|---|---|
subscriber_id |
Yes. The signature proves the subscriber, the channel, the plugin and the action |
subscribed |
Yes. It is false when the person unsubscribed or is inactive. Links never expire, so decide what such a person may still do |
value |
No. It is not signed. Anyone with the link can type another value in the address bar |
So always check value against a list of allowed answers. It is absent if the link has none, and Broadcast refuses a value over 200 characters.
The link does not expire, because a person can read an email at any time. It stops working if the plugin is turned off, or if the subscriber is deleted.
A person can forward an email, and the link still counts as the first person. Do not use this hook for anything that a stranger must not do, such as a change to an account.
What you return
Return { title, message }. Both are plain text, and HTML is never rendered. A title can be 200 characters, and a message can be 1,000 characters. Without a title, the page shows Thank you.
Limits
- The call has 5 seconds.
- If your code fails or takes too long, the person sees Something went wrong and We could not save this. Please try the link again later. Your error message is never shown.
- Broadcast allows 5 clicks a minute for each link, and 60 a minute from one IP address.
Where links work
Action links work in broadcasts and in sequence emails. They do not work in transactional emails or test sends, because plugin hooks do not run there. In those emails, the tag is empty.
To try a link, send a broadcast to a small segment with your own address in it.
Common mistakes
You trust value. See above. The example checks it against ALLOWED.
You do not plan for a second click. People click twice, and a page reload can repeat the request. Write the handler so that a repeat does no harm. Overwrite the old answer, or check for an earlier answer as Build a Poll does.
You put HTML in the message. It shows as plain characters.
You do slow work. You have 5 seconds, and a person is waiting. Save the answer and return. Do the slow work in a schedule.
You build the link yourself. Only the merge tag has a valid signature.