Payment challenge — the 402 response that tells a client what to pay.
A challenge is returned in the WWW-Authenticate: Payment header when a
request lacks a valid payment credential. The challenge ID is HMAC-SHA256
bound to all challenge parameters, making it tamper-proof without requiring
server-side state.
HMAC Binding
The challenge ID is computed as:
base64url(HMAC-SHA256(secret_key, realm|method|intent|request|expires|digest|opaque))Seven fixed positional slots joined by |. Optional fields use empty string
when absent, preserving slot positions. The request and opaque fields are
used as their raw base64url-encoded strings (never re-serialized).
Fields
id— HMAC-SHA256 challenge ID (computed bycreate/2)realm— server protection space (e.g.,"api.example.com")method— payment method name (e.g.,"stripe","tempo")intent— intent type (e.g.,"charge")request— base64url-encoded JSON request payload (pre-encoded)description— (optional) human-readable descriptiondigest— (optional) content digest per RFC 9530expires— (optional) RFC 3339 expiration timestampopaque— (optional) base64url-encoded JSON server correlation data
API Functions
| Function | Arity | Description | Param Kinds |
|---|---|---|---|
verify | 2 | Verify a challenge's HMAC-bound ID against a secret key using constant-time comparison. | challenge: value, secret_key: value |
create | 2 | Create a new challenge with an HMAC-SHA256 bound ID. | params: value, secret_key: value |
Summary
Functions
Create a new challenge with an HMAC-SHA256 bound ID.
Verify a challenge's HMAC-bound ID against a secret key using constant-time comparison.
Types
Functions
Create a new challenge with an HMAC-SHA256 bound ID.
Parameters
params- Keyword list with:realm,:method,:intent,:request(required) and:description,:digest,:expires,:opaque(optional) (value)secret_key- HMAC-SHA256 secret key for challenge binding (value)
Returns
Challenge struct with computed id (struct)
Composes With
verify
# descripex:contract
%{
params: %{
params: %{
description: "Keyword list with `:realm`, `:method`, `:intent`, `:request` (required) and `:description`, `:digest`, `:expires`, `:opaque` (optional)",
kind: :value
},
secret_key: %{
description: "HMAC-SHA256 secret key for challenge binding",
kind: :value
}
},
returns: %{type: :struct, description: "Challenge struct with computed `id`"},
composes_with: [:verify]
}
@spec verify(t(), String.t()) :: :ok | {:error, :invalid_challenge}
@spec verify(t(), String.t()) :: {:error, :invalid_challenge}
Verify a challenge's HMAC-bound ID against a secret key using constant-time comparison.
Parameters
challenge- Challenge struct to verify (value)secret_key- HMAC-SHA256 secret key used when challenge was created (value)
Returns
:ok if valid, {:error, :invalid_challenge} if tampered (tagged)
Errors
:invalid_challenge
Composes With
create
# descripex:contract
%{
params: %{
challenge: %{description: "Challenge struct to verify", kind: :value},
secret_key: %{
description: "HMAC-SHA256 secret key used when challenge was created",
kind: :value
}
},
errors: [:invalid_challenge],
returns: %{
type: :tagged,
description: "`:ok` if valid, `{:error, :invalid_challenge}` if tampered"
},
composes_with: [:create]
}