# SingleFax AP2 payments

SingleFax implements the **human-present merchant flow of AP2 v0.1.0** for $20, $50 and $100 prepaid fax-credit packs. Credits belong to the authenticated SingleFax account. This flow accepts Stripe Shared Payment Tokens (SPTs); it does not spend saved cards or existing account credit. Every purchase requires separate passkey approval by the account owner.

The A2A extension is `https://github.com/google-agentic-commerce/AP2/tree/v0.1.0`, with role `merchant`. The reference extension URI `https://github.com/google-agentic-commerce/ap2/tree/v0.1` is also accepted for negotiation. AP2 v0.2 and purchases without the user present are not negotiated. The source models are [the pinned v0.1.0 AP2 mandate definitions](https://github.com/google-agentic-commerce/AP2/blob/v0.1.0/src/ap2/types/mandate.py).

## Authentication and discovery

- Agent card: `https://singlefax.com/.well-known/agent-card.json`.
- Merchant and trusted approval-surface signing keys: `https://singlefax.com/.well-known/ap2/jwks.json` (Ed25519, `alg: EdDSA`).
- Catalog: `GET https://singlefax.com/api/v1/commerce/products`.
- REST: bearer `sf_live_…` or `sf_test_…` API key with **credits:checkout**. The key must match the server's Stripe environment. Do not use browser cookies on the agent REST endpoints.
- A2A: `POST https://singlefax.com/a2a`, using the MCP OAuth audience and **credits:checkout** scope. Send `A2A-Extensions: https://github.com/google-agentic-commerce/AP2/tree/v0.1.0` on AP2 messages and task polling. See `/auth.md` for OAuth registration and consent.

## Purchase sequence

1. **Create a cart.** `POST /api/v1/commerce/ap2/carts`, with `Content-Type: application/json`, bearer authorization and a unique `Idempotency-Key`. Supply a product ID and an unsigned AP2 IntentMandate:

   ```json
   {
     "product_id": "credit-pack-20",
     "intent_mandate": {
       "user_cart_confirmation_required": true,
       "natural_language_description": "Buy $20 of SingleFax prepaid fax credits",
       "merchants": ["https://singlefax.com"],
       "skus": ["credit-pack-20"],
       "requires_refundability": false,
       "intent_expiry": "2026-10-11T16:00:00Z"
     }
   }
   ```

   Set `intent_expiry` to a future ISO 8601 timestamp when running the request. The cart lasts at most 15 minutes, bounded by the intent expiry. The server sets the price; the same creation key returns the same cart, and changing its request returns 409. The response contains `id`, status and `ap2.mandates.CartMandate`.

2. **Verify the cart.** Verify `merchant_authorization` as a compact JWS against the advertised JWKS, requiring `alg: EdDSA`, `typ: ap2-cart+jwt`, a known `kid`, `iss: https://singlefax.com`, `aud: https://singlefax.com/a2a`, `sub` and `jti` equal to the cart ID, and valid `iat`/`exp`. Its `cart_hash` is base64url SHA-256 of RFC 8785 canonical JSON of `CartMandate.contents`. Read the signed `payment_request.details.total`; do not substitute a client price. Credit packs have no guaranteed refund period (`refund_period: 0`); the website refund policy applies.

3. **Obtain a scoped SPT.** Use an authorized payment-credential provider such as Stripe Link. Scope the token to SingleFax's Stripe merchant profile, the exact USD total, a single purchase and a short expiration. The merchant profile is available in the signed cart's method data. SingleFax does not issue the user's wallet token. See [Stripe's seller SPT integration](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens?agent-seller=seller).

4. **Prepare the payment mandate.** `POST /api/v1/commerce/ap2/carts/{id}/prepare`, with `Idempotency-Key` and JSON containing `ap2.mandates.PaymentMandate`. At this stage omit `user_authorization`:

   ```json
   {
     "ap2.mandates.PaymentMandate": {
       "payment_mandate_contents": {
         "payment_mandate_id": "a-unique-payment-mandate-id",
         "payment_details_id": "the-returned-cart-id",
         "payment_details_total": {
           "label": "SingleFax $20 credit pack",
           "amount": { "currency": "USD", "value": 20 },
           "refund_period": 0
         },
         "payment_response": {
           "request_id": "the-returned-cart-id",
           "method_name": "https://stripe.com/shared-payment-token",
           "details": { "shared_payment_token": "spt_from_your_credential_provider" }
         },
         "merchant_agent": "https://singlefax.com/a2a",
         "timestamp": "2026-10-11T15:50:00Z"
       }
     }
   }
   ```

   Use the returned cart ID, **copy the signed total exactly**, and set the timestamp to the current time. Only one immutable payment mandate may be prepared per cart. The service saves its hash and the token hash, never the raw SPT. Changed amounts, currencies, recipients, tokens or mandate identifiers require a new cart and new approval.

