Reference
Webhook events
Every event a webhook subscription can receive, and how deliveries are signed and retried.
A webhook subscription receives the events it asks for as JSON POSTs. Each event is a row of the org's audit log: type names what happened, and data carries the row (its id, the org and brand, who did it, the resource and the details in metadata). The management API reference shows the exact payload.
Sent to each subscription that wants it, with webhook-id, webhook-timestamp and webhook-signature (Standard Webhooks). Answer 2xx within 10 seconds; anything else is tried again, 6 times in all.
| Event | Resource | What happened |
|---|---|---|
org.created | org | Someone made an org, and its default brand: they own it. |
org.updated | org | An org's name, slug or settings changed. |
org.deleted | org | An org was deleted: purged 30 days later. |
org.plan_changed | org | An org moved to another plan (metadata.from, metadata.to). |
org.disabled | org | Synq's staff disabled the org, metadata.reason saying why: nothing serves it (no route but reading it, no key, no issuer, no webhook) until they enable it again. Nothing is deleted. |
org.enabled | org | Synq's staff enabled a disabled org again (metadata.reason): everything serves it again. |
brand.created | brand | |
brand.updated | brand | A brand's name, branding or sign-up switch changed. |
brand.deleted | brand | A brand was deleted: its issuer stopped at once; purged 30 days later. |
brand.reply_to_confirmed | brand | Someone who reads a brand's reply-to address confirmed replies to its users' email may come there, through the link Synq emailed it: metadata.reply_to. Setting it is brand.updated. |
brand.app_registrations_full | brand | The brand holds as many apps that registered themselves (MCP hosts, agents) as Synq keeps for one brand, metadata.limit, so its issuer refuses new registrations until unused ones are removed. At most one an hour per brand. |
app.created | app | |
app.updated | app | An app's name, URIs, links, settings, or whether it is disabled, changed. |
app.secret_rotated | app | An app got a new secret; the old one works until previous_secret_expires_at. |
app.deleted | app | |
app.api_access_changed | app | Which of an API's scopes an app may ask for changed (metadata.api). |
app.refresh_token_replayed | app | A refresh token the app had already rotated came back inside the app's reuse window and got its rotation's tokens again, instead of ending the grant: metadata.user, whose grant it is (the actor too), and metadata.elapsed_seconds since the rotation. Many of them may mean the app's servers race, or a token was stolen with the app's secret. |
api.created | api | |
api.updated | api | An API's name, scopes, token lifetime, third-party switch or own client changed. |
api.deleted | api | An API was deleted: requests for it fail with invalid_target from then on. |
org.ownership_transferred | member | An org's owner handed it to another member, who is now its owner. |
member.added | member | Someone joined the org (metadata.by: how, such as an invite). |
member.role_assigned | member | |
member.removed | member | |
member.left | member | A member left the org of their own accord. |
role.created | role | One of the org's own roles was made, changed or deleted. |
role.updated | role | |
role.deleted | role | |
invite.created | invite | An invitation was sent, revoked, or accepted by the person it was for. |
invite.revoked | invite | |
invite.accepted | invite | |
webhook.created | webhook | A webhook subscription was made, changed, had its secret rotated, or was deleted. |
webhook.updated | webhook | |
webhook.secret_rotated | webhook | |
webhook.deleted | webhook | |
webhook.disabled | webhook | Synq turned a webhook off after its deliveries kept failing. |
token_gate.created | token_gate | A token gate was made, changed (its name, rule, schedule, message, whether it is on, or where it applies) or deleted: what its rule is made of (how many conditions of each kind, how deep, on which chains, how many addresses its allowlists hold), never the addresses. |
token_gate.updated | token_gate | |
token_gate.deleted | token_gate | |
email_domain.created | email_domain | A sending domain was added for the org's brands' email: metadata.domain. Synq registers it with Postmark and shows the DNS records to publish. |
email_domain.verified | email_domain | A sending domain verified (metadata.domain): its records resolve and Postmark agrees, so its brands' email comes from it. Synq is the actor. |
email_domain.failed | email_domain | A verified sending domain stopped verifying (metadata.domain, metadata.problem): two checks in a row missed a record, and its brands' email comes via Synq until it is back. Synq is the actor. |
email_domain.deleted | email_domain | |
brand.custom_domain_added | custom_domain | A brand's custom domain was added: metadata.hostname. It waits for its CNAME (metadata.cname_target). |
brand.custom_domain_verified | custom_domain | Its CNAME pointed at its own target: the tenant controls the name (metadata.hostname). |
brand.custom_domain_activated | custom_domain | Its certificate is live and its issuer answers at https://<hostname> (metadata.issuer). |
brand.custom_domain_failed | custom_domain | It failed (metadata.problems: the codes, such as verification_expired or cloudflare_blocked): it isn't served, and the tenant can remove it and add it again. |
brand.custom_domain_suspended | custom_domain | Its org's plan lost branding.custom_domain: it is still served until metadata.grace_ends_at, then removed unless the plan gets it back. |
brand.custom_domain_restored | custom_domain | Its org's plan got branding.custom_domain back in the grace period: active again. |
brand.custom_domain_removed | custom_domain | It was removed and no longer answers (metadata.reason: requested, plan_lapsed, dns_failing, verification_expired, brand_deleted, org_deleted or abuse); apps that use its issuer stop working. |
api_key.created | api_key | An API key was made: its name, prefix, permissions and limits, never the key. |
api_key.revoked | api_key | |
change_request.created | change_request | A member asked from an MCP host for a change that waits for their approval in the dashboard: its kind, the app and what it sets; then approved (the change itself is its own row, app.updated) or declined, each naming the host's app as asked_from. |
change_request.approved | change_request | |
change_request.declined | change_request | |
file.uploaded | file | An upload was checked, re-encoded and kept: metadata.purpose (brand_logo, theme_background), what was kept (content_type, size, width, height, sha256). What it is for changes in its own row (brand.updated). |
file.rejected | file | An upload wasn't what its purpose takes, and nothing was kept: metadata.reason (type, size, dimensions, unreadable) and what the upload said it was (declared_type, declared_size). |
file.downloaded | file | A short-lived link to a private file was handed out (metadata.purpose, expires_in seconds). None of Synq's files is private yet. |
user.created | user | Someone signed up: the brand's first sign-in with a new identity. |
user.updated | user | Someone changed their own profile, through the account API. |
user.looked_up | user | Synq's staff looked users of the brand up, for support: metadata.users (their ids), metadata.query and metadata.reason. The actor is the staff member. |
user.email_verified | user | A user's email address was proved theirs (metadata.by: how). Signing in with an email code to an account whose address nobody had proved removes what its maker could sign in with (metadata.methods_removed) and ends its sessions. |
user.password_reset | user | A user chose a new password with a reset link from their email; every session and grant ended (metadata.sessions_ended). |
user.passkey_added | user | A user made a passkey: metadata.passkey its id, metadata.synced whether it can leave the device, metadata.by where (sign_up, sign_in when an app asked for one after a sign-in, account_api). |
user.passkey_removed | user | A user's passkey was removed (metadata.passkey, metadata.by). |
user.identity_linked | user | A way to sign in joined a user's account (metadata.provider; metadata.by: how): a provider whose word on the email the brand takes vouched for the address the user had verified (verified_email,), or the person added it, signed in again (link,). |
user.identity_unlinked | user | A way to sign in left a user's account (metadata.provider, metadata.identity; metadata.by: who),. |
user.consent_granted | user | A user allowed an app that isn't the brand's own something they hadn't before: metadata.client_id, and what this answer added (metadata.scopes, metadata.claims, metadata.apis: each API's identifier and scopes). Later sign-ins to it don't ask again for what they allowed. |
user.consent_revoked | user | What a user had allowed an app was withdrawn (metadata.client_id; metadata.by: account_api for the person, tenant): the app's grants and tokens for them ended, and their next sign-in to it asks again. |
user.api_key_created | user | A user made an API key for one of the brand's APIs: metadata.key_id its id, metadata.prefix, metadata.api the API's identifier, metadata.scopes and metadata.expires_at. Never the key. |
user.api_key_revoked | user | A user's API key was revoked (metadata.key_id, metadata.prefix, metadata.api; metadata.by: account_api for the person, tenant). Revoking an API's keys all at once, by no longer letting its users make them, is recorded once, as the API's api.updated. |
user.sessions_revoked | user | Sessions ended, and with them their grants (metadata.sessions: how many). |
user.deletion_scheduled | user | Someone asked for their account to be deleted, 30 days on. |
user.deletion_cancelled | user | |
user.deleted | user | A user was deleted for good, 30 days after it was asked for: only their id remains. |
user.sign_in_failed | user | A sign-in that didn't finish: the method refused what it was given, or the pipeline refused the person (metadata.reason). Never the secret. |
user.token_gate_checked | user | A sign-in to an app with token gates, or the app refreshing its tokens, checked what the user holds: metadata.at is sign_in or refresh, and metadata.result passed, refused (on a refresh, the app's grant ended) or unavailable (what decides it couldn't be read: a sign-in was refused, a refresh asked to try again), with each gate's id, its result (passed, refused, unavailable, not_in_effect outside its window, closed) and the paths of its conditions not met or not read, how many reads it cost and how many answers were kept, and the app as act_client_id. Never the wallets or what they hold. |
user.token_gate_dry_run | user | A member, or an org's key, tested a token gate or a draft rule against a user's wallets in a brand with POST /v1/orgs/{org}/token-gates/check: the gate (or that it was a draft) and its result. Never the wallets or what they hold. A test against wallet addresses alone reads only public chain data, and isn't recorded. |
webhook.test | webhook | A test delivery you asked for (POST /v1/orgs/{org}/webhooks/{webhook}/test), signed like any other. |