> ## Documentation Index
> Fetch the complete documentation index at: https://notifizz.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Subscriptions from Your Backend

> The subscription methods in the Node, Java and PHP SDKs — the two widget authentication modes, minting the secure-mode token, and notifying a group.

# Subscriptions from your backend

Your backend has two jobs in the subscription flow, and both are small. It **authorises** the widget — one hash per person, per resource — and it **notifies** a group when something happens to the thing they follow. Everything else happens in the browser, in your own interface.

This page is for developers. The concept it implements is described in [subscriptions](/docs/concepts/subscriptions); the browser half is [the subscribe widget](/docs/sdks/subscriptions/widget).

<Warning>
  **Closed beta.** Embedding the subscribe widget in your own app is enabled per organisation and is off by default. The methods below ship in the released SDKs, but the surface they drive is not open to every organisation yet — ask your Notifizz contact to switch it on.
</Warning>

## TL;DR

* Two widget authentication modes, chosen per environment: **`secure`** (the default) requires a server-computed hash for every subscriber-and-resource pair, **`public`** trusts the front API key alone.
* **`generateSubscribeToken(subscriberId, resourceId)`** mints that hash. It is a local HMAC computation — no network call, no rate limit, safe to compute per render.
* The hash **binds one person to one resource**. It is not a session, it does not expire, and it authorises nothing else.
* **`notifySubscribers(resourceId, properties)`** is a thin wrapper over `track()`: it emits the event `subscription-entered` with `resourceId` merged into the properties.
* Any event works. `notifySubscribers()` is a shortcut, not a requirement — your own event carrying the group id does the same job.
* In `public` mode the subscriber list is returned **without email addresses**. Only `secure` mode may see them.

## The two authentication modes

The widget talks to Notifizz from the browser, so it can only carry secrets that are safe to ship in a bundle. The mode decides what proof it must carry on top of the front API key. It is configured **per environment**, so your dev environment can be permissive while production is not.

|                                | `secure` (default)                               | `public`                                                |
| ------------------------------ | ------------------------------------------------ | ------------------------------------------------------- |
| What the browser sends         | Front API key + subscriber id + hash             | Front API key + subscriber id                           |
| What your backend must do      | Compute one hash per (subscriber, resource) pair | Nothing                                                 |
| Whose subscriber id is trusted | The one the hash proves                          | Whatever the browser claims                             |
| Email in the subscriber list   | Returned                                         | Never returned                                          |
| Use it for                     | Anything behind a login, anything private        | Public resources with no privacy or authorisation stake |

### Why `secure` is the default

The front API key is public by design — it is embedded in your frontend bundle and visible to anyone who opens the network tab. In `public` mode it is the only credential, so the browser is free to name any subscriber id it likes: subscribe somebody else to a resource, unsubscribe them from one, or read who else is subscribed.

In `secure` mode the request must also carry a hash your backend computed with a secret the browser never sees. Notifizz recomputes it and compares the two in constant time; a mismatch is refused before anything is read or written. And because the hash proves the subscriber id, the server **uses the proven id and ignores the one in the request body** — a tampered body changes nothing.

That last property is why `public` mode also refuses to return email addresses. If any browser can pose as any subscriber, the subscriber list must not be a way to harvest addresses.

## `generateSubscribeToken`

Mints the hash for one subscriber and one resource:

```
HMAC-SHA256( authSecretKey, subscriberId + resourceId )   → lowercase hex
```

Available in all three backend SDKs, with the same argument order and the same output.

<CodeGroup>
  ```javascript Node.js theme={null}
  import { NotifizzClient } from '@notifizz/nodejs';

  const notifizz = new NotifizzClient(
    process.env.NOTIFIZZ_AUTH_SECRET_KEY,
    process.env.NOTIFIZZ_SDK_SECRET_KEY,
  );

  const hash = notifizz.generateSubscribeToken('user_8f21', 'proj_4a19f');
  // → 'c4f0…' — pass this to the browser
  ```

  ```java Java theme={null}
  NotifizzClient notifizz = new NotifizzClient(authSecretKey, sdkSecretKey);

  String hash = notifizz.generateSubscribeToken("user_8f21", "proj_4a19f");
  ```

  ```php PHP theme={null}
  use Notifizz\NotifizzClient;

  $notifizz = new NotifizzClient($authSecretKey, $sdkSecretKey);

  $hash = $notifizz->generateSubscribeToken('user_8f21', 'proj_4a19f');
  ```
</CodeGroup>

It is a pure local computation: no HTTP call, no quota, no failure mode other than a wrong key. Compute it wherever you already know both ids — in the handler that renders the page, or in the endpoint that returns the resource.

### Getting the hash to the browser

Whatever you already do for authenticated data works. Two shapes cover almost everything:

* **Server-rendered page** — compute the hash alongside the resource and pass it into the template. The widget receives it as a prop or a `data-` attribute.
* **Single-page app** — return the hash on the object your frontend already fetches. A list of projects becomes a list of `{ id, name, subscribeHash }`.

