Build integrations · Partner
SAPP webhooks
Receive signed account-change events and verify the HMAC signature before reconciling with the read API.
SAPP webhooks notify subscribed applications when tenants, users, memberships, permissions, or metadata change. Each delivery is an HTTP POST with a signed JSON body.
POST /sapp/webhooks HTTP/1.1
Host: receiver.example.net
Content-Type: application/json
User-Agent: Skyfallen-SAPP/1.0
X-Skyfallen-SAPP-Protocol: SAPP
X-Skyfallen-SAPP-Delivery: 018fd4ca-07f9-75d3-a3f2-b1ff7ce2b8b2
X-Skyfallen-SAPP-Event: users.metadata.update
X-Skyfallen-SAPP-Timestamp: 1780675200
X-Skyfallen-SAPP-Signature: v1=0f45b87cfbc9cdb0f0f2fdf9922abdf872655e0f0acff5d63d7139d950e59f52
{
"id": "018fd4ca-07f9-75d3-a3f2-b1ff7ce2b8b2",
"protocol": "SAPP",
"protocol_version": "2026-06-05",
"event": "users.metadata.update",
"occurred_at": "2026-06-05T10:32:20.000000Z",
"tenant_ids": [
"018fd4c2-57de-7bc4-8e53-4e5758c0f624"
],
"resource": {
"type": "user",
"id": "018fd4c0-e2da-7c7d-8b8e-0f033193b383"
},
"context": {},
"changes": {
"metadata": {
"hris_id": {
"old": null,
"new": "HR-100044"
}
}
}
}
{
"accepted": true
}
Subscription model
Subscriptions are managed in ONE administration and stored as SAPP subscriptions.
| Field | Meaning |
|---|---|
app_name |
Receiving application name. |
publisher |
Publisher or owner of the receiving application. |
tenant_id |
Tenant-specific subscription. null means all matching events. |
webhook_url |
HTTPS endpoint that receives webhook posts. |
secret |
HMAC signing secret. |
subscription_fields |
Event scopes selected by the receiving app. |
enabled |
Disabled subscriptions do not receive webhooks. |
Event scopes
Tenant-scoped subscriptions receive only matching tenant events. User events are delivered to tenant-scoped subscriptions for every tenant the user belongs to.
| Scope | Emitted when |
|---|---|
tenants.create |
A tenant is created. |
tenants.update |
A tenant changes. |
tenants.metadata.update |
Tenant metadata changes. |
users.create |
A user is created. |
users.update |
A user changes. |
users.metadata.update |
User metadata changes. |
memberships.update |
A membership, membership role, or membership permission changes. |
Delivery headers
SAPP sends these headers on every webhook delivery:
| Header | Meaning |
|---|---|
User-Agent |
Skyfallen-SAPP/1.0. |
X-Skyfallen-SAPP-Protocol |
Always SAPP. |
X-Skyfallen-SAPP-Delivery |
Delivery ID, same as payload id. |
X-Skyfallen-SAPP-Event |
Event name. |
X-Skyfallen-SAPP-Timestamp |
Unix timestamp used in the signature base string. |
X-Skyfallen-SAPP-Signature |
Versioned HMAC signature. |
Signature verification
The signature format is:
Use the raw request body exactly as received. Do not parse and re-serialize JSON before verifying.
Reject timestamps outside your replay window. Five minutes is a practical default.
v1={hex_hmac_sha256(secret, timestamp + "." + raw_request_body)}
import crypto from "node:crypto";
function verifySapp(secret, timestamp, rawBody, signature) {
const now = Math.floor(Date.now() / 1000);
const ts = Number(timestamp);
if (!Number.isFinite(ts) || Math.abs(now - ts) > 300) {
return false;
}
const expected =
"v1=" +
crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
function verify_sapp(string $secret, int $timestamp, string $rawBody, string $signature): bool
{
if (abs(time() - $timestamp) > 300) {
return false;
}
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $signature);
}
Payload shape
All webhook payloads share the same base shape.
{
"id": "018fd4c8-e77f-74be-97ab-88f8fb10b9d7",
"protocol": "SAPP",
"protocol_version": "2026-06-05",
"event": "users.update",
"occurred_at": "2026-06-05T10:30:00.000000Z",
"tenant_ids": [
"018fd4c2-57de-7bc4-8e53-4e5758c0f624"
],
"resource": {
"type": "user",
"id": "018fd4c0-e2da-7c7d-8b8e-0f033193b383"
},
"context": {},
"changes": {
"status": {
"old": "email_not_verified",
"new": "active"
}
}
}
Delivery behavior
Webhook delivery is queued. The job sends each HTTP request with a 10 second timeout and retries transient outbound attempts inside the job.
Non-2xx responses are logged and treated as failed deliveries by the queue worker.
Consumers should treat webhooks as change notifications. If the full current object matters, call the SAPP read API after accepting and verifying the event.