Agent skill

Uphold Webhooks Skill

Receive and verify Uphold Enterprise (Core API) webhooks. Use when setting up Uphold webhook handlers, finding the whsec_ signing secret in the Svix-hosted Webhooks Portal, debugging Webhook-Signature verification (Webhook-Id, Webhook-Timestamp, Webhook-Signature headers, Svix / Standard Webhooks scheme), or handling Uphold events like core.user.created, core.transaction.status-changed, core.account.balance-changed, core.external-account.status-changed and core.kyc.*.status-changed.

Install this skill

npx skills add hookdeck/webhook-skills --skill uphold-webhooks


Uphold Enterprise (the Core API at api.enterprise.uphold.com) sends near real-time webhook events for users, KYC/KYB, accounts, external accounts, and transactions. Deliveries are made by Svix under Uphold's name: the headers use the unbranded webhook- prefix (not svix-), and Uphold points to Svix's verification guide and SDKs.

This skill covers the Enterprise REST APIs only. The legacy (pre-Enterprise) Uphold API has its own, older webhook scheme, which this skill does not cover.

When to Use This Skill

  • How do I receive Uphold webhooks?
  • How do I verify an Uphold webhook signature?
  • Where do I find my Uphold webhook signing secret?
  • How do I handle core.transaction.status-changed or core.user.created?
  • Why is my Uphold Webhook-Signature verification failing?
  • How do I get into the Uphold Webhooks Portal (management link)?

Verification (core)

HeaderValue
Webhook-IdUnique message id for the delivery. The same value is reused when the delivery is retried
Webhook-TimestampUnix seconds when the delivery was sent
Webhook-SignatureSpace-delimited list of v1,<base64 HMAC-SHA256> signatures

Signed content is {webhook-id}.{webhook-timestamp}.{raw body}. The key is the base64-decoded part of the endpoint secret after whsec_. The official svix SDK (npm and PyPI) accepts webhook-* headers directly. It enforces a 5-minute timestamp tolerance and compares in constant time.

import { Webhook } from 'svix';

// rawBody: the exact request bytes as a string, NOT re-serialised JSON
function verifyUpholdWebhook(rawBody, headers) {
  new Webhook(process.env.UPHOLD_WEBHOOK_SECRET).verify(rawBody, {
    'webhook-id': headers['webhook-id'],
    'webhook-timestamp': headers['webhook-timestamp'],
    'webhook-signature': headers['webhook-signature'],
  }); // throws WebhookVerificationError → respond 400
  // svix 2.x verify() returns undefined, so parse the body yourself
  return JSON.parse(rawBody);
}

Python: from svix.webhooks import Webhook, then Webhook(secret).verify(await request.body(), headers) followed by json.loads(raw_body).

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Envelope

{ "id": "6a3d8cea-5250-4bd0-a443-eb07b04c85d4", "type": "core.transaction.status-changed",
  "createdAt": "2025-02-20T21:21:09.943Z",
  "data": { "transaction": { "id": "623000c8-...", "status": "completed", "...": "..." } } }

The payload id and createdAt are Uphold's event fields. The Webhook-Id and Webhook-Timestamp headers belong to the Svix delivery. Don't mix them up.

Common Event Types

EventFired whendata
core.user.created / core.user.deletedA user was created / deleteduser
core.transaction.createdA transaction was createdtransaction
core.transaction.status-changedTransaction status changed: processing, on-hold, completed, failedtransaction (with statusDetails.reason when failed or on hold)
core.account.created / core.account.archived / core.account.unarchivedAccount lifecycleaccount
core.account.balance-changedAn account's balance changedaccount.balance.{total,available}
core.external-account.created / .status-changed / .deletedLinked card, bank, APM or crypto account changesexternalAccount
core.kyc.<process>.status-changedAn individual's KYC process changed (profile, email, phone, address, identity, proof-of-address, screening, risk, ...){ status, userId }
core.kyb.<process>.status-changedA business's KYB process changed{ status, userId }
core.assets.cooldown-endedAn asset cooldown period ended{ userId, assets, cooldown }

KYC/KYB status is one of ok, failed, running, pending, exempt. references/overview.md lists every event.

Environment Variables

UPHOLD_WEBHOOK_SECRET=whsec_xxxxx   # endpoint signing secret from the Webhooks Portal

Local Development

# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 uphold --path /webhooks/uphold

Add the printed URL as an endpoint in the Webhooks Portal, then send a test event from the portal or trigger one with the Sandbox test helpers.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Oct 28, 2026

View on GitHub →