5. **Ask the owner to approve.** Open the returned `approval_url`. The account owner signs in, reviews the intent, credit pack and exact price, then explicitly confirms with a registered passkey. The browser ceremony requires user presence and user verification; challenge, relying party, site origin, account ownership and credential counter are verified on the API. An agent bearer token cannot approve a purchase. The owner can decline or revoke approval before charging starts.

6. **Retrieve authorization.** `GET /api/v1/commerce/ap2/carts/{id}` returns `user_authorization` after approval. It is a base64url-encoded W3C Verifiable Presentation containing one issuer-signed JWT Verifiable Credential. This integration trusts **SingleFax's own approval surface**; third-party, self-signed and arbitrary JWKS credentials are not accepted. The JWT uses `typ: ap2-approval+jwt`, the same issuer/audience and key set, `urn:singlefax:account:{account_id}` as `sub`, an approval URN as `jti`, and the cart's expiration. Its `vc.credentialSubject.transaction_data` binds `[hash(CartMandate), hash(PaymentMandateContents)]` using RFC 8785/base64url SHA-256. A proof hash records the verified passkey ceremony. The custom credential type and terms are defined at `https://singlefax.com/.well-known/ap2/credential-context.jsonld`. This is a JWT VC profile, not an SD-JWT profile.

7. **Complete payment.** `POST /api/v1/commerce/ap2/carts/{id}/complete` with `Idempotency-Key` and **both** `ap2.mandates.CartMandate` and `ap2.mandates.PaymentMandate`. Echo the original signed cart and the exact prepared payment contents, adding the retrieved `user_authorization` to the PaymentMandate. All signatures, hashes, issuer, audience, owner, approvals and expiration are checked before initiating a charge.

8. **Poll fulfillment.** A Stripe success initially returns `processing`. `GET` returns `completed` and a signed receipt only after the credit ledger confirms the grant. If a bank requires authentication, status is `requires_action` and `approval_url` returns the owner to the payment authentication page. Only that signed-in owner receives the Stripe client secret; agent responses never contain it. A failed payment requires a new cart and explicit approval.

## A2A transport

The card exposes `ap2_create_cart`, `ap2_prepare_payment`, `ap2_complete_payment`, `ap2_get_cart` and `ap2_cancel_cart`. Use a typed data part `{ "skill": "ap2_create_cart", "arguments": { ... } }`. Creation arguments are `product_id`, `intent_mandate`, `idempotency_key`; subsequent arguments use `id` and the mandate keys described above. Get/cancel take only `id`.

Standard AP2 DataParts also work: an `ap2.mandates.IntentMandate` alongside `product_id` and `idempotency_key` creates a cart; a CartMandate and unsigned PaymentMandate prepare approval; the same pair with `user_authorization` completes payment. CartMandate outputs use `ap2.mandates.CartMandate` in the returned artifact's data part. Do not mix typed tool calls with standard mandate parts in one message.

AP2 tasks are `input-required` while awaiting a mandate, approval or bank authentication; `working` while payment/fulfillment is processing; and `completed` only after the ledger confirms credits. `GetTask` re-reads state; `CancelTask` may revoke an uncharged cart. Existing fax skills keep their hosted checkout behavior.

## Retry, cancellation and receipts

The cart is the durable payment idempotency boundary, independent of changing request headers. A journal is committed before contacting Stripe. Every retry uses the same Stripe key `ap2:{cart_id}` and approved SPT. Once a PaymentIntent ID is known, requests retrieve that payment instead of creating another. An ambiguous result without an ID stops retrying at 23 hours, before Stripe's minimum idempotency retention expires, and requires operator reconciliation. Do not start a replacement purchase for an uncertain payment outcome.

`POST /api/v1/commerce/ap2/carts/{id}/cancel` with `{}` is idempotent before charge initiation. A started payment cannot be canceled through this endpoint. Refund requests follow the website policy and cannot authorize a replacement agent payment.

Receipts are compact JWS tokens with `typ: ap2-receipt+jwt`, signed by the merchant key. They bind the cart hash, payment-mandate hash, processor payment ID, amount/currency and `status: fulfilled`. Their validity is one year. Verify the JWT signature and audience before relying on a receipt. Payment-token contents and bank-authentication secrets are excluded from receipts.

Do not send PHI in AP2, A2A or machine-payment requests. Covered Entities use HIPAA Secure Mode on the website with an electronic BAA.
