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

# Subscribe to Tappify events

> React in your UI when the owner edits your settings, changes your scopes or a release ships.

Tappify publishes events for the things that happen in an owner's project. Subscribe in your
component and the host delivers them, typed, with no polling.

```tsx theme={null}
import { useEffect } from "react";
import { useTap, useTapQuery, useTapFilters } from "@tappify/extension-sdk";

export function Summary() {
  const tap = useTap();
  const filters = useTapFilters();
  const downloads = useTapQuery({
    kind: "series",
    metric: "downloads",
    range: filters.range,
  });

  const refetch = downloads.refetch;

  useEffect(
    () =>
      tap.data.subscribe("settings.changed", (payload) => {
        tap.ui.toast(`Settings updated: ${payload.keys.join(", ")}.`, "neutral");
        refetch();
      }),
    [tap, refetch],
  );

  return null;
}
```

`subscribe` returns its own unsubscribe function, which is why the effect returns it
directly. `refetch` is stable across renders, so the effect subscribes once rather than on
every render. The event name is typed from the catalogue the SDK ships with, so a name that
does not exist is a compile error; once
[`tappify extension types`](/extensions/build/generated-types) has generated the payloads, a
field that is not on that payload is one too. `settings.changed` is one of the six events
Tappify sends today, so this handler runs the first time an owner saves your panel.

## Refetching without a subscription

When all you want is for a query to reload, name the events instead of writing a handler:

```tsx theme={null}
const summary = useTapServer("getSummary", input, {
  invalidateOn: ["settings.changed", "scopes.changed"],
});
```

## The events you can subscribe to

| Event                                | Fires when                                               | Needs                 | Sent today |
| ------------------------------------ | -------------------------------------------------------- | --------------------- | ---------- |
| `install.created`                    | Your extension is installed                              | —                     | yes        |
| `install.paused` / `install.resumed` | The owner pauses or resumes the install                  | —                     | yes        |
| `install.revoked`                    | The owner uninstalls, or you retire the extension        | —                     | yes        |
| `install.token_rotated`              | The owner rotates the install's token and inbound secret | —                     | yes        |
| `scopes.changed`                     | The owner grants or removes scopes                       | —                     | yes        |
| `settings.changed`                   | The owner saves your settings panel                      | —                     | yes        |
| `release.shipped`                    | A release goes live on a store                           | `projects:read`       | yes        |
| `metadata.changed`                   | Store listing metadata changes                           | `store.metadata:read` | yes        |
| `keyword.set_changed`                | The tracked keyword set changes                          | `store.metadata:read` | yes        |
| `screenshots.updated`                | Screenshots change for a locale                          | `store.metadata:read` | yes        |
| `price.changed`                      | A price changes in a territory                           | `store.metadata:read` | yes        |

Delivery is filtered by what the owner granted: `price.changed` reaches an install only when
`store.metadata:read` is granted. The type stays the full catalogue either way.
Every payload and every field is on [Events](/extensions/reference/events).

## The same events on your server

The handler routes `POST /tappify/events` for the names your manifest lists under
`server.events`, with the token already verified and `request.event` typed to the payload:

```ts theme={null}
createTappifyHandler({
  extensionId: "starter",
  events: {
    "install.created": (request) => {
      void provision(request.install.id, request.event?.grantedScopes ?? []);
    },
  },
});
```

<Note>
  Tappify posts every name your manifest lists under `server.events` to `POST /tappify/events` as
  it happens, one delivery per install. A 5xx or an unreachable server is retried three times over
  about twelve minutes, for that install alone; a 4xx is taken as your answer and recorded on the
  install's runtime timeline instead. A name your manifest does not list reaches your components
  only, through `tap.data.subscribe`.
</Note>

Adding a field to a payload is free. A renamed field is a new event name, so a handler never
silently reads the wrong thing.
