Agent skill
Token.io Webhooks Skill
Receive and verify Token.io webhooks. Use when setting up Token.io webhook handlers, debugging Ed25519 signature verification, subscribing to webhook config via PUT /webhook/config, or handling open banking / A2A payment events like PAYMENT_STATUS_CHANGED, REFUND_STATUS_CHANGED, VRP_STATUS_CHANGED, and VIRTUAL_ACCOUNT_CREDIT_RECEIVED. Note: Token.io does NOT use HMAC or Standard Webhooks — it signs the raw body with an ASYMMETRIC Ed25519 signature in the token-signature header, verified with your member's Ed25519 public key.
Install this skill
npx skills add hookdeck/webhook-skills --skill tokenio-webhooks
When to Use This Skill
- How do I receive Token.io webhooks?
- How do I verify the Token.io
token-signatureEd25519 signature? - Why is my Token.io webhook signature verification failing?
- How do I subscribe to webhooks with
PUT /webhook/config? - How do I handle
PAYMENT_STATUS_CHANGED,REFUND_STATUS_CHANGED,VRP_STATUS_CHANGED, orVIRTUAL_ACCOUNT_CREDIT_RECEIVEDevents? - What do the payment statuses
INITIATION_PROCESSING,INITIATION_COMPLETED, andINITIATION_REJECTEDmean?
How Token.io Webhooks Work (Read This First)
Token.io is an open banking / account-to-account (A2A) payments provider. Its webhooks are not HMAC and not Standard Webhooks. Every delivery is signed with an asymmetric Ed25519 signature:
token-signature— the Ed25519 signature of the raw POST body, base64url encoded.token-event— the event type, e.g.PAYMENT_STATUS_CHANGED(a separate header, not a body field).
You verify with your member's Ed25519 public key from the Token Dashboard (Settings → Member Information), which is base64url-encoded (no padding). There is no shared secret — Token holds the private key, you hold the public key.
Token.io ──POST body + token-signature + token-event──▶ your endpoint
│ Ed25519.verify(publicKey, rawBody, signature)
▼
dispatch on token-event → act → return 200
Critical: the signed message is the exact raw bytes of the POST body. Capture the raw body before JSON parsing — any re-serialization (key reorder, whitespace, unicode escaping) changes the bytes and the signature will not match.
Verification (core)
Import the base64url public key as an Ed25519 JWK and verify the raw body with Node's built-in crypto — no external SDK is needed for verification. The official token-io npm package is a broad API client (used to subscribe to webhooks), not a webhook verifier, so verify manually with a crypto library.
const crypto = require('crypto');
// token-signature: Ed25519 signature of the RAW body, base64url.
// token-event: the event type (e.g. PAYMENT_STATUS_CHANGED).
// publicKeyB64url: your member's Ed25519 public key from the Token Dashboard
// (Settings → Member Information), base64url, no padding.
function verifyTokenWebhook(rawBody, signatureHeader, publicKeyB64url) {
if (!signatureHeader || !publicKeyB64url) return false;
try {
const key = crypto.createPublicKey({
key: { kty: 'OKP', crv: 'Ed25519', x: publicKeyB64url },
format: 'jwk',
});
const message = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody, 'utf8');
return crypto.verify(null, message, key, Buffer.from(signatureHeader, 'base64url'));
} catch {
return false; // malformed key/signature = invalid
}
}
Always verify against the raw body — parse JSON only after the signature checks out.
For complete handlers with route wiring, event dispatch, and tests, see:
Common Event Types
The event type arrives in the token-event header (not the body). Subscribe to the ones you need via PUT /webhook/config (see references/setup.md).
Event (token-event) | Fires When | Common Use Cases |
|---|---|---|
PAYMENT_STATUS_CHANGED | A Payments v2 payment changes status | Update order/payment state, fulfilment |
TRANSFER_STATUS_CHANGED | A Payments v1 transfer changes status | Legacy payment tracking |
REFUND_STATUS_CHANGED | A refund changes status | Reconcile refunds |
VRP_STATUS_CHANGED | A Variable Recurring Payment changes status | Subscriptions, sweeping |
VRP_CONSENT_STATUS_CHANGED | A VRP consent/mandate changes status | Mandate lifecycle |
VIRTUAL_ACCOUNT_CREDIT_RECEIVED | A virtual account (payin) is credited | Reconcile inbound funds |
PAYOUT_STATUS_CHANGED | A payout changes status | Settlement tracking |
Token.io also emits SETTLEMENT_RULE_PAYOUT_EXECUTION_FAILED, BANK_AIS_OUTAGE_STATUS_CHANGED, and BANK_SIP_OUTAGE_STATUS_CHANGED. See references/overview.md for the full list and payloads.
Payment status values
PAYMENT_STATUS_CHANGED carries a payment object whose status is one of INITIATION_PROCESSING, INITIATION_COMPLETED, INITIATION_REJECTED (and later SUCCESS). The raw ISO 20022 bank status is in bankPaymentStatus — use status for your logic and keep bankPaymentStatus for audit/debugging.
Environment Variables
# Your member's Ed25519 PUBLIC key (base64url, no padding) from the Token
# Dashboard → Settings → Member Information. NOT a shared secret, and NOT a
# PEM/DER-wrapped key — this is the raw 32-byte key as ~43 base64url chars.
TOKEN_WEBHOOK_PUBLIC_KEY=L3OIceAp0ZGy7xUrkeY6Lk4fB2DvtAsm0m7Wa1DSdvo
Local Development
# Start tunnel (no account needed) — forwards to your local handler
npx hookdeck-cli listen 3000 tokenio --path /webhooks/tokenio
Register the resulting public URL as the url in your webhook config (PUT /webhook/config). Token.io requires your endpoint to return 200; non-200 responses are retried with exponential backoff (~10, 30, 70, 150 min) for up to 72 hours (~10 attempts).
Reference Materials
- references/overview.md - Event types, payload structure, payment statuses
- references/setup.md - Dashboard public key, subscribing with PUT /webhook/config
- references/verification.md - Ed25519 verification in depth and gotchas