# Public Actions Hook

> Let a person click a link in an email and run plugin code, with no API token or sign-in. Signed links, mail scanners and safe handlers explained.

Source: https://sendbroadcast.net/docs/plugins-hook-public-actions

> Coming in Broadcast 3.0. Until then, this page applies only to the Upcoming (edge) build.

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

1. You declare an action in the manifest.
2. You put a merge tag in an email. Broadcast makes a signed link for each subscriber.
3. A person clicks the link.
4. A small page opens and submits itself.
5. Your function runs and returns a title and a message.
6. 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`:

```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`:

```javascript
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:

```html
<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

```json
{ "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](https://sendbroadcast.net/docs/plugins-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](https://sendbroadcast.net/docs/plugins-hook-schedules).

**You build the link yourself.** Only the merge tag has a valid signature.
