---
name: verify-receipts-and-sessions
description: Verify what Chain Daddy signs on your own server before acting on it - store purchase receipts (iap-receipt+v1), Crown App session tokens (app-session+v1), Sign in with Chain Daddy ID tokens (signin+v1), and webhook signatures - with the exact keys, claims and checks, plus published test vectors. Use when writing server code that grants items, credits a purchase, trusts a player's wallet, or receives Chain Daddy webhooks.
license: MIT
---

# Verify receipts, sessions and webhooks

Never act on something because a client sent it. Each kind below has its own keys and its own `typ` claim,
so one can never pass for another - as long as you check `typ`.

## Which token is which

| You have | `typ` claim | Algorithm | `iss` | Keys (JWKS) | Also check |
|---|---|---|---|---|---|
| Store receipt (a confirmed purchase) | `iap-receipt+v1` | EdDSA (Ed25519) | `https://chaindaddy.io` | `https://api.chaindaddy.io/api/v2/iap/jwks` | `storeId` is your store; `livemode` is `true` in production; credit each `orderId` once |
| Crown App session token (`session-token` action) | `app-session+v1` | EdDSA | `https://chaindaddy.io` | `https://api.chaindaddy.io/api/v2/apps/jwks` | `aud` is your app id (`<developerId>/<name>`); `exp` (5 min, allow 30 s skew); refuse a `jti` you have seen; `idv === true` if you need a verified person |
| Sign in with Chain Daddy ID token | `signin+v1` | ES256 | `https://api.chaindaddy.io` | `https://api.chaindaddy.io/api/v2/oauth/jwks` | `aud` is your client id; `nonce`; `exp` (5 min) |

Notes:
- The JWS **header** `typ` is a plain `JWT`. What the token is lives in the `typ` **claim**. Check the claim.
- Pick the key by the header's `kid`. Each set lists the current key first and keeps the previous one, so a
  cached remote key set that refetches on an unknown `kid` survives rotation with no work.
- `sub` is the wallet in all three: lowercase `0x…` on EVM, base58 on Solana.

## TypeScript (jose)

```ts
import { createRemoteJWKSet, jwtVerify } from 'jose';

const API = 'https://api.chaindaddy.io';
const ISSUER = 'https://chaindaddy.io';
const receiptKeys = createRemoteJWKSet(new URL('/api/v2/iap/jwks', API));
const sessionKeys = createRemoteJWKSet(new URL('/api/v2/apps/jwks', API));

export async function verifyReceipt(jws: string, storeId: string) {
  const { payload } = await jwtVerify(jws, receiptKeys, { algorithms: ['EdDSA'], issuer: ISSUER });
  if (payload.typ !== 'iap-receipt+v1') throw new Error('not a store receipt');
  if (payload.storeId !== storeId) throw new Error('receipt is for another store');
  if (payload.livemode === false && process.env.NODE_ENV === 'production') throw new Error('test receipt');
  return payload; // grant by payload.sku, payload.quantity, payload.wallet; dedupe on payload.orderId
}

export async function verifySession(token: string, appId: string) {
  const { payload } = await jwtVerify(token, sessionKeys, {
    algorithms: ['EdDSA'], issuer: ISSUER, audience: appId, clockTolerance: 30,
  });
  if (payload.typ !== 'app-session+v1') throw new Error('not an app session token');
  if (!payload.exp) throw new Error('no expiry');
  return payload; // payload.sub is the wallet; check payload.idv === true to require a verified person
}
```

For the ID token, use your OpenID Connect library (see `sign-in-with-chain-daddy`), or `jwtVerify` with the
oauth key set, `issuer: 'https://api.chaindaddy.io'`, `audience: <client id>`, `algorithms: ['ES256']`, then
check `typ === 'signin+v1'` and the nonce. Go: `server/cdapi` in https://gitlab.com/opencrown/flux-gp-kit.

## Refunds: offline is not enough for everything

An offline receipt check can't see a refund made since. For anything that matters after the fact, also check
online - `POST https://api.chaindaddy.io/api/v2/iap/receipts/verify` with `{"receipt": "…"}` returns
`valid` or `revoked` - or read the store's item ledger (`GET …/entitlements?wallet=`), which is the source of
truth for what a wallet owns now.

## Webhook signatures

| Webhook | Header | How to check |
|---|---|---|
| Store webhook (`iap.*`) | `X-Webhook-Signature` | Hex HMAC-SHA256 of the **raw body**, keyed with the `whsec_…` secret as given |
| Crown App runner events | `X-App-Signature` | Hex HMAC-SHA256 of the raw body, keyed with the runner secret **hex-decoded to bytes** |

```js
import crypto from 'node:crypto';
function verifyStoreWebhook(rawBody, signature, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return signature?.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
```

Check against the raw bytes before parsing JSON, compare in constant time, and ignore a repeated event `id`
(retries reuse it). Answer 2xx within 5 seconds.

## Prove your checker with the test vectors

https://docs.chaindaddy.io/vectors/tokens.json holds two key sets (`jwks.session`, `jwks.receipts`), a fixed
`now`, an `appId`, a `storeId` and 15 tokens marked `valid` or not (expired, clock skew, another app, another
issuer, `typ` only in the header, no expiry, a forgery, a receipt offered as a session and the reverse,
another store). Load the sets with `createLocalJWKSet`, run every case at `now`, and pass only when your
code accepts exactly the valid ones. Add this as a unit test.

## Mistakes to avoid

- Checking the header `typ` instead of the claim, or not checking `typ` at all.
- Verifying a session token against the receipt keys, or the other way round: they use different key sets.
- Using `iss: https://api.chaindaddy.io` for receipts and sessions. Only Sign in ID tokens use the `api.` issuer.
- Accepting a test receipt (`livemode: false`) or another store's receipt in production.
- Crediting the same `orderId` twice when the webhook and your poller both see it.
- Parsing the webhook body before checking its signature, or comparing with `===`.
- Treating a session token as API access: it only proves presence to your server.

## Docs

- https://docs.chaindaddy.io/developer/verifying-tokens
- https://docs.chaindaddy.io/developer/store-and-rewards#verifying-receipts-on-your-server
- https://docs.chaindaddy.io/developer/security#session-tokens
