domain.verb, past tense, snake_case. Adding a field
to a payload is free; a renamed field is a new event name.
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.
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.
projectId, platform, appId, locale, fields, changedAt.
keyword.set_changed
Needs store.metadata:read. The tracked keyword set changed in a country.
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.
projectId, platform, appId, locale, updatedAt. deviceClass and
count can be absent.
price.changed
Needs store.metadata:read. A price changed in a territory.
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.
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.
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.
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.
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.
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.
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 underserver.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 undercontributes.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:POST /tappify/events:
request.event is the payload alone, and the route answers 204:
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.