# API Endpoints Hook

> Add your own API address to Broadcast, so that another system can call your plugin with an API token.

Source: https://sendbroadcast.net/docs/plugins-hook-api-endpoints

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

Use this hook to let another system call your plugin. For example, your shop can tell Broadcast about a new order.

This is a sensitive permission. The admin sees **Its own API**.

## How it works

The address of your endpoint is:

```text
https://your-broadcast-server/api/plugins/<plugin id>/<endpoint name>
```

Broadcast checks the API token first, and then calls your function. The token needs two things:

1. The permission that the endpoint names in the manifest.
2. The endpoint's name in the token's **Plugin endpoints** field. Write `local.order-tagger/orders`, one on each line, or `local.order-tagger/*` for all endpoints of that plugin.

So an existing token cannot call your endpoint until someone allows it by name.

## Full example: tag buyers

`manifest.json`:

```json
{
  "id": "local.order-tagger",
  "name": "Order tagger",
  "version": "1.0.0",
  "requires": { "broadcast": ">=3.0.0 <4.0.0", "pluginApi": "1" },
  "permissions": {
    "subscribers": "read",
    "tags": "write"
  },
  "hooks": {
    "apiEndpoints": [
      { "name": "orders", "method": "POST", "permission": "subscribers_write" }
    ]
  }
}
```

`dist/main.js`:

```javascript
export default definePlugin({
  apiEndpoints: {
    async orders(request, ctx) {
      const { email, product } = request.body ?? {};
      if (!email || !/^[a-z0-9-]{1,50}$/.test(product ?? "")) {
        return { status: 422, body: { error: "Send an email and a product code." } };
      }

      const subscriber = await ctx.broadcast.subscribers.find(email);
      if (!subscriber) return { status: 404, body: { error: "No such subscriber." } };

      await ctx.broadcast.tags.add({ subscriber_id: subscriber.id, tags: [`bought:${product}`] });
      return { status: 201, body: { tagged: true } };
    },
  },
});
```

Call it with a channel API token:

```bash
curl -X POST https://your-broadcast-server/api/plugins/local.order-tagger/orders \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email": "ada@example.com", "product": "pro"}'
```

The answer is `{"tagged":true}` with status 201. An admin API token must also send `broadcast_channel_id`. See [API Authentication](https://sendbroadcast.net/docs/api-authentication).

## What you get

```json
{
  "name": "orders",
  "method": "POST",
  "query": {},
  "body": { "email": "ada@example.com", "product": "pro" },
  "token_label": "My shop"
}
```

`body` is the parsed JSON, or the raw text if it is not JSON, or `null`. It can be up to 1 MB. `query` holds the query string.

## What you return

Return `{ status, body }` to choose the HTTP status. Return any other JSON value, and it is the body with status 200.

## Errors the caller can see

| Status | Answer | Reason |
|---|---|---|
| 404 | `{"error":"Not found"}` | The plugin is off in this channel, or there is no such endpoint |
| 405 | `{"error":"Method not allowed"}` | The endpoint uses another method |
| 403 | `{"error":"This token may not call this plugin endpoint."}` | The token lacks the permission, or the endpoint is not in its list |
| 413 | `{"error":"The request body is too large."}` | The body is over 1 MB |
| 502 | `{"error":{"code":"plugin_error","message":"..."}}` | Your code threw, or it took too long |

## A complete example

The official Polls plugin has an API with five endpoints, input checks and an idempotency key. See [Polls API](https://sendbroadcast.net/docs/plugins-polls-api).

Endpoints have fixed names, with no path parameters. Pass an ID in the query or the body.

## Common mistakes

**You trust the input.** A token only proves who called. Check each field, as the example does for `product`.

**You mix up the two permissions.** `permission` in the endpoint is what the *caller's token* needs. `permissions` at the top of the manifest is what the *plugin* needs. This example needs both.

**You expect `subscribers.find` to throw.** It returns `null` when there is no such person, so check for that.

**You forget that a caller can retry.** Networks fail, and a caller can send the same request twice. Make your work safe to repeat, or use an `idempotency_key` built from the caller's order number.
