Skip to main content
This is the reference for the Tappify event catalogue: twenty-one names, of which twelve are published and carry a payload. Names are domain.verb, past tense, snake_case. Adding a field to a payload is free; a renamed field is a new event name.
Events reach your UI through tap.data.subscribe, which the host serves by polling the install every 15 seconds and fanning the page out to every mount. A subscription starts from the moment your component subscribes, so it never receives a backlog. Your server declares the names it wants under server.events and answers them at POST /tappify/events. Delivery is filtered by what the owner granted: an install without store.metadata:read never sees a price.changed. The types stay the full catalogue either way, so a handler compiles whether or not the owner granted the scope it needs.
Both paths are live. An install that holds the scope sees the event through tap.data.subscribe; of those, the ones whose manifest lists the name under server.events also get it posted to POST /tappify/events.

The twelve published events

The seven install events fire from the install, consent, rotation and retirement paths. The five project events fire from the store paths that cause them: a release going live, a locale push, a tracked-keyword change, a screenshot push and a price update.

release.shipped

Needs projects:read. A release went live on a store.
Always present: projectId, platform, appId, version, shippedAt. build can be null. platform is ios or android on every project-scoped event.

metadata.changed

Needs store.metadata:read. Store listing metadata changed for a locale.
Always present: projectId, platform, appId, locale, fields, changedAt.

keyword.set_changed

Needs store.metadata:read. The tracked keyword set changed in a country.
Always present: projectId, platform, appId, country, changedAt. added, removed and total describe the change and can be absent.

screenshots.updated

Needs store.metadata:read. Screenshots changed for a locale.
Always present: projectId, platform, appId, locale, updatedAt. deviceClass and count can be absent.

price.changed

Needs store.metadata:read. A price changed in a territory.
Always present: projectId, platform, appId, territory, to, currency, changedAt. productId and from can be null, so a first price carries to with no from.

install.created

No scope. Your extension was installed.
Always present: installId, extensionId, organizationId, installScope, source, grantedScopes, createdAt. installScope is project or organization, and projectId is null on an organization install. source is one of directory, link, recipe, assistant, connect, dev.

install.paused and install.resumed

No scope. The owner paused or resumed the install.
Always present: installId, extensionId, organizationId, and pausedAt or resumedAt. Neither event carries installScope, source or grantedScopes — those are on install.created alone.

install.revoked

No scope. The install ended.
Always present: installId, extensionId, organizationId, revokedAt. reason is uninstalled when the owner uninstalls and retired when you retire the extension. The payload schema also allows suspended; nothing sends it today.

install.token_rotated

No scope. The install’s token generation and inbound secret were rotated; re-read the secret from the portal.
Always present: installId, extensionId, organizationId, generation, rotatedAt. The new inbound signing secret is never in the payload — it is shown once, to the owner, in the portal.

scopes.changed

No scope. The owner granted or removed scopes.
Always present: installId, extensionId, organizationId, grantedScopes, changedAt. grantedScopes is the whole set after the change, so a handler can replace rather than patch.

settings.changed

No scope. The owner saved your settings panel.
Always present: installId, extensionId, organizationId, keys, changedAt. keys names the fields that changed and carries none of their values — an owner’s settings document holds whatever they typed into your form, so the event says what moved and not what it now says. Read the values back with useTapStorage("settings").

Nine reserved names

These names are in the catalogue and carry no payload. A manifest that lists one under server.events validates, the registry marks it unpublished, and nothing is ever delivered until the feature that emits it ships.

Your own events

Events you declare under contributes.webhooks travel the other way: your server posts them to Tappify, and as decides whether one becomes an alert, a banner, or an event your UI can subscribe to. Their names follow the same domain.verb shape. A banner contribution names a webhook you declared with as: "banner", and a marker names one you declared with as: "event"; naming an event nothing declares fails the manifest schema.

Raise alerts, banners, and markers

Declaring one, signing the envelope, and what each as does to the delivery.

Receiving Tappify’s events on your server

Declare which of the events above your server should receive:
Tappify posts each one to POST /tappify/events:
request.event is the payload alone, and the route answers 204:
A subscription needs the same scope the event needs in the browser: release.shipped needs projects:read, the four store events need store.metadata:read, and the install and settings events need none. An install that does not hold the scope is skipped, and a name outside the published catalogue fails the manifest schema and the publish with it. Answer 2xx. A 5xx or an unreachable server is retried three times, waiting 30 seconds, then 2 minutes, then 10 minutes, and then dropped, for that install alone; a 4xx is taken as your answer and recorded on the install’s runtime timeline instead. Every attempt carries its own X-Tappify-Event-Id, so deduplicate on what the payload itself names — releaseId on release.shipped, changedAt on the store events — rather than on the header.

Subscribe to Tappify events

Handling these in a component and on your server.