1.2.0-rc.1 / Payment profiles

Optional payments

Disclosed fixed quotes, explicit payer authority and separate payment and access states.

1.2.0-draft.3 · fixed-quote-v1 · 17 September 2026

This page specifies the fixed-quote-v1 pure contract and transition implementation for 1.2.0-draft.3. The module is not itself an HTTP service or a payment-rail adapter; its callers are described in paid service integration. The published draft-2 free reference remains unchanged. These decisions resolve ambiguities in native draft-1 section 11 under a new version and are not claimed to be wire-compatible with draft 2. This candidate remains an implementation candidate.

The implementation never initiates a charge, opens checkout, contacts a provider or verifies a signature. Its caller must supply authenticated facts, resolve current origin/delegation authority, enforce transaction uniqueness and persist results atomically. A confirmed result means the authenticated provider asserts that the exact quote obligation is satisfied under its documented status mapping. It does not mean checkout was merely created, cash is irrevocably settled, a bank was independently inspected or reversal is impossible. A real adapter must document what its mapped statuses establish and test that mapping.

Six machine contracts

profiles/payment.schema.json has closed $defs for quote, accepted_binding, mandate, check, verification and free. The five typed envelopes use exact protocol_version, profile and record_type fields; free bypass is exactly {"required":false}. The profile reuses bounded native UTF-8 JSON, canonical HTTPS references, integer-token rules and exact UTC timestamps. Bodies are at most 128 KiB. Unknown fields are rejected. JSON Schema remains a structural superset: equality, arithmetic, lexical numbers, calendars, authority and state require the Python checks and trusted integration.

Quote identity and fees

A quote names a UUID quote_id, UUID offer_id, policy origin, origin-scoped agreement service_id, origin-scoped verification_service_id, provider provider_id (the verification service’s issuer), exact verification_endpoint, payer, currency, scale, ordered items, total and issue/expiry times.

This draft uses verification_endpoint in place of the earlier status_url. It must match the authoritative verification descriptor’s base_url + "verify"; the pure engine compares the resolver’s result and does not fetch a caller-selected URL. Agreement-service authority and payment-verifier authority are separate capabilities.

An item has unique bounded item_id, kind, explicit payee_id, amount_minor and description. Kinds are resource_license and provider_service. At least one resource-license line is required, including an explicit zero resource price when only a provider fee is payable. A provider cannot silently charge its originator subscription to an agent. The payer separately authorizes the exact quote, including every fee/payee.

Amounts are unsigned canonical decimal strings, at most 18 digits; scale is an integer token from 0 to 9. The positive total must equal the exact item sum and remain below 10^18. Currency is a three-uppercase-letter identifier; this parser does not claim a live currency registry or infer a currency’s scale. The adapter must support the quoted identifier/scale explicitly. There are no floats, exchange-rate conversions, metering, partial payments or arbitrary quantity debits.

register_quote(registry, raw_quote_bytes) keeps immutable identity (provider_id, quote_id) against the exact byte digest. Changed whitespace or items under that identity conflict. bind_quote_to_agreement additionally permits only one agreement for that quote. The caller must persist this registry and agreement/offer uniqueness in the same transaction as creating payment state; in-memory helpers alone do not provide distributed idempotency.

Accepted terms are not spending authority

accepted_binding is a trusted adapter projection of a verified accepted agreement, not a substitute signature or client-supplied assertion. It contains agreement/principal/origin/service/offer IDs, exact offer_digest, exact quote_digest, accepted request, acceptance time and accepted access/use expiry times. The full request has exactly url, actions, purposes and explicit supported_obligations; it must belong to the quote origin and use known vocabulary.

The HTTP adapter must verify that these values actually appear in the original accepted offer/receipt before calling start_payment. That function only validates the supplied binding. It cannot infer those facts from the offer digest alone. Acceptance must have occurred while the quote was valid, and accepted access must end after acceptance and no later than use expiry.

A separate payment.mandate is mandatory for the paid path. It binds its own UUID, agreement, exact quote digest/ID, origin, service, provider, payer, currency, scale, total, issue/expiry and the exact authorization pay_exact_quote_once. authorize_payment requires a separately authenticated payer ID matching the quote payer. The accepting principal may differ from the payer; assent does not prove authority to spend that payer’s money. Mandate replacement is rejected in this bounded profile; an identical byte retry changes nothing. Issuing a mandate is not a charge.

Read-only verification

verification_request(state, check_id) creates the payment.check body for authenticated read-only POST verify. It includes check ID, quote identity/digest, accepted offer digest, accepted request digest, agreement, mandate, origin, verification service, provider, payer, currency, scale and total. Request digest is SHA-256 of contracts.json_bytes(accepted_binding.request); object keys are sorted, arrays retain their accepted order. Do not silently reconstruct a different ordered request.

A payment.verification response binds all those fields plus positive monotonic sequence, one of pending/confirmed/failed/reversed, provider transaction reference, effective_at and checked_at. Confirmed/reversed require a nonsecret bounded provider reference. After first confirmation/reversal, that reference cannot change. effective_at must not predate mandate issuance or move backwards. Confirmation must have become effective before mandate expiry. A delayed check after mandate expiry may confirm a payment that became effective inside the authorized interval.

