Palm Verification SDK Guides /

Native SDK Webhooks

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#

  1. Open your Palm Verification SDK app in the Developer Portal.
  2. Under Settings → Webhooks, choose New.
  3. Enter a public HTTPS URL and select review.status_changed, user.status_changed, or both.
  4. Copy the signing secret immediately. It is shown only once.
  5. Open the webhook and choose Send test to queue a signed webhook.test delivery.

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_id is stable across retries and manual redelivery. Store it as an idempotency key.
  • external_user_id is the same app-scoped user ID returned by the SDK.
  • client_reference_id is included on review.status_changed when you supplied clientReferenceId for the SDK operation that created that review.
  • user.status_changed is account-level and does not include a transaction-specific client_reference_id.
  • status carries 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 2xx response within five seconds.
  • Redirects are never followed: any 3xx response 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.

VeryAI

Get the VeryAI app

Scan the QR code to download the app