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 with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
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.
How do I verify the Token.io token-signature Ed25519 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, or VIRTUAL_ACCOUNT_CREDIT_RECEIVED events?
What do the payment statuses INITIATION_PROCESSING, INITIATION_COMPLETED, and INITIATION_REJECTED mean?
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 notStandard 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.functionverifyTokenWebhook(rawBody, signatureHeader, publicKeyB64url) {
if (!signatureHeader || !publicKeyB64url) returnfalse;
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 {
returnfalse; // 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:
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).
We recommend installing the webhook-handler-patterns skill alongside this one for handler sequence, idempotency, error handling, and retry logic. Key references (open on GitHub):
hookdeck-event-gateway - Webhook infrastructure that replaces your queue — guaranteed delivery, automatic retries, replay, rate limiting, and observability for your webhook handlers