Caller authentication of provider responses is mandatory; the intended signed response type is odexa-payment-status+jws, checked by the actual adapter. This module does not perform that signature check. HTTPS, credential handling, provider signing and response parsing at the transport boundary remain integration work.

apply_verification(state, raw_response, expected_check_id=..., authority=full_resolver_result, now=...) requires:

  • An explicit decision == "allow"; matching identity fields in a denial are insufficient.
  • Exact policy origin, verification service, issuer/provider, capability verify_payments, endpoint and authorized payment-status key use.
  • authority.operation_id == check_id and authority.payment_request_digest equal to the digest of this exact canonical payment-check body.
  • authority.request_digest equal to the accepted resource/action/purpose/support request digest. A decision for resource B cannot authorize payment processing for agreement A.
  • A current authority observation no more than five seconds old, matching the delegation contract. Newly applied provider responses must have checked_at no more than 60 seconds old and not in the future.

The resolver’s authority object is trusted adapter input. These comparisons do not authenticate arbitrary JSON or prevent a malicious caller from fabricating it. The caller must use the resolver’s result directly after establishing its HTTPS/current-authority observation; these helpers are not a trust bootstrap.

State and time decisions

PaymentState is immutable internal Python state, not a public wire payload. It keeps retained quote/acceptance/mandate bytes, separate payment and access states, check identities/digests and sequence history. Exact response bytes and authenticated verification material must also be durably retained by the caller for evidence/export.

Current payment state Allowed next provider states
pending pending, failed, confirmed, reversed
failed pending, failed, confirmed, reversed
confirmed confirmed, reversed
reversed reversed only

A failed attempt does not assert the quote can never be satisfied: a later authenticated newer provider state may confirm the same quote under the same valid one-payment mandate. This read-only transition never retries a debit. A reversal may be received before a local confirmation was observed; it still blocks access. A confirmed state cannot degrade to failed/pending; the provider must express a reversal.

Matching confirmation changes an otherwise eligible pending agreement to active. Pending/failed do not grant access. Reversal sets future access to revoked. Expired or administratively revoked access remains terminal even when later financial information arrives; a financial confirmation can be retained while access stays expired/revoked. A new agreement is needed to restore access. Past acceptance/use rights and evidence are not rewritten, and delivered bytes cannot be recalled.

Quote expiry applies to new acceptance, not settlement of a timely accepted quote. The accepted agreement’s windows and payer mandate remain authoritative. New confirmation after accepted access expiry records the financial assertion without granting new access. expire and revoke implement the payment-related access gate only. An active result is not a policy grant or access token: the service must still perform every current authority, policy/agreement and gateway authorization check.

An exact authenticated check-ID/response-byte replay returns the current state, including after reversal; it cannot restore the earlier state. Changed bytes under the same check ID, stale sequence, reversed time or mismatched context fail without mutating input. The bounded helper retains 256 check identities; at capacity it fails closed. Production history pagination/storage is not implemented. The caller serializes concurrent changes, atomically stores original responses and never lets two workers replace a newer state with an older snapshot.

Free path and omitted functions

free_payment({"required":false}) creates only a satisfied payment gate. It makes no provider discovery/request and rejects mandates or verification. No paid code, provider configuration, checkout or payment account is needed for a free agreement. Paid-profile implementation is optional for deployers; its optionality does not turn payment-gate success into permission to access an asset.

No automatic debit, bank/card data, traffic-to-charge mapping, refund initiation, dispute processing, tax calculation, irrevocable-settlement claim or production payment adapter is supplied. Reversal is an authenticated status fact, not a refund command. Implementing this profile must not silently standardize those omitted provider functions.

Recorded focused verification

From the extracted source/ directory described in the quickstart:

PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p test_payment_profile.py -v

The earlier focused component run recorded 24 tests passed. Coverage includes exact-byte quote identity, fee sums, wrong payer, separate mandate, scope/check/authority substitution, denied authority, malformed amounts/JSON, quote/mandate/access time boundaries, stale/out-of-order responses, retry conflicts, reversal, immutable inputs and free bypass. One test also checks the semantic classifications of all 27 machine-readable corpus cases in profiles/fixtures/payment-cases.json.

The corpus supplies $defs names, values and independent structural/semantic expected results; some semantically invalid values intentionally pass structural JSON Schema. Independent AJV and cross-profile resolver checks are distinct evidence. The original focused count does not establish HTTP integration, durable financial handling or complete conformance; current validation records the current integrated results.

Additional integration evidence

The reference adds a durable SQLite ledger, an authenticated signature boundary, network verification and paid agreement service around this pure contract. Each layer retains its own trust and persistence requirements. See current validation for executed evidence and release status for publication gates. These integrations do not initiate a debit or independently prove settlement.

Odexa / Protocol explorer

This page. Your terms.

Inspect this website’s published policy and see how a proposed use is evaluated.

Current pagehttps://odexa.io/guides/payments/
Loading policy…

Published JSON
Open JSON

This is a local policy check, not a signed agreement or proof of agent compliance. Other published licences and applicable rights still apply. How policy evaluation works →

Odexa / Get in touch

Start a conversation.

Tell us what you have in mind. We’ll respond where we can.

We use these details to review and respond to your enquiry. Please leave out confidential information. Submitting does not subscribe you to marketing. Privacy policy.