Payments create explicit USD top-ups through Stripe Checkout. A browser redirect has no effect on a balance. Credits come only from signed processor evidence and the configured credits ledger callback.
Payments are disabled by default. With PAYMENTS_ENABLED unset, empty or false,
every /v1/payments/* request returns JSON 404 without payment storage or
processor access. A malformed flag or configuration disables payments and emits
one warning without configuration values.
An enabled example uses references to privately configured secrets:
{
"PAYMENTS_ENABLED": "true",
"PAYMENT_PROCESSOR_CONFIG_JSON": "{\"processor\":\"stripe\",\"secret_key_ref\":\"STRIPE_MERCHANT_SECRET\",\"return_origins\":[\"https://billing.example.com\"],\"product_name\":\"Gateway credits\"}",
"PAYMENT_WEBHOOK_KEY_REF": "STRIPE_ENDPOINT_SECRET"
}The merchant key and webhook signing secret are supplied by the host's secret resolver. Their values never belong in the JSON configuration, responses or logs. Use a webhook endpoint belonging to the same direct Stripe merchant as the Checkout key. Connected-account events are rejected. Test and live mode must match the Checkout Session returned by Stripe.
Payment routes require a migrated durable store. The private D1 domain is
http://intelligence.internal/v1/managed-state/payments, with fixed reserve,
save, find, claim and finish operations. D1PaymentStore accepts a private
transport; SqlPaymentStore accepts an existing migrated SQLite database.
The application selects D1 when configured, otherwise the existing USAGE_DB_PATH SQLite store. Neither creates tables or falls back to memory. Missing tables, missing
columns, unavailable storage or unconnected storage return JSON 503
payments_unavailable before any processor request. Apply the additive payment
migration through the normal migration process before enabling the flag.
Send a gateway key to POST /v1/payments/checkout with:
{
"amount_microusd": 500000,
"currency": "USD",
"idempotency_key": "topup-2026-10-09-001",
"return_url": "https://billing.example.com/return"
}The default permission grants only the verified key owner in the legacy single-principal scope. The registered permission authority verifies an active billing or admin workspace role using the same verified tenant context as request admission. A top-up funds that principal in that workspace, rather than the organisation as a whole. Client body fields cannot select a workspace. Gateway key scope enforcement also applies.
Amounts are integer micro-USD, in whole cents: multiples of 10,000, from 500,000
($0.50) to 1,000,000,000 ($1,000). The currency is exactly USD. The return URL
must use HTTPS and match an allowlisted origin, including its port, exactly.
An idempotency key is an opaque 1–128 character identifier using letters,
digits, underscores, colons, periods or hyphens.
At most ten distinct checkout reservations are accepted per principal per UTC
day, including processor failures. Reusing the same scoped idempotency key and
body returns the same checkout without consuming another reservation. Reusing
it with a different body returns 409 payment_idempotency_conflict. Concurrent
creation in another process can return retryable 503
payment_checkout_processing; retry the same key and body.
The processor request is form encoded, goes only to
https://api.stripe.com/v1/checkout/sessions, has redirects disabled and uses
the opaque checkout ID as Stripe's idempotency key and client reference. Its
only metadata is that opaque ID. Processor failures return 503
payment_processor_unavailable; retrying preserves the original processor
idempotency ID.
A successful checkout response is:
{
"checkout_id": "pay_example",
"url": "https://checkout.stripe.com/c/example",
"amount_microusd": 500000,
"currency": "USD",
"status": "pending"
}Opening the URL allows payment at Stripe. Returning to the browser URL never
credits a balance. The returned pending describes the checkout response,
not a confirmed payment.
The ledger charges requests only when CREDITS_ENABLED=true and
CREDITS_ENFORCEMENT is funded (owners with a ledger revision above zero) or
all (every authenticated owner, including administrators). The default off
records top-ups without charging requests. Credit each principal before choosing
all; insufficient credit returns JSON 402 credits_insufficient before dispatch.
Refunds and disputes append negative adjust entries with actor
processor:stripe, under the payment event's immutable operation ID. They may
make balance and available credit negative after credit has been spent; new
charged requests remain refused until the shortfall is covered. Only the verified
payment callback can create processor entries.
Configure Stripe to deliver these types to POST /v1/payments/webhook, and
confirm them in the Stripe dashboard:
checkout.session.completed: credit only withpayment_status=paid.checkout.session.async_payment_succeeded: credit the delayed payment.checkout.session.async_payment_failed: record failure without credit.charge.refunded: append the increase in cumulative refunded amount.charge.dispute.created: append a negative processor adjustment through the ledger callback, using the dispute ID as its stable operation identity. A later dispute win requires an operator adjustment.
The webhook requires no gateway key and is CSRF-exempt. It reads raw bytes
before parsing JSON, verifies every supported v1 signature candidate with
HMAC-SHA256 and requires the timestamp to be within 300 seconds in either
direction. Re-serialization invalidates signatures. Unsupported schemes never
authorize an event. Bodies are limited to 65,536 bytes.
Signed events must match the stored session, direct merchant, mode, currency and amount. Refunds and disputes must link through the payment intent recorded on verified session evidence. Refund totals cannot exceed the original charge; disputes cannot exceed the checkout amount. Mismatches and unknown types are recorded and acknowledged without credit.
Refund or dispute evidence arriving before the session's payment intent is known
is retained as pending_match and returns 503 for retry. After the verified
session links the intent, the same event can be reconciled without losing its
original evidence hash or audit history.
An atomic event claim precedes the immutable PaymentEvent callback. A
completed replay returns 200 without invoking it again. Both session success
types share one credit operation, cumulative refund notifications apply only
their delta, and repeat dispute IDs share one adjustment operation. An event
ID reused with different bytes returns 400 payment_event_conflict.
The registered credits callback applies verified top-ups and processor refunds
or disputes to the same per-principal workspace owner as GET /v1/credits.
Refunds after spending may make the balance negative and block new reservations.
The callback is the only payment credit authority. It applies its immutable
idempotency ID atomically, including when recovering from a process crash after
ledger application but before the payment record is finalized. A missing,
denied, failing or unbound callback leaves pending_credit evidence and returns
503 payment_credit_unavailable, allowing Stripe to retry. Concurrent active
claims also return this error. A claim can be recovered after a 60-second lease;
retry the original event first before processing other events for that checkout.
Do not acknowledge outstanding credit work manually as paid. Ledger history is
never erased by refunds or disputes.
Invalid checkout input or signature returns 400; absent authentication returns
401; denied billing permission returns 403; disabled payments return 404;
idempotency conflicts return 409; velocity exhaustion returns 429
payment_velocity_limited; unavailable processor, permission, storage or credit
authority returns 503. Payment responses use Cache-Control: no-store and
contain no secrets. Unknown payment methods return 405 when enabled.
Stored records contain opaque identity and checkout scope, processor IDs, integer amounts, mode, event/body hashes, claims, revisions and content-free audit outcomes. Return URLs and raw webhook bodies are not stored. Card data, customer names and email addresses are not stored. Operational records have no automatic purge; retain them according to reconciliation policy. Velocity counters use UTC day partitions and are independent of refund history.
Taxes, invoices, processor fees and charge policies remain explicit operator settings. The top-up amount is the credit amount; estimated usage is not an invoice. Rehearse the configured checkout and webhook flow in the merchant sandbox before accepting live payments.
The fixed processor contract follows the Stripe documentation: webhooks, signature verification, Checkout Session creation, Checkout metadata, and currency units. Verify endpoint event selection in the Stripe dashboard.