```javascript Node.js — hash alongside the resource theme={null}
app.get('/api/projects', async (req, res) => {
  const projects = await db.projectsVisibleTo(req.user.id);
  res.json(
    projects.map((p) => ({
      id: p.id,
      name: p.name,
      subscribeHash: notifizz.generateSubscribeToken(req.user.id, p.id),
    })),
  );
});
```

Three properties are worth internalising before you design around it:

* **One hash per pair.** A hash minted for `proj_4a19f` is refused on `proj_77c02`. A page showing twenty cards needs twenty hashes.
* **It never expires.** It is a binding, not a session. Revoking someone's access to a resource in your product does not invalidate a hash you already handed them — which is fine, because the hash only ever authorises following that one resource and reading who else follows it.
* **`authSecretKey` never leaves your backend.** It is the same secret behind `generateHashedToken` for notification-center auth; see [API keys](/docs/environments/api-keys). Shipping it to the browser would make `secure` mode exactly as strong as `public` mode.

<Note>
  The subscriber id you hash must be **byte-identical** to the one the widget is initialised with. `User_8f21` and `user_8f21` produce different hashes, and the widget gets a 401 with no other clue. If subscribing silently does nothing, compare those two strings first.
</Note>

## `notifySubscribers`

Emits the event that a subscriber campaign listens to:

<CodeGroup>
  ```javascript Node.js theme={null}
  await notifizz.notifySubscribers('proj_4a19f', {
    title: 'Deployment finished',
    actor: 'Alice Bertrand',
    status: 'succeeded',
  });
  ```

  ```java Java theme={null}
  Map<String, Object> properties = new HashMap<>();
  properties.put("title", "Deployment finished");
  properties.put("actor", "Alice Bertrand");
  properties.put("status", "succeeded");

  notifizz.notifySubscribers("proj_4a19f", properties);
  ```

  ```php PHP theme={null}
  $notifizz->notifySubscribers('proj_4a19f', [
      'title'  => 'Deployment finished',
      'actor'  => 'Alice Bertrand',
      'status' => 'succeeded',
  ]);
  ```
</CodeGroup>

What it actually does is one line: it merges `resourceId` into the properties and calls `track('subscription-entered', …)`. Everything true of [`track()`](/docs/sdks/event-tracking/overview) is true here — the same idempotency handling, the same schema modes, the same failure behaviour.

| Language | Signature                                                                                                    |
| -------- | ------------------------------------------------------------------------------------------------------------ |
| Node     | `notifySubscribers(resourceId, properties?, options?)` — `options` accepts `idempotencyKey` and `occurredAt` |
| Java     | `notifySubscribers(String resourceId, Map<String, Object> properties)`                                       |
| PHP      | `notifySubscribers(string $resourceId, array $properties = [], ?string $idempotencyKey = null)`              |

Two consequences of it being a plain `track()`:

* **A campaign has to exist** on the `subscription-entered` event, with **Subscribers** as its recipient. Without one, the event is recorded and nothing is sent — the same as any event nobody listens to.
* **In `strict` schema mode you must declare the event first.** `strict` refuses to push an undeclared event, and `notifySubscribers()` does not exempt itself:

```javascript Node.js — required in strict mode theme={null}
notifizz.declareEvent('subscription-entered', {
  description: 'Something happened on a followed resource',
  schema: z.object({
    resourceId: z.string(),
    title: z.string(),
    actor: z.string(),
    status: z.string(),
  }),
});
```

In the default `soft` mode the track goes through and the SDK warns.

### You do not have to use it

`notifySubscribers()` exists so the common case is one call. It is not a privileged path: the campaign resolves the group from **whichever event property carries the group id**, so your own domain event does the same job and reads better in your codebase.

```javascript Node.js — your own event, same result theme={null}
await notifizz.track('project.deployment_finished', {
  projectId: 'proj_4a19f',   // ← the property carrying the group id
  actor: 'Alice Bertrand',
  status: 'succeeded',
});
```

Configure the campaign on `project.deployment_finished` with **Subscribers** as the recipient, and the orchestrator resolves `projectId` as the group. Prefer this when the event already exists for other reasons — one event, several campaigns, no duplicate.

