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-changedorcore.user.created? - Why is my Uphold
Webhook-Signatureverification failing? - How do I get into the Uphold Webhooks Portal (management link)?
Verification (core)
| Header | Value |
|---|---|
Webhook-Id | Unique message id for the delivery. The same value is reused when the delivery is retried |
Webhook-Timestamp | Unix seconds when the delivery was sent |
Webhook-Signature | Space-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
| Event | Fired when | data |
|---|---|---|
core.user.created / core.user.deleted | A user was created / deleted | user |
core.transaction.created | A transaction was created | transaction |
core.transaction.status-changed | Transaction status changed: processing, on-hold, completed, failed | transaction (with statusDetails.reason when failed or on hold) |
core.account.created / core.account.archived / core.account.unarchived | Account lifecycle | account |
core.account.balance-changed | An account's balance changed | account.balance.{total,available} |
core.external-account.created / .status-changed / .deleted | Linked card, bank, APM or crypto account changes | externalAccount |
core.kyc.<process>.status-changed | An individual's KYC process changed (profile, email, phone, address, identity, proof-of-address, screening, risk, ...) | { status, userId } |
core.kyb.<process>.status-changed | A business's KYB process changed | { status, userId } |
core.assets.cooldown-ended | An 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
- references/overview.md - Full event list, payload shapes, delivery and retries
- references/setup.md - Opening the Webhooks Portal, adding endpoints, finding the secret, IPs
- references/verification.md - SDK and manual verification, gotchas, debugging