Native SDK Webhooks
Webhooks notify your backend when a Native SDK user's review or account status changes.
This is especially important when the SDK returns userStatus: "pending": the SDK submission succeeded, and the webhook is the source of truth for the later decision.
Create an endpoint#
- Open your Palm Verification SDK app in the Developer Portal.
- Under Settings → Webhooks, choose New.
- Enter a public HTTPS URL and select
review.status_changed,user.status_changed, or both. - Copy the signing secret immediately. It is shown only once.
- Open the webhook and choose Send test to queue a signed
webhook.testdelivery.
Organization owners and admins can create, test, re-enable, redeliver, and delete endpoints. Other app members can inspect endpoint health and delivery history.
Event payload#
{
"version": 1,
"event": "review.status_changed",
"event_id": "evt_opaque_id",
"external_user_id": "vu-1ed0a927-...",
"status": "approved",
"occurred_at": "2026-07-27T08:30:00Z",
"client_reference_id": "gate-withdrawal-42"
} event_idis stable across retries and manual redelivery. Store it as an idempotency key.external_user_idis the same app-scoped user ID returned by the SDK.client_reference_idis included onreview.status_changedwhen you suppliedclientReferenceIdfor the SDK operation that created that review.user.status_changedis account-level and does not include a transaction-specificclient_reference_id.statuscarries the new review or user status.
Verify the signature#
Read the request body as raw bytes before JSON parsing. The X-Webhook-Signature header has the form t=unix_seconds,v1=hex_hmac. The signed input is t + "." + raw_body, using HMAC-SHA256 and your endpoint secret.
import crypto from 'node:crypto';
export function verifyVeryWebhook(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(part => part.split('=', 2))
);
// Parse the timestamp only for the freshness check below.
const timestamp = Number(parts.t);
if (!Number.isSafeInteger(timestamp)) throw new Error('Invalid signature timestamp');
// Reject replayed or significantly future-dated requests before comparing HMACs.
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
throw new Error('Stale webhook');
}
// Reconstruct the signed input from the raw t token exactly as sent.
// Re-serializing the parsed number would reject otherwise valid signatures.
const expected = crypto
.createHmac('sha256', secret)
.update(parts.t)
.update('.')
.update(rawBody)
.digest();
const received = Buffer.from(parts.v1 ?? '', 'hex');
if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
throw new Error('Invalid webhook signature');
}
}
Check that the timestamp is within five minutes, reconstruct the signed input from the raw t value in the header (not a re-serialized number), and compare signatures in constant time. Also use the X-Webhook-Event-Id and X-Webhook-Event-Type headers for routing and deduplication.
Delivery behavior#
- Return any
2xxresponse within five seconds. - Redirects are never followed: any
3xxresponse counts as a failed delivery, so the endpoint URL must respond directly. - Failed deliveries are retried up to eight total attempts with increasing backoff.
- Owners and admins can manually redeliver completed or exhausted deliveries with the same
event_id. - Repeated failures can disable an endpoint. Re-enable it in the portal after fixing the receiver; this resets the consecutive failure counter.
- Disabled or deleted endpoints receive no new events or queued retries.
Security: Endpoint URLs must use HTTPS and resolve to public internet addresses. Requests to localhost, private networks, and link-local ranges are blocked, and redirects are never followed — register the final URL, not one that redirects to it.