Skip to Content
ReferenceWebhook event catalog

Webhook event catalog

Every event Synq can deliver, with the payload schema. Subscribe to any subset via the events array on POST /webhooks.

Every delivery has this envelope:

interface WebhookEnvelope { id: string // delivery id — matches Synq-Delivery header type: string // event name — matches Synq-Event header timestamp: string // ISO 8601 org: string // org slug actorUserId: string | null data: object // event-specific shape (below) }

consent.granted

{ "user": "user_abc", "app": "client_id_xyz", "scopes": ["openid", "profile", "email"] }

Fires when:

  • A user grants scopes to an App for the first time.
  • A user grants additional scopes to an existing grant (only the newly-granted scopes appear in scopes).

consent.revoked

{ "user": "user_abc", "app": "client_id_xyz", "scopes": ["openid", "profile", "email"] }

Fires when a user revokes a grant from their dashboard or an app revokes via DELETE /users/:userId/consent-grants/:appId.

Member events

member.added

{ "user": "user_abc", "roles": ["role_owner"] }

User joined the org (typically via invite acceptance).

member.removed

{ "user": "user_abc", "removedRoles": ["role_admin", "role_member"] }

User left or was removed.

member.role.changed

{ "user": "user_abc", "added": ["role_admin"], "removed": ["role_member"] }

Member’s role assignments changed. added and removed are disjoint.

OIDC client events

oidcClient.created

{ "clientId": "marketplace-web", "brand": "marketplace", "name": "Marketplace Web", "type": "web", "isConfidential": true, "redirectUris": ["https://marketplace.com/api/oauth/callback"], "allowedGrantTypes": ["authorization_code", "refresh_token"], "allowedScopes": ["openid", "profile", "email"] }

oidcClient.updated

{ "clientId": "marketplace-web", "before": { "name": "Marketplace Web", "redirectUris": ["…"] }, "after": { "name": "Marketplace Web Prod", "redirectUris": ["…", "…"] } }

before and after contain only the fields that actually changed.

oidcClient.deleted

{ "clientId": "marketplace-web", "brand": "marketplace" }

oidcClient.secret.rotated

{ "clientId": "marketplace-web" }

Does not contain the new secret. Rotate via API to receive it in the response.

Provider (BYO) events

socialProvider.upserted

{ "brand": "marketplace", "type": "google", "enabled": true }

A connection was created or its config was updated. The config itself is not in the payload (it’s encrypted at rest and never emitted).

socialProvider.enabled

{ "brand": "marketplace", "type": "google" }

socialProvider.disabled

{ "brand": "marketplace", "type": "google" }

Token gate events

tokenGate.created

{ "gateId": "gate_abc", "name": "Holder access", "mintAddress": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", "minHoldings": "1000000", "enabled": true }

tokenGate.updated

{ "gateId": "gate_abc", "before": { "minHoldings": "1000000" }, "after": { "minHoldings": "5000000" } }

tokenGate.deleted

{ "gateId": "gate_abc" }

System events

webhook.test

{ "test": true }

Fired by POST /webhooks/:id/test. The delivery still includes a real signature, so use it to validate your verification path end-to-end.

High-volume events (off by default)

Subscribe explicitly via the events array; don’t use * if you don’t want them.

token.issued

{ "user": "user_abc", "app": "marketplace-web", "scopes": ["openid", "profile"], "grant": "authorization_code" }

Fires on every successful token issuance.

token.refreshed

{ "user": "user_abc", "app": "marketplace-web", "scopes": ["openid", "profile"] }

Fires on every refresh-token exchange.

Never includes the access token or refresh token itself.

Subscribing to a subset

await fetch('https://synq.id/identity/organizations/acme/webhooks', { method: 'POST', headers: { Authorization: `Bearer ${tok}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://example.com/webhook', events: [ 'consent.granted', 'consent.revoked', 'member.added', 'member.removed', 'oidcClient.created', 'oidcClient.deleted', ], }), })

Receiving + dispatching

A typical receiver:

import { verifyWebhookSignature } from '@synqid/js' app.post('/synq/webhook', express.raw({ type: '*/*' }), async (req, res) => { const sig = req.header('Synq-Signature') if (!verifyWebhookSignature(req.body, sig, SECRET)) { return res.status(401).end() } const event = JSON.parse(req.body.toString()) switch (event.type) { case 'member.added': await provisionUser(event.data.user) break case 'consent.granted': await trackConsentMetrics(event.data) break case 'oidcClient.deleted': await invalidateAppCaches(event.data.clientId) break case 'webhook.test': break // … default: console.warn('unknown event type', event.type) } res.status(200).end() })

Mental model summary

  • Every event has the same envelope (id, type, timestamp, org, actorUserId, data).
  • data shape varies per event type but is stable.
  • High-volume events (token.*) are off by default; opt in explicitly.
  • For mutating events, before + after give you a diff.
  • Webhook signatures cover the entire payload; verify before parsing JSON.
Last updated on