# Token.io Webhooks

## When to Use This Skill

* How do I receive Token.io webhooks?
* 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 not [Standard Webhooks](https://www.standardwebhooks.com/).
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.

```javascript
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:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/tokenio-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/tokenio-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/tokenio-webhooks/examples/fastapi/)

## 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/tokenio-webhooks/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](https://github.com/hookdeck/webhook-skills/blob/main/skills/tokenio-webhooks/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

```bash
# 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

```bash
# 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/tokenio-webhooks/references/overview.md) - Event types, payload structure, payment statuses
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tokenio-webhooks/references/setup.md) - Dashboard public key, subscribing with PUT /webhook/config
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tokenio-webhooks/references/verification.md) - Ed25519 verification in depth and gotchas