Payment hooks
Observe payment lifecycles
Payment hooks let you attach logging, metrics, tracing, and request-local context to MPP payment flows without rewriting the payment handler.
Lifecycle overview
Payment hooks follow the 402 payment flow. The server issues a Challenge for an unpaid request. The client selects that Challenge, creates a Credential, and retries the request. The server verifies the Credential, returns the protected response, and attaches a Receipt.
Server hooks
Register server hooks on the object returned by Mppx.create from mppx/server.
| Hook | Canonical event | Runs when |
|---|---|---|
onChallengeCreated | challenge.created | The server issues a payment Challenge |
onPaymentSuccess | payment.success | The server verifies payment and creates a Receipt |
onPaymentFailed | payment.failed | A submitted Credential or standalone verification fails |
on('*', handler) | * | Any server payment event fires |
Server handlers are awaited inline and sequentially on the payment request path. Handler errors are ignored and do not change payment handling, but slow handlers still delay the response.
import { , } from 'mppx/server'
const = .({
: [.(), .()],
})
.(({ , , }) => {
.('challenge.created', {
: .,
: .,
: .,
: .,
})
})
.(({ , , }) => {
.('payment.success', {
: .,
: .,
: .,
: .,
})
})
.(({ , , }) => {
.('payment.failed', {
: ?.,
: .,
: .,
: .,
})
})Client hooks
Register client hooks on the object returned by Mppx.create from mppx/client.
| Hook | Canonical event | Runs when |
|---|---|---|
onChallengeReceived | challenge.received | A 402 Challenge is selected |
onCredentialCreated | credential.created | A Credential is created for the selected Challenge |
onPaymentResponse | payment.response | The retry after payment returns a successful response |
onPaymentFailed | payment.failed | Challenge parsing, Credential creation, or retry handling fails |
on('*', handler) | * | Any client payment event fires |
onChallengeReceived runs before onChallenge. It can return a non-empty Credential string to override the default credential flow. Other client hooks are observers: thrown errors are ignored and do not change payment handling.
import { , } from 'mppx/client'
import { } from 'viem/accounts'
const = ('0x...')
const = .({
: [
.({ }),
.({ , : '10' }),
],
: false,
})
.(({ }) => {
.('challenge.received', {
: .,
: .,
: .,
})
})
.(({ }) => {
.('credential.created', {
: .,
: .,
})
})
.(({ , }) => {
.('payment.response', {
: .,
: .,
})
})
.(({ , }) => {
.('payment.failed', {
: ?.,
: instanceof ? . : 'Error',
})
})Charge intent
For charge, one request maps to one payment. The hook events describe the Challenge, the client Credential, and the server Receipt for that charge.
Use method.intent === 'charge' on server hooks or challenge.intent === 'charge' on client hooks to isolate charge telemetry.
Session intent
For session, hooks observe the MPP request flow around session Challenges and Credentials. Session-specific channel open, voucher, top-up, close, and settlement behavior is handled by the session method APIs; the payment hook names remain the same.
Use method.intent === 'session' on server hooks or challenge.intent === 'session' on client hooks to isolate session telemetry.
import { , } from 'mppx/server'
const = .({
: [.()],
})
.(({ , , }) => {
if (. !== 'session') return
.('session.payment.success', {
: .,
: .,
: .,
})
})Subscription intent
For subscription, hooks observe the payment flow when the server issues a subscription Challenge and the client returns a subscription Credential. Later requests can be authorized by method-specific subscription state.
Use method.intent === 'subscription' on server hooks or challenge.intent === 'subscription' on client hooks to isolate subscription telemetry.
Event payloads
Payloads carry the selected method, Challenge, request context, and event result. on('*') receives { name, payload }; typed helpers receive the inner payload directly.
{
name: 'payment.success',
payload: {
challenge: { id: 'ch_123', intent: 'session', method: 'tempo' },
method: { intent: 'session', name: 'tempo' },
receipt: {
method: 'tempo',
reference: '0x...',
status: 'success',
timestamp: '2026-06-24T00:00:00.000Z',
},
request: { amount: '0.01' },
},
}{
name: 'payment.response',
payload: {
challenge: { id: 'ch_123', intent: 'session', method: 'tempo' },
credential: 'Payment ...',
method: { intent: 'session', name: 'tempo' },
response: new Response(null, { status: 200 }),
},
}Subscription management
Each hook registration returns an unsubscribe function. Keep the function when a handler is temporary, such as request-scoped instrumentation, tests, or a process that recreates payment instances. Call it to detach the handler and stop receiving events.
import { , } from 'mppx/server'
const = .({
: [.()],
})
const = .(({ }) => {
.(.)
})
()