The property may hold **a single id or an array of ids**. An event naming several groups notifies their union, with anyone in more than one of them counted once. Never put a list of *people* in the payload — see [why](/docs/concepts/subscriptions#never-a-list-inside-the-event).

### Replays and backfills

Pass `occurredAt` when the business event happened at a moment other than the one you are calling from — a replay, an offline batch, a history import. It dates the event itself:

```javascript Node.js theme={null}
await notifizz.notifySubscribers(
  'proj_4a19f',
  { title: 'Deployment finished', actor: 'Alice Bertrand', status: 'succeeded' },
  { occurredAt: '2026-08-19T09:14:00Z' },
);
```

<Warning>
  **A replay notifies today's members, not the ones from back then.** `occurredAt` records when the
  event happened; it does not rewind the group. Subscribers are resolved at the moment the campaign
  runs, so anyone who joined since will receive the replayed notification and anyone who left will
  not — however old the declared date is.

  Replaying a long history is therefore rarely what you want on a subscriber group. If the point is
  to reach the people who were members at that time, resolve that list yourself and target it
  explicitly.
</Warning>

A value in the future is refused at ingestion, beyond a small tolerance for clock drift. Omit it in the ordinary case.

## What the backend SDKs do not do

There is **no supported server-side path to create or remove a subscription**. Subscriptions come from the [subscribe widget](/docs/sdks/subscriptions/widget), where the person subscribing is the person consenting and the click is the record. A backend that could subscribe people on their behalf would turn that record into an assertion nobody made.

Reading who is subscribed is likewise a widget concern: every mounted instance already exposes the list for its resource, shared across the components watching it.

If your product genuinely needs an audience you assemble yourself rather than one people opt into, that is a different mechanism — resolve it with an [enricher](/docs/why/enrichers), or launch a [broadcast](/docs/concepts/broadcasts) against a CRM segment.

## Failure modes

| Symptom                                     | Cause                                                             | Fix                                                                                                                           |
| ------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Widget shows nothing, requests return `401` | Front API key does not resolve to an environment                  | Copy the **Front API Key** of the right environment                                                                           |
| `401` only in `secure` mode                 | Hash mismatch                                                     | Check that the subscriber id and resource id hashed server-side are byte-identical to the ones the widget uses, in that order |
| `401` mentioning missing headers            | `secure` mode with no hash passed to the widget                   | Pass the hash — the widget refuses to mount without one in `secure` mode                                                      |
| Subscriber list has no `email`              | Environment is in `public` mode                                   | Expected. Switch the environment to `secure` if the addresses are needed                                                      |
| Event recorded, nothing sent                | No campaign on the event, or its recipient is not **Subscribers** | Configure the campaign, then regenerate its orchestrator                                                                      |
| `track` rejected locally                    | `strict` schema mode, event not declared                          | Declare `subscription-entered` (or your own event name) with `declareEvent()`                                                 |
| `429` from the widget endpoints             | Per-IP rate limit on the browser-facing routes                    | Expected under load testing from one address; not a limit real users hit                                                      |

## FAQ

<AccordionGroup>
  <Accordion title="Can I switch an environment from public to secure later?">
    Yes, and existing subscriptions are unaffected — the mode governs how requests are authorised, not what is stored. What breaks is any page still mounting the widget without a hash: it starts refusing to mount. Ship the hash first, flip the mode second.
  </Accordion>

  <Accordion title="Should I cache the hash?">
    There is nothing to cache. It is one HMAC over two short strings, with no network call — recomputing it per render costs less than looking it up. Do not store it anywhere durable either: a hash you persist is a hash you have to invalidate, for no benefit.
  </Accordion>

  <Accordion title="Is the hash sensitive? It ends up in my HTML.">
    It is a capability, not a secret: whoever holds it can follow that one resource as that one subscriber, and read who else follows it. That is exactly what the person it was minted for is allowed to do. Mint it only for the people your own authorisation rules already allow on the resource, and never for a subscriber id other than the caller's own.
  </Accordion>

  <Accordion title="Do I need the webhook signing secret for any of this?">
    No. The third constructor argument is only needed to expose [enrichers](/docs/sdks/how-to/enrichers-tutorial). A client that only mints tokens and tracks events needs the auth secret and the SDK secret.
  </Accordion>

  <Accordion title="Can several events target the same group?">
    Yes, and that is the normal shape. A group is just an id; any number of campaigns, on any number of events, can resolve the same one. The subscription is a statement about the resource, not about a single kind of message.
  </Accordion>

  <Accordion title="What identifier should the subscriber id be?">
    The one your target channel needs. For the notification center it must match the identity your widget authenticates with, exactly. For email it should be resolvable to an address — either it is the address, or an enricher maps it. See [reaching a subscriber on a channel](/docs/concepts/subscriptions#reaching-a-subscriber-on-a-channel).
  </Accordion>
</AccordionGroup>

## See also

<CardGroup cols={2}>
  <Card title="Subscribe widget" icon="code" href="/docs/sdks/subscriptions/widget">
    The browser half — React, Vanilla, and the script tag.
  </Card>

  <Card title="Subscriptions" icon="user-plus" href="/docs/concepts/subscriptions">
    What a subscription is and how it becomes a notification.
  </Card>

  <Card title="API keys" icon="key" href="/docs/environments/api-keys">
    Which secret goes where, and which one signs this hash.
  </Card>

  <Card title="Event tracking" icon="satellite-dish" href="/docs/sdks/event-tracking/overview">
    `track()`, schema modes and idempotency — all of which apply here.
  </Card>
</CardGroup>
