# Navigating These Docs Source: https://docs.prava.space/ai How to read these docs as a human, and how to feed them to an AI tool or agent. These docs are built for two kinds of readers: people, and the AI tools people build with. This page covers both. **Two different MCP servers.** The **docs MCP** on this page (`docs.prava.space/mcp`) lets an AI *read these docs*. The **payments MCP** (`mcp.pay.prava.space/mcp`) lets an agent actually *pay*; see [Prava MCP](/mcp/overview). ## Reading as a human Start at the [home page](/) and pick the card that matches you. Each card opens a path you can read top to bottom: | You are | Read in this order | | - | - | | Evaluating Prava | [Use Cases](/use-cases) → [How Prava Works](/concepts/how-it-works) → [Compliance](/guides/compliance) | | Building an AI app | [Choosing Your Integration](/choosing-your-integration) → [Quickstart](/quickstart) → [the tutorial](/guides/add-payments-to-your-ai-app) → [API Reference](/api-reference/overview) | | Connecting an agent | [Prava MCP](/mcp/overview) or [Prava CLI](/prava-pay/overview), then that section's pages in order | | An agent owner | [Your Prava Pay Dashboard](/prava-pay/your-wallet). One page; no code. | Stuck on a term? The [Glossary](/concepts/glossary) maps every name across the API, CLI, and dashboard. Stuck on anything else? [FAQ](/developer-faq). ## Reading as an AI tool or agent Three ways, from lightest to deepest: 1. **Paste an index**: [`llms.txt`](https://docs.prava.space/llms.txt) (page index) or [`llms-full.txt`](https://docs.prava.space/llms-full.txt) (entire docs as one file) into any LLM. 2. **Per-page**: every page has a top-right AI menu (copy as Markdown, open in ChatGPT/Claude, view raw Markdown). 3. **Connect the docs MCP** (below): your coding agent gets a live "search the Prava docs" tool and answers from current pages instead of stale training data. And when the agent should *act*, not only read: connect the [payments MCP](/mcp/connect) too. ## Connect the docs MCP server We host a standard **Model Context Protocol (MCP)** server for these docs. MCP is an open protocol, so **any MCP-compatible AI tool** can connect to the same endpoint and gain a "search the Prava docs" capability: Claude Code, Cursor, VS Code / Copilot, Windsurf, Cline, Continue, Goose, Zed, and others. Add this to your client's MCP settings: ```json theme={null} { "mcpServers": { "prava-docs": { "url": "https://docs.prava.space/mcp" } } } ``` ### Quick-add by tool ```bash Claude Code theme={null} claude mcp add --transport http prava-docs https://docs.prava.space/mcp ``` ```text Cursor / Windsurf / VS Code theme={null} MCP settings → "Add server" → paste the URL: https://docs.prava.space/mcp ``` ```text Claude (web / desktop) theme={null} Settings → Connectors → "Add custom connector" → Name: Prava Docs URL: https://docs.prava.space/mcp ``` ### Older / stdio-only clients If a client only supports local (stdio) MCP servers, bridge to the hosted endpoint with `mcp-remote`: ```bash theme={null} npx -y mcp-remote https://docs.prava.space/mcp ``` Once connected, ask your agent things like *"Using the Prava docs, how do I create a payment session?"* and it will pull the answer straight from these pages. ## Feed the docs to any LLM (llms.txt) Prefer to paste context into a model directly? These docs publish machine-readable indexes: | File | What it is | | - | - | | [`https://docs.prava.space/llms.txt`](https://docs.prava.space/llms.txt) | A concise index of every page; good for navigation. | | [`https://docs.prava.space/llms-full.txt`](https://docs.prava.space/llms-full.txt) | The full docs as one text file; paste it in for complete context. | Drop either URL (or its contents) into ChatGPT, Claude, or any LLM to ground it in Prava's documentation. ## Per-page AI actions Every page has an AI menu in the top-right: * **Copy page**: copy the page as clean Markdown. * **Open in ChatGPT / Claude**: start a chat pre-loaded with this page. * **View as Markdown**: see the raw Markdown a model would read. ## Why this matters for Prava Prava is built for AI agents. Making the docs first-class for the tools your developers already use (Cursor, Claude Code, and others) means less guesswork, fewer hallucinated API calls, and a faster path from "what's Prava?" to a working integration. # Create Session Source: https://docs.prava.space/api-reference/create-session api-reference/openapi.json POST /v1/sessions Create a payment session for a customer and order — standard checkout or mandate setup. Create a short-lived, single-use session tied to one merchant, customer, and order. The response gives you a `session_token` + `iframe_url` for the frontend — embed the iframe via the [SDK](/sdk/overview) or open the [hosted checkout](/sdk/integration-modes) directly. `POST /v1/sessions` · Authenticated with your secret key. ## Which flow? Omit `mandate_setup` (or send `intent: "checkout"`). The session collects a card and charges immediately. Include a `mandate_setup` block with `intent: "mandate_setup"`. The session is **authorize-only**: it returns an `iframe_url` for passkey approval, and completing setup issues **no** payment credentials. Charge later with [Charge a Mandate](/api-reference/mandate-charge). There is no separate `POST /v1/mandates` create endpoint — a [mandate](/concepts/mandates) is always set up through this endpoint. ## Mandate setup **Mandate creation is part of the Session APIs — this endpoint is how mandates are created.** Send a `mandate_setup` block (see [Body](#body)) with `intent: "mandate_setup"` and the session becomes authorize-only: the response carries an `iframe_url` where the owner approves **once** with their passkey. Completing setup does not issue payment credentials. Once the mandate is `active`, charge it with [Charge a Mandate](/api-reference/mandate-charge) within its approved terms — no further passkey needed. The `mandate_setup` field reference below covers `recurring_frequency`, `merchant_scope`, `max_charges`, and `valid_until`; see [Mandates](/concepts/mandates) for the concept and guardrails. **Choosing a card source.** Omit `card` to let the cardholder select or enter a card in the collection surface. Send `card.card_id` to pre-select an already-enrolled card, or `card.vault_ref_id` (a Skyflow UUID) for the vault-ref flow. Send **exactly one** — if both are present, `card_id` wins. After collection, [Get Payment Result](/api-reference/get-payment-result) returns the card bound to each transaction as `transactions[].card_id`. ## Body Unique identifier for the customer in your system (1–255 chars). Required for merchant (secret-key) sessions — omitting it returns `400 VAL_2001`. Customer email. Required for merchant sessions. Must be a valid email address. Total order amount as a decimal string with up to 2 decimals, e.g. `"49.99"`. Need not equal the sum of line items (it may include tax, shipping, and fees). This value becomes the authorized amount cap. ISO 4217 code — 3 uppercase letters — and must be a **supported currency**. Supported: USD, EUR, GBP, INR, CAD, AUD, JPY, SGD, AED, HKD, MXN, BRL, CHF, CNY, NZD, SEK, NOK, DKK, ZAR, THB, KRW, PLN, TWD, PHP, IDR, MYR, CZK, ILS, CLP, ARS, COP, PEN, SAR, QAR, KWD, BHD, OMR, EGP, NGN, KES, GHS, TZS, UGX, PKR, BDT, LKR, VND, MMK, NPR. Other codes are rejected. This is your **pricing currency** and is independent of the card's issuing country: there is no requirement that it match `country_code_iso2` (the merchant's location) or the cardholder's country. A US-issued card can pay an INR-denominated session; the card network settles the cross-currency conversion. Price in whatever currency your product uses — you don't need to re-denominate. What is being bought. Two modes — pick **one**: * **From a quote** — send `quote: true` and the opaque `quote_id` (`qte_…`) returned when the quote was created. Merchant and product details come from that quote, so nothing else is needed. * **Custom** — omit `quote` (or send `false`) and describe the merchant and products yourself in `custom`. Set `true` to build the session from an existing quote. When `true`, pass `quote_id` and nothing else — `custom` must be omitted. When `false` or absent, `custom` is required and `quote_id` must be omitted. **Required when `quote` is `true`.** The opaque public quote id returned by quote creation (for example, `qte_…`). The quote carries the merchant, products, and locked price. Create the session before the quote expires; its expiry is separate from the session's lifetime. `total_amount` and `currency` must match the quote's final total. Do not send a merchant checkout-session id. **Required when `quote` is not `true`.** Exactly one entry — multi-merchant sessions are not yet supported. The **destination merchant** — the business the cardholder is buying from, **not your application**. The `name` renders as the header on the checkout page and is forwarded to Visa as the merchant of record for the token. Merchant display name. Sanitized to a Visa-safe character set (e.g. `H&M` → `HM`); must contain at least one usable character. Merchant website URL. **Must use `https`** — the URL is forwarded to Visa. Merchant country — 2 uppercase ISO 3166-1 letters, e.g. `"US"`. Merchant category code (MCC). Free-form string, max 10 characters. Merchant category description, max 100 characters. At least one product. Product description. Unit price as a string, e.g. `"24.99"`. Your external product identifier, max 50 characters. Quantity of this product. Positive integer. How long the resulting mandate stays effective, in minutes. Positive integer. `full_checkout` (hosted redirect) or `embedding` (mount the iframe via the SDK). Read by the frontend to choose the surface. For hosted checkout: the URL Prava redirects the cardholder to after they finish. **Must use `https`**, max 2048 chars. Optional for all integration types. Pre-select a card. Send one of the two fields below (see the note above). An enrolled card id (the `enrollmentId` from the SDK's [`collectPAN()`](/sdk/cards/collect-pan), or a `card_id` from [List Cards](/api-reference/list-cards)). Scoped to this customer + merchant. A Skyflow vault reference UUID. Triggers provisioning at session-create. Turns the session into mandate setup (authorize-only). See the Mandate setup tab above. `mandate_setup` (authorize-only, no charge) or `checkout`. Defaults to `mandate_setup` when this block is present. `one_time`, `weekly`, `monthly`, or `yearly`. `listed` (locked to this merchant) or `any` (any merchant). Recurring frequencies force `listed` — sending `any` with a recurring frequency returns `400 MANDATE_RECURRING_MUST_BE_SCOPED`. ISO 8601 datetime. **Ignored for `one_time`**, which is always clamped to 7 days. Recurring horizons: yearly 5y, monthly 2y, weekly 1y. Maximum number of charges allowed against this mandate. Positive integer. (The per-charge *amount* cap is enforced separately, at the card network.) Customer phone number. Customer country — 2 uppercase ISO 3166-1 letters. Your external order reference id, max 255 chars. Human-readable description of the order. Optional, sandbox-only payment success simulation for quote-backed checkout. The exact shape is `{ "stage": "payment", "outcome": "success" }`; both fields are required and additional fields are rejected. Custom checkout, production, and mandate-setup sessions cannot use it. Normal card collection and verification still run. An eligible terminal payment result can complete the sandbox lifecycle while `observed_result` retains the actual outcome. See [Sandbox simulation](/api-reference/testing#simulate-successful-payment-completion). ## Response Unique session identifier. JWT authenticating client-side SDK calls within this session. Secure card-collection (or, for mandate setup, passkey-approval) surface. Internal order identifier. ISO 8601 timestamp when the session expires. ## Notes * Sessions are **short-lived** and **single-use**, tied to a specific merchant, customer, and order. * `session_token` and `iframe_url` are used on the frontend: embedded via the [SDK](/sdk/overview), or opened directly for [hosted checkout](/sdk/integration-modes). * Sessions can be revoked before use via [`POST /v1/sessions/:id/revoke`](/api-reference/revoke-session). ## Error responses | Status | Code | Cause | | - | - | - | | 400 | `VAL_2001` | Validation error (missing/invalid fields; `user_id`/`user_email` missing on a merchant session; `quote: true` without a `quote_id`; `custom` missing when `quote` is not true). `details` names the fields. | | 400 | `CARD_NOT_FOUND` | Pre-selected `card_id` does not exist for this customer + merchant | | 400 | `CARD_INACTIVE` | Pre-selected card is not active | | 400 | `MANDATE_RECURRING_MUST_BE_SCOPED` | Recurring frequency sent with `merchant_scope: "any"` | | 400 | `SANDBOX_SIMULATION_NOT_ALLOWED` | Simulation requested outside sandbox, for a custom checkout, or for mandate setup | | 401 | `AUTH_1001` | Invalid API key | | 401 | `AUTH_1002` | Missing or invalid Authorization header | | 409 | `QUOTE_SESSION_MISMATCH` | Session amount or currency differs from the quote's final total | | 410 | `QUOTE_EXPIRED` | The quote expired before session creation | | 429 | `TRIES_EXHAUSTED` | Sandbox test-transaction limit reached for this merchant | | 500 | `MERCHANT_LOOKUP_ERROR` | Failed to load the merchant account for the key | | 500 | `CONFIG_ERROR` | Server-side configuration issue (Visa or Skyflow not configured) | ```bash cURL theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "user_id": "user_123", "user_email": "jane@example.com", "total_amount": "49.99", "currency": "USD", "purchase_context": { "custom": [{ "merchant_details": { "name": "Acme Store", "url": "https://acme.example.com", "country_code_iso2": "US" }, "product_details": [{ "description": "Widget", "unit_price": "24.99", "quantity": 2 }] }] }, "integration_type": "full_checkout", "callback_url": "https://acme.example.com/return" }' ``` ```bash cURL · From a quote theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "user_id": "user_123", "user_email": "jane@example.com", "total_amount": "22.45", "currency": "USD", "purchase_context": { "quote": true, "quote_id": "qte_9f2c..." } }' ``` ```python Python theme={null} import requests resp = requests.post( "https://sandbox.api.prava.space/v1/sessions", headers={"Authorization": "Bearer sk_test_..."}, json={ "user_id": "user_123", "user_email": "jane@example.com", "total_amount": "49.99", "currency": "USD", "purchase_context": { "custom": [{ "merchant_details": {"name": "Acme Store", "url": "https://acme.example.com", "country_code_iso2": "US"}, "product_details": [{"description": "Widget", "unit_price": "24.99", "quantity": 2}], }], }, "integration_type": "full_checkout", "callback_url": "https://acme.example.com/return", }, ) ``` ```javascript JavaScript theme={null} const resp = await fetch("https://sandbox.api.prava.space/v1/sessions", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ user_id: "user_123", user_email: "jane@example.com", total_amount: "49.99", currency: "USD", purchase_context: { custom: [{ merchant_details: { name: "Acme Store", url: "https://acme.example.com", country_code_iso2: "US" }, product_details: [{ description: "Widget", unit_price: "24.99", quantity: 2 }], }], }, integration_type: "full_checkout", callback_url: "https://acme.example.com/return", }), }); ``` ```bash cURL · Mandate setup theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "user_id": "user_123", "user_email": "jane@example.com", "total_amount": "40.00", "currency": "USD", "purchase_context": { "custom": [{ "merchant_details": { "name": "Acme Store", "url": "https://acme.example.com", "country_code_iso2": "US" }, "product_details": [{ "description": "Monthly plan", "unit_price": "40.00" }] }] }, "mandate_setup": { "intent": "mandate_setup", "recurring_frequency": "monthly", "merchant_scope": "listed", "max_charges": 12 } }' ``` ```json 201 Standard theme={null} { "session_id": "sess_9f2c...", "session_token": "eyJhbGciOi...", "iframe_url": "https://checkout.prava.space/s/sess_9f2c...", "order_id": "ord_4a1b...", "expires_at": "2026-07-26T12:15:00Z" } ``` ```json 201 Mandate setup theme={null} { "session_id": "sess_7d1a...", "session_token": "eyJhbGciOi...", "iframe_url": "https://checkout.prava.space/s/sess_7d1a...", "order_id": "ord_88c2...", "expires_at": "2026-07-26T12:15:00Z" } ``` ```json 400 Validation theme={null} { "error": { "code": "VAL_2001", "message": "Validation failed", "details": { "currency": "Unsupported currency code" } } } ``` ```json 401 Unauthorized theme={null} { "error": { "code": "AUTH_1001", "message": "Invalid API key" } } ``` # Delete Card Source: https://docs.prava.space/api-reference/delete-card api-reference/openapi.json POST /v1/deleteCard Delete a customer's enrolled card and retire its network token Delete an enrolled card. Deletion also retires the card's network token (the tokenized stand-in for the card at the card network) where one exists. `POST /v1/deleteCard` · Authenticated with your secret key. ## Body The customer whose card to delete (the `user_id` used at session creation). The card to delete — the same identifier as the `enrollmentId` from the SDK's [`collectPAN`](/sdk/cards/collect-pan) and the `card_id` in [List Cards](/api-reference/list-cards). Why the card is being deleted: `CUSTOMER_CONFIRMED`, `LOST`, `STOLEN`, `SUSPECTED_FRAUD`, `CLOSED_ACCOUNT`, or `OTHER`. ## Response Whether the card was deleted. The deleted card id. Whether the deleted card was the customer's default. If `true`, the customer picks a new default on their next payment. Whether a card-network token was retired as part of the deletion. ## Error responses | Status | Code | Cause | Resolution | | - | - | - | - | | 401 | `AUTH_1001` | Invalid API key | Check your secret key | | 400 | `INVALID_REQUEST` | Missing `customer_id` or `card_id` | Include both fields | | 404 | `CUSTOMER_NOT_FOUND` | No customer for the given identifier | Verify `customer_id` | | 404 | `NOT_FOUND` | Card not found | Verify `card_id` via [List Cards](/api-reference/list-cards) | | 502 | `NETWORK_DELETE_FAILED` | Card-network deletion failed | Retry the request | ```bash cURL theme={null} curl -X POST https://sandbox.api.prava.space/v1/deleteCard \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "customer_id": "user_123", "card_id": "card_1", "reason": "CUSTOMER_CONFIRMED" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://sandbox.api.prava.space/v1/deleteCard", headers={"Authorization": "Bearer sk_test_..."}, json={"customer_id": "user_123", "card_id": "card_1", "reason": "CUSTOMER_CONFIRMED"}, ) ``` ```javascript JavaScript theme={null} const resp = await fetch("https://sandbox.api.prava.space/v1/deleteCard", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ customer_id: "user_123", card_id: "card_1", reason: "CUSTOMER_CONFIRMED", }), }); ``` ```json 200 Success theme={null} { "success": true, "card_id": "card_1", "was_default": false, "network_token_deleted": true } ``` ```json 400 Validation theme={null} { "error": { "code": "INVALID_REQUEST", "message": "Missing customer_id or card_id" } } ``` ```json 404 Not Found theme={null} { "error": { "code": "NOT_FOUND", "message": "Card not found" } } ``` # Enroll Card Source: https://docs.prava.space/api-reference/enroll-card api-reference/openapi.json POST /v1/sessions/enroll-card Provision a card only, or run the complete card-and-passkey enrollment flow without creating a payment. Register a card for a merchant-scoped customer without creating an order, transaction, mandate, cryptogram, or payment credential. `POST /v1/sessions/enroll-card` · Authenticated with your secret key. This endpoint has two deliberately different contracts. `provision_only` is a synchronous, server-to-server operation. `full_enrollment` creates an interactive session that the cardholder completes on Prava's secure surface. ## Choose the request shape | Goal | `enrollment_mode` | `vault_ref_id` | Cardholder UI | Response | Poll status | | - | - | - | - | - | - | | Provision an already-vaulted card | Omit or `provision_only` | Required | None | `card_id` | No | | Full enrollment with card entry | `full_enrollment` | Omit | Embedded or hosted | Session artifacts | Yes | | Full enrollment with an already-vaulted card | `full_enrollment` | Include | Embedded or hosted passkey flow | Session artifacts | Yes | Do not send iframe-only fields with `provision_only`. The request schemas are strict: `integration_type` and `callback_url` are accepted only with `full_enrollment`, and unknown fields return `400 VAL_2001`. ## Request examples by use case Choose the example that matches both where the card comes from and how Prava's secure surface should open. ```bash Provision only theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions/enroll-card \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "user_id": "customer_123", "user_email": "customer@example.com", "vault_ref_id": "0b92f9d9-16ca-4a8e-ae75-2a4061cda90c", "consent_id": "consent_123" }' ``` ```bash Embedded + card entry theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions/enroll-card \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "enrollment_mode": "full_enrollment", "user_id": "customer_123", "user_email": "customer@example.com", "integration_type": "embedding", "consent_id": "consent_123" }' ``` ```bash Embedded + vault reference theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions/enroll-card \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "enrollment_mode": "full_enrollment", "user_id": "customer_123", "user_email": "customer@example.com", "vault_ref_id": "0b92f9d9-16ca-4a8e-ae75-2a4061cda90c", "integration_type": "embedding", "consent_id": "consent_123" }' ``` ```bash Hosted + card entry theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions/enroll-card \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "enrollment_mode": "full_enrollment", "user_id": "customer_123", "user_email": "customer@example.com", "integration_type": "full_checkout", "callback_url": "https://merchant.example/card-setup/complete", "consent_id": "consent_123" }' ``` ```bash Hosted + vault reference theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions/enroll-card \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "enrollment_mode": "full_enrollment", "user_id": "customer_123", "user_email": "customer@example.com", "vault_ref_id": "0b92f9d9-16ca-4a8e-ae75-2a4061cda90c", "integration_type": "full_checkout", "callback_url": "https://merchant.example/card-setup/complete", "consent_id": "consent_123" }' ``` ```json Provision-only response theme={null} { "card_id": "card_123" } ``` ```json Full-enrollment response theme={null} { "session_id": "ses_123", "session_token": "eyJhbGciOi...", "expires_at": "2026-09-05T12:30:00.000Z", "iframe_url": "https://sandbox.collect.prava.space/?session=ses_123" } ``` ## Body ### Common fields Your stable identifier for this customer. It is scoped to the merchant account authenticated by your secret key. Must be non-blank and no longer than 255 characters. Customer email, maximum 320 characters. It is optional only when Prava already has an email for this merchant's `user_id`; otherwise the request returns `400 ENROLL_USER_EMAIL_REQUIRED`. Optional reference to the consent record in your system, maximum 255 characters. When present it must be non-blank and contain no control characters. Prava records whether this reference was supplied; it does not independently validate the underlying consent. `provision_only` or `full_enrollment`. Omitting the field preserves the existing `provision_only` behavior. ### Mode-specific fields Skyflow record UUID created using your configured card-collection credentials. Required for `provision_only`. Optional for `full_enrollment`: include it to skip card entry, or omit it to let the cardholder enter a card in Prava's iframe. Full enrollment only. Use `embedding` to mount the secure surface inside your application, or `full_checkout` to open the Prava-hosted flow. Full enrollment only. Optional HTTPS URL, maximum 2048 characters. In hosted mode Prava redirects the cardholder here after completion. If omitted, the final screen remains visible and tells the cardholder they may safely close it. ## Provision only Use this mode when you already have a valid `vault_ref_id` and only need the card provisioned. The call completes synchronously and returns the merchant-scoped card identifier. Do not call the enrollment-status endpoint for this mode. The `201` response is the result. ## Full enrollment Full enrollment makes the card and passkey ready for later use. It creates an orderless session and returns the same client artifacts used by other Prava interactive surfaces: Identifier used when polling enrollment status. Short-lived token passed to the SDK for iframe authentication. ISO 8601 expiry. Treat this returned value as authoritative. Secure Prava URL. Pass or open it exactly as returned. There is no `order_id`, transaction identifier, payment token, dynamic CVV, or cryptogram in this response. Omit `vault_ref_id`. The cardholder enters the card on Prava's secure surface, then completes passkey setup or verifies an existing passkey. ```json theme={null} { "enrollment_mode": "full_enrollment", "user_id": "customer_123", "user_email": "customer@example.com", "integration_type": "embedding", "consent_id": "consent_123" } ``` Include `vault_ref_id`. Card entry is skipped, but the interactive surface still opens so the cardholder can complete passkey setup or verification. ```json theme={null} { "enrollment_mode": "full_enrollment", "user_id": "customer_123", "user_email": "customer@example.com", "vault_ref_id": "0b92f9d9-16ca-4a8e-ae75-2a4061cda90c", "integration_type": "full_checkout", "callback_url": "https://merchant.example/card-setup/complete" } ``` ## Integration types Set `integration_type: "embedding"`, return the session artifacts from your backend to your frontend, and mount the returned `iframe_url` using the SDK. ```typescript theme={null} import { PravaSDK } from '@prava-sdk/core'; const prava = new PravaSDK({ publishableKey: 'pk_test_...' }); void prava.collectPAN({ sessionToken: session.session_token, iframeUrl: session.iframe_url, container: '#card-setup', // Card provisioning completed. Full enrollment may still be in progress. onSuccess: (card) => console.log(card.enrollmentId), // Card provisioning and passkey readiness are both complete. onEnrollmentComplete: (result) => { console.log(result.card_id, result.passkey); }, onEnrollmentCancelled: ({ card_id }) => { console.log('Enrollment cancelled', card_id); }, onError: (error) => console.error(error.code, error.message), }); ``` Keep the SDK mounted after `onSuccess`. For a full-enrollment session, completion is signaled by `onEnrollmentComplete`, and your backend should confirm it through [Get Card Enrollment Status](/api-reference/get-card-enrollment-status). Use `full_checkout` (the default) and redirect or open the returned URL. No frontend SDK is required. ```javascript theme={null} window.location.href = session.iframe_url; ``` With `callback_url`, Prava redirects after completion. Without one, Prava keeps the completion result visible and tells the cardholder they can safely close the window. In both cases your backend determines the result by polling the status endpoint. ## Complete integration sequence Call this endpoint with your secret key. Never expose the secret key in browser code. Embed or navigate to `iframe_url`. Pass it exactly as returned; it already contains the `session_id`. Poll `GET /v1/sessions/{sessionId}/status`. Do not send your secret key from the browser. Enrollment succeeded only when `status` is `completed` and `outcome` is `fully_enrolled`. Retries happen inside the same iframe. If the status response contains `error.retryable: true`, do not create a new session and do not call a retry endpoint. Keep the current iframe available while the cardholder selects **Try again**; Prava reuses the already selected or provisioned card. ## Error responses | Status | Code | Cause | Recovery | | - | - | - | - | | 400 | `VAL_2001` | Missing, incompatible, unknown, or invalid request field | Correct the request using the mode rules above | | 400 | `ENROLL_USER_EMAIL_REQUIRED` | No email was supplied or previously stored for this `user_id` | Supply `user_email` | | 400 | `BIN_COUNTRY_NOT_ALLOWED` / `BIN_ISSUER_NOT_ALLOWED` | Card is not eligible for provisioning | Ask the cardholder for another eligible card | | 401 | `AUTH_1001` / `AUTH_1002` | Missing or invalid secret key | Use the correct environment's secret key | | 429 | Rate limited | Too many enrollment requests | Back off before retrying | | 500 | `MERCHANT_LOOKUP_ERROR` / `CONFIG_ERROR` | Merchant configuration could not be resolved | Contact support with the `X-Response-ID` | Full-enrollment failures that occur after the session is created are returned through the status resource, not by changing this create response. Every response includes an `X-Response-ID` header. Include it when contacting [support@prava.space](mailto:support@prava.space). # Errors Source: https://docs.prava.space/api-reference/errors Every error code the Prava API can return — status, cause, and how to recover. All errors share one envelope: ```json theme={null} { "error": { "code": "ERROR_CODE", "message": "Human-readable error message", "details": {} } } ``` `details` is optional; it's included for validation errors to specify which fields failed. Every response also carries an `X-Response-ID` header; include it when contacting [support@prava.space](mailto:support@prava.space). ## Authentication & validation These can occur on **any** endpoint: | Status | Code | Cause | Recovery | | - | - | - | - | | 401 | `AUTH_1001` | Invalid or missing API key | Use `Authorization: Bearer sk_…` with a valid secret key for the right environment | | 401 | `AUTH_1002` | Missing or invalid Authorization header | Include the header exactly as `Bearer sk_…` | | 401 | `AUTH_1003` | Session has expired | Create a new session | | 401 | `AUTH_1004` | Session has been revoked | Create a new session | | 400 | `VAL_2001` | Request failed schema validation | Check `details` for the failing fields | | 400 | `INVALID_REQUEST` | A required field is missing (e.g. `customer_id`, `card_id`) | Include the named field | ## Create Session — `POST /v1/sessions` | Status | Code | Cause | Recovery | | - | - | - | - | | 429 | `TRIES_EXHAUSTED` | Your account's session allowance is depleted | Contact [support@prava.space](mailto:support@prava.space) | | 500 | `MERCHANT_LOOKUP_ERROR` | Your merchant account couldn't be resolved | Retry; if persistent, contact support with the `X-Response-ID` | | 500 | `CONFIG_ERROR` | Merchant account is missing payment configuration | Contact support; this is an account-setup issue, not a request problem | | 400 | `CARD_NOT_FOUND` | A pre-selected card doesn't exist | Verify via [List Cards](/api-reference/list-cards) | | 400 | `CARD_INACTIVE` | A pre-selected card is not active | Choose an active card | | 500 | `SESSION_CREATE_ERROR` | Internal failure creating the session | Retry; then support | ## Payment Result — `GET /v1/sessions/{sessionId}/payment-result` Quote checkout failures are polling outcomes, so the endpoint still returns HTTP `200` with top-level `status: "failed"` and may include one of these `error.code` values. | Status | Code | Cause | Recovery | | - | - | - | - | | 200 | `CARD_DECLINED` | The issuer declined the payment | Ask the cardholder to check with their issuer or use another card | | 200 | `INSUFFICIENT_FUNDS` | The card has insufficient funds | Use another funding source | | 200 | `INVALID_CARD_NUMBER` / `INVALID_SECURITY_CODE` / `INVALID_EXPIRY_DATE` | Card details are invalid | Ask the cardholder to correct the relevant details | | 200 | `EXPIRED_CARD` | The card has expired | Use a valid card | | 200 | `BILLING_DETAILS_MISMATCH` | Billing details could not be verified | Check the billing details | | 200 | `CARD_NOT_SUPPORTED` / `PAYMENT_METHOD_UNAVAILABLE` | The selected payment method cannot be used | Choose another supported payment method | | 200 | `AUTHENTICATION_FAILED` | Payment authentication failed | Review the failure before starting another attempt | | 200 | `PAYMENT_FAILED` | Generic payment failure; no recognized canonical detail is available | Review the result before retrying | | 200 | `MERCHANT_PREPARE_FAILED` | The merchant checkout could not be prepared | Create a fresh quote and session | | 200 | `MERCHANT_ACTION_REQUIRED` | The merchant requires additional verification | Contact support or complete the required merchant action | | 404 | `NOT_FOUND` | Session not found, or it belongs to another merchant | Verify the session ID and which key created it | | 409 | `PAYMENT_RESULT_UNSUPPORTED` | The session is an enrollment-only flow | Use [Get Card Enrollment Status](/api-reference/get-card-enrollment-status) | Read canonical failure details from the top-level `error`. When merchant lifecycle data exists, `merchant_res` contains only `status` and optional `orderId`. For an explicitly simulated sandbox result, the actual failure can appear under `observed_result.error` while the effective top-level status is `completed` and `merchant_res.status` is `paid`. At session creation, `400 SANDBOX_SIMULATION_NOT_ALLOWED` means simulation was requested for production, custom checkout, or mandate setup. Only the exact payment-stage success shape is accepted; malformed shapes return a validation error. See [Sandbox simulation](/api-reference/testing#simulate-successful-payment-completion). ## Shopping checkout address errors | Status | Code | Cause | Recovery | | - | - | - | - | | 422 | `DELIVERY_UNAVAILABLE_FOR_ADDRESS` | The merchant cannot deliver the selected items to the supplied address | Ask the shopper to choose another delivery address or item; retrying the same item and address will not help | | 422 | `INVALID_SHIPPING_REGION` | The supplied state or region is unavailable for this merchant checkout | Choose a region accepted by the merchant for the destination address | | 422 | `INVALID_PHONE_NUMBER` | The merchant rejected the buyer's phone number | Correct the phone number for the destination country before trying again | An apartment or unit-number suggestion is a non-blocking quote warning, `ADDRESS_UNIT_CONFIRMATION_SUGGESTED`, rather than an error. The shopper can confirm or add a unit number while keeping the quote usable. ## Card Enrollment — `POST /v1/sessions/enroll-card` · `GET /v1/sessions/{sessionId}/status` | Status | Code | Cause | Recovery | | - | - | - | - | | 400 | `ENROLL_USER_EMAIL_REQUIRED` | No email was supplied or previously stored for this merchant's `user_id` | Supply `user_email` | | 400 | `BIN_COUNTRY_NOT_ALLOWED` / `BIN_ISSUER_NOT_ALLOWED` | The card is not eligible for provisioning | Ask the cardholder for another eligible card | | 404 | `NOT_FOUND` | Session not found, or it belongs to another merchant | Verify the session ID, environment, and key | | 409 | `SESSION_STATUS_UNSUPPORTED` | The status endpoint received a payment or another unsupported session | Use that flow's result endpoint | | 409 | `ENROLLMENT_ACTION_NOT_FOUND` | The enrollment session is missing its linked action | Contact support with the `X-Response-ID` | Failures after a `full_enrollment` session is created normally use HTTP `200` with the stable status body. When `error.retryable` is true, the cardholder retries inside the same iframe; the merchant does not call another API or create another session. ## Report Status — `POST /v1/sessions/{sessionId}/report-status` | Status | Code | Cause | Recovery | | - | - | - | - | | 404 | `NOT_FOUND` | Session / order / transaction reference not found | Verify the session ID and `txn_ref_id` | | 400 | `INVALID_STATE` | No transaction awaiting a result | It may already be reported, or the cardholder hasn't finished | | 400 | `MANDATE_EXPIRED` | The mandate has expired | Create a new session | | 400 | `PRODUCT_NOT_FOUND` | Product not found by the given ID | Verify `product_id` / `product_ref_id` | | 409 | `PAYMENT_AMOUNT_MISMATCH` | The reported total or product amount differs from the amount stored for the transaction | Report the exact persisted amount; decimal-equivalent values such as `25` and `25.00` are accepted | | 409 | `QUOTE_PAYMENT_CONFLICT` | The report conflicts with the payment attempt or outcome already bound to the quote | Stop submitting the conflicting report and retrieve the authoritative payment result | | 502 | `VISA_CONFIRMATION_PENDING` | The payment outcome is saved, but card-network confirmation has not completed | Preserve the saved outcome and retry according to your normal reporting policy | | 502 | `VISA_CONFIRMATION_RECONCILIATION_REQUIRED` | Automatic card-network confirmation attempts are exhausted | Preserve the saved outcome and contact support with the `X-Response-ID`; do not submit a different outcome | | 502 | `VISA_CONFIRMATION_FAILED` | Card-network confirmation failed | Retry; then support | | 500 | `REPORT_STATUS_ERROR` | Internal error | Contact support with the `X-Response-ID` | ## Cards — `GET /v1/listCards` · `POST /v1/deleteCard` | Status | Code | Cause | Recovery | | - | - | - | - | | 404 | `CUSTOMER_NOT_FOUND` | No customer for the given identifier | Verify `customer_id` (the `user_id` you used at session creation) | | 404 | `NOT_FOUND` | Card not found (delete) | Verify `card_id` via [List Cards](/api-reference/list-cards) | | 502 | `NETWORK_DELETE_FAILED` | Card-network deletion failed (delete) | Retry | ## Revoke Session — `POST /v1/sessions/{id}/revoke` | Status | Code | Cause | Recovery | | - | - | - | - | | 404 | `NOT_FOUND` | Session not found | Verify the session ID | | 400 | `INVALID_STATE` | Session not in a revocable state | It may already be completed or expired | ## Mandates — `/v1/mandates/*` Mandate routes authenticate with a secret key **or** an agent Ed25519 signature; a missing/invalid credential returns `401 AUTH_REQUIRED`. | Status | Code | Cause | Recovery | | - | - | - | - | | 401 | `AUTH_REQUIRED` | Missing or invalid credentials | Send a valid secret key or agent signature | | 403 | `MANDATE_FORBIDDEN` | The caller doesn't own this mandate | Act only on mandates owned by the calling agent/merchant | | 403 | `MANDATE_MERCHANT_NOT_ALLOWED` | A charge fell outside a `listed` mandate's merchant | Charge only at the mandate's merchant, or use an `any`-scope mandate | | 404 | `MANDATE_NOT_FOUND` | No mandate with that id for the caller | Verify the mandate id via [List Mandates](/api-reference/mandate-list) | | 409 | `MANDATE_NOT_ACTIVE` | Charged a mandate that isn't `active` (e.g. paused, consumed, expired) | Resume it, or set up a new mandate | | 409 | `MANDATE_INVALID_TRANSITION` | An illegal lifecycle change (e.g. resume a cancelled mandate) | Only pause `active`, resume `paused`, cancel `active`/`paused` | | 409 | `NO_INSTRUCTION` / `NO_ORDER` | The mandate is missing its underlying instruction/order (charge) | Re-run mandate setup | | 500 | `NO_TOKEN` | Credential minting failed (charge) | Retry; then support | Reporting a charge (`/charges/{txnId}/report`) can additionally return `404 CHARGE_NOT_FOUND`, `409 CHARGE_NOT_REPORTABLE` / `CHARGE_NO_TLI`, `502 VISA_CONFIRMATION_FAILED`, or `500 MANDATE_CHARGE_REPORT_DB_FAILED`. `THRESHOLD_EXCEEDED` is **not** a Prava error code — it's a Visa decline (an over-cap charge) surfaced in a failed charge's `errorCode` / `errorMessage`, not in the error envelope. See [Charge a Mandate](/api-reference/mandate-charge). The **SDK** has its own client-side error codes (`SDK_ALREADY_ACTIVE`, `INVALID_CONFIG`, `IFRAME_LOAD_ERROR`, `SDK_INIT_ERROR`); see [Collect Card Details](/sdk/cards/collect-pan). CLI errors are mapped on the [Prava Pay troubleshooting page](/prava-pay/troubleshooting). # Get Card Enrollment Status Source: https://docs.prava.space/api-reference/get-card-enrollment-status api-reference/openapi.json GET /v1/sessions/{sessionId}/status Poll the outcome of a full card-enrollment session without exposing payment credentials. Poll the lifecycle of a `full_enrollment` session created by [Enroll Card](/api-reference/enroll-card). This endpoint is server-to-server and returns no payment token, dynamic CVV, cryptogram, order, or transaction. `GET /v1/sessions/{sessionId}/status` · Authenticated with the same merchant secret key that created the session. This endpoint supports only `full_enrollment` card sessions. Payment sessions must use [Get Payment Result](/api-reference/get-payment-result); calling the wrong status resource returns `409`. ## Path parameters The `session_id` returned by `POST /v1/sessions/enroll-card` when `enrollment_mode` is `full_enrollment`. ## Response The JSON shape is stable across pending work, recoverable failures, success, cancellation, and expiry. Enrollment session identifier. Session lifecycle: `requires_action`, `processing`, `completed`, `failed`, `cancelled`, or `expired`. Durable work completed: `not_started`, `card_provisioned`, or `fully_enrolled`. Merchant-scoped card identifier. It becomes non-null after card provisioning succeeds, even if later passkey work is incomplete or fails. `registered` when this flow created a passkey, `existing` when it verified an already available passkey, or `null` before passkey completion. `provided` when the create request included `consent_id`; otherwise `not_provided`. This reports reference presence, not independent proof of consent. Safe failure information: `code`, `message`, and `retryable`. It is `null` when no failure is active and after completion or cancellation. ## How to interpret status and outcome | `status` | Typical `outcome` | Meaning | Integrator action | | - | - | - | - | | `requires_action` | `not_started` | The cardholder must continue in the iframe | Keep the iframe open and continue polling | | `requires_action` | `card_provisioned` | The card is saved; passkey work still needs user action | Keep the same iframe and card | | `processing` | `not_started` or `card_provisioned` | Prava or the card network is processing | Continue polling | | `completed` | `fully_enrolled` | Card enrollment and passkey readiness are complete | Stop polling and store `card_id` | | `failed` | `not_started` or `card_provisioned` | A non-recoverable step failed | Stop polling; inspect how much completed | | `cancelled` | `not_started` or `card_provisioned` | The cardholder cancelled | Stop polling | | `expired` | `not_started` or `card_provisioned` | The session expired before completion | Stop polling and create a new session if the user wants to continue | Enrollment success is exactly `status: "completed"` together with `outcome: "fully_enrolled"`. Do not infer full enrollment from a non-null `card_id`: `card_id` proves only that card provisioning completed. ## Retry behavior When `error.retryable` is `true`, the cardholder retries inside the existing iframe: * Do not call a retry API—there is no public retry endpoint. * Do not create another enrollment session. * Do not ask the cardholder to select or enter the card again after `card_id` exists. * Keep polling this same `session_id` while the cardholder selects **Try again**. Prava retains the selected/provisioned card and restarts only the recoverable enrollment step. Incorrect OTP attempts remain inside the card-network experience and do not require merchant-side state. ## Examples ```bash cURL theme={null} curl "https://sandbox.api.prava.space/v1/sessions/ses_123/status" \ -H "Authorization: Bearer sk_test_..." ``` ```python Python theme={null} import requests response = requests.get( "https://sandbox.api.prava.space/v1/sessions/ses_123/status", headers={"Authorization": "Bearer sk_test_..."}, ) enrollment = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch( `https://sandbox.api.prava.space/v1/sessions/${sessionId}/status`, { headers: { Authorization: `Bearer ${process.env.PRAVA_SECRET_KEY}`, }, }, ); const enrollment = await response.json(); ``` ```json 200 Awaiting cardholder theme={null} { "session_id": "ses_123", "status": "requires_action", "outcome": "not_started", "card_id": null, "passkey": null, "consent_status": "not_provided", "error": null } ``` ```json 200 Recoverable failure after provisioning theme={null} { "session_id": "ses_123", "status": "requires_action", "outcome": "card_provisioned", "card_id": "card_123", "passkey": null, "consent_status": "provided", "error": { "code": "DEVICE_BINDING_FAILED", "message": "Secure verification could not be completed. Try again.", "retryable": true } } ``` ```json 200 Completed with a new passkey theme={null} { "session_id": "ses_123", "status": "completed", "outcome": "fully_enrolled", "card_id": "card_123", "passkey": "registered", "consent_status": "provided", "error": null } ``` ```json 200 Completed with an existing passkey theme={null} { "session_id": "ses_456", "status": "completed", "outcome": "fully_enrolled", "card_id": "card_456", "passkey": "existing", "consent_status": "not_provided", "error": null } ``` ```json 200 Terminal failure before provisioning theme={null} { "session_id": "ses_789", "status": "failed", "outcome": "not_started", "card_id": null, "passkey": null, "consent_status": "not_provided", "error": { "code": "BIN_COUNTRY_NOT_ALLOWED", "message": "Card is not eligible", "retryable": false } } ``` ## Polling example Poll from your backend, not the browser. A practical starting cadence is once per second while the cardholder is active, backing off toward five seconds during longer processing. The endpoint is limited to 120 requests per minute. ```typescript theme={null} const terminal = new Set(['completed', 'failed', 'cancelled', 'expired']); async function waitForEnrollment(sessionId: string) { let delayMs = 1_000; while (true) { const response = await fetch( `https://sandbox.api.prava.space/v1/sessions/${sessionId}/status`, { headers: { Authorization: `Bearer ${process.env.PRAVA_SECRET_KEY}`, }, }, ); if (!response.ok) throw new Error(`Status request failed: ${response.status}`); const result = await response.json(); if (terminal.has(result.status)) return result; await new Promise((resolve) => setTimeout(resolve, delayMs)); delayMs = Math.min(delayMs * 1.5, 5_000); } } ``` ## Error responses Enrollment lifecycle failures use HTTP `200` with the stable response above. HTTP errors mean the status request itself could not be served. | Status | Code | Cause | Recovery | | - | - | - | - | | 401 | `AUTH_1001` / `AUTH_1002` | Missing or invalid secret key | Use the key that created the session | | 404 | `NOT_FOUND` | Session not found or owned by another merchant | Verify the session and environment | | 409 | `SESSION_STATUS_UNSUPPORTED` | The session is a payment or another unsupported flow | Use that flow's result endpoint | | 409 | `ENROLLMENT_ACTION_NOT_FOUND` | Enrollment action is not linked correctly | Contact support with the `X-Response-ID` | | 429 | Rate limited | Polling exceeded the endpoint limit | Back off before retrying | Every response includes an `X-Response-ID` header. Include it when contacting [support@prava.space](mailto:support@prava.space). # Get Payment Result Source: https://docs.prava.space/api-reference/get-payment-result api-reference/openapi.json GET /v1/sessions/{sessionId}/payment-result Retrieve transaction status, card identity, and payment credentials for a session A polling endpoint: after the cardholder completes card entry and passkey approval on Prava's secure surface, poll it to read the outcome and, for non-quote checkouts, the single-use credentials. `GET /v1/sessions/{sessionId}/payment-result` · Authenticated with your secret key. ## Path parameters The session id from [Create Session](/api-reference/create-session). ## Response Overall session status: `pending`, `processing`, `awaiting_result`, `completed`, or `failed`. `processing` means authentication or tokenization is in flight, or a quote-backed merchant checkout is not yet terminal. The card bound to this transaction—the source of truth whether it was preselected or selected or entered on the collection surface. `null` when no card is bound. `initiated` | `awaiting_result` | `completed` | `failed`. Omitted from credential-free quote-backed results. Line-item reference — pass this to [Report Status](/api-reference/report-status). `pending` | `credentials_generated` | `completed` | `failed`. Virtual card number (network token) your agent uses at checkout. Non-null after credentials are generated. Single-use CVV. Non-null after credentials are generated. Present only when the transaction failed: `{ code, message }`. Present for quote-backed sessions when merchant lifecycle data exists. Omitted for non-quote checkouts and sessions without that data. This projection contains only a normalized status and an optional merchant order ID. `pending` | `quote_ready` | `prepared` | `prepare_failed` | `paid` | `failed` | `needs_human` | `unknown`. An effective completed result maps to `paid`, including successful sandbox simulation. Optional merchant order ID, distinct from Prava's top-level `order_id`. Returned only when it meets the bounded identifier format. Present for an explicitly simulated sandbox session once an eligible terminal payment outcome is observed. Omitted for ordinary results and outcomes that cannot be simulated. Always `payment`. The actual observed outcome: `completed` or `failed`. Present when the observed payment failed: canonical `{ code, message }`. Present for a terminal quote-backed failure: `{ code, message }`. Confirmed payment failures use canonical codes such as `CARD_DECLINED`, `INSUFFICIENT_FUNDS`, or the fallback `PAYMENT_FAILED`. See [Errors](/api-reference/errors). ## Notes * For non-quote checkouts, `token` and `dynamic_cvv` contain the virtual card credentials your agent uses at checkout. * Read `transactions[].card_id` as the source of truth for the card bound to the transaction, whether it was preselected or chosen on the collection surface. * For quote-backed checkouts, Prava performs the merchant checkout. Results contain credential-free transaction summaries (`txn_id`, `card_id`, and `status`); they never include token, CVV, expiry, or line-item credentials. * `merchant_res` contains only `status` and optional `orderId`. Read canonical failure details from the top-level `error`. Keep polling while the top-level status is `pending` or `processing`. * [Sandbox simulation](/api-reference/testing#simulate-successful-payment-completion) can return effective `completed` and `merchant_res.status: "paid"` while `observed_result.status` is `failed`. Those effective statuses are returned only after Core's sandbox completion succeeds; they do not establish merchant settlement. The simulation request is supplied at session creation and is not echoed as a response-side `sandbox_simulation` object. * Responses use `Cache-Control: no-store`. * Prava performs a lazy mandate expiry check on every request — expired mandates are reflected in the status. * After using non-quote checkout credentials, report the outcome via [Report Status](/api-reference/report-status). ## Error responses | Status | Code | Cause | | - | - | - | | 401 | `AUTH_1001` | Invalid API key | | 401 | `AUTH_1002` | Missing or invalid Authorization header | | 404 | `NOT_FOUND` | Session not found or doesn't belong to your merchant account | ```bash cURL theme={null} curl "https://sandbox.api.prava.space/v1/sessions/sess_123/payment-result" \ -H "Authorization: Bearer sk_test_..." ``` ```python Python theme={null} import requests resp = requests.get( "https://sandbox.api.prava.space/v1/sessions/sess_123/payment-result", headers={"Authorization": "Bearer sk_test_..."}, ) ``` ```javascript JavaScript theme={null} const resp = await fetch("https://sandbox.api.prava.space/v1/sessions/sess_123/payment-result", { headers: { Authorization: "Bearer sk_test_...", }, }); ``` ```json 200 Success theme={null} { "session_id": "sess_123", "order_id": "ord_4a1b", "status": "awaiting_result", "transactions": [ { "txn_id": "txn_1", "card_id": "card_42", "status": "awaiting_result", "line_items": [ { "txn_ref_id": "tli_1", "merchant_name": "Acme Store", "total_amount": "49.99", "status": "credentials_generated", "token": "4111111111111111", "dynamic_cvv": "123", "expiry_month": "12", "expiry_year": "2030", "products": [] } ] } ] } ``` ```json 200 Quote-backed theme={null} { "session_id": "sess_123", "order_id": "ord_4a1b", "status": "processing", "transactions": [ { "txn_id": "txn_1", "card_id": "card_42", "status": "awaiting_result" } ], "merchant_res": { "status": "prepared" } } ``` ```json 200 Quote payment completed theme={null} { "session_id": "sess_123", "order_id": "ord_4a1b", "status": "completed", "transactions": [ { "txn_id": "txn_1", "card_id": "card_42", "status": "awaiting_result" } ], "merchant_res": { "status": "paid", "orderId": "merchant_order_123" } } ``` ```json 200 Quote payment failed theme={null} { "session_id": "sess_123", "order_id": "ord_4a1b", "status": "failed", "transactions": [ { "txn_id": "txn_1", "card_id": "card_42", "status": "awaiting_result" } ], "merchant_res": { "status": "failed" }, "error": { "code": "CARD_DECLINED", "message": "The payment was declined by the card issuer." } } ``` ```json 401 Unauthorized theme={null} { "error": { "code": "AUTH_1002", "message": "Missing or invalid Authorization header" } } ``` ```json 404 Not Found theme={null} { "error": { "code": "NOT_FOUND", "message": "Session not found" } } ``` # List Cards Source: https://docs.prava.space/api-reference/list-cards api-reference/openapi.json GET /v1/listCards Retrieve enrolled cards for a customer Retrieve a customer's enrolled cards. Only non-sensitive card metadata is returned — full PANs (the real card numbers) are never exposed. `GET /v1/listCards` · Authenticated with your secret key. Cards are scoped by `merchant_id` (derived from your secret key) and the `customer_id` query parameter. ## Query parameters The customer to list cards for — the same value you passed as `user_id` when creating the session. `active` (default) or `all` (include previously deleted cards for historical reference). `true` to include `card_art_url` and display `metadata`; otherwise those fields are null. ## Response Card id — the same identifier as the SDK `enrollmentId` from [`collectPAN()`](/sdk/cards/collect-pan). Last 4 digits. e.g. `"visa"`, or null if unknown. 1–12. 4-digit year. Masked PAN, or null. Whether this is the customer's default card. `active` or `deleted`. Only when `include_card_art=true`. Display metadata (`backgroundColor`, `foregroundColor`, `labelColor`). ISO 8601 enrollment timestamp. Number of cards returned. ## Error responses | Status | Code | Cause | | - | - | - | | 400 | `INVALID_REQUEST` | Missing `customer_id` query parameter | | 401 | `AUTH_1001` | Invalid API key | | 401 | `AUTH_1002` | Missing or invalid Authorization header | | 404 | `CUSTOMER_NOT_FOUND` | No customer found for the given `customer_id` | ```bash cURL theme={null} curl "https://sandbox.api.prava.space/v1/listCards?customer_id=user_123&status=active" \ -H "Authorization: Bearer sk_test_..." ``` ```python Python theme={null} import requests resp = requests.get( "https://sandbox.api.prava.space/v1/listCards", headers={"Authorization": "Bearer sk_test_..."}, params={"customer_id": "user_123", "status": "active"}, ) ``` ```javascript JavaScript theme={null} const resp = await fetch( "https://sandbox.api.prava.space/v1/listCards?customer_id=user_123&status=active", { headers: { Authorization: "Bearer sk_test_...", }, } ); ``` ```json 200 Success theme={null} { "cards": [ { "card_id": "card_1", "card_last4": "4242", "card_brand": "visa", "card_exp_month": 12, "card_exp_year": 2030, "is_default": true, "status": "active", "created_at": "2026-05-01T10:00:00Z" } ], "count": 1 } ``` ```json 400 Validation theme={null} { "error": { "code": "INVALID_REQUEST", "message": "Missing customer_id" } } ``` ```json 404 Not Found theme={null} { "error": { "code": "CUSTOMER_NOT_FOUND", "message": "No customer for the given customer_id" } } ``` # Cancel a Mandate Source: https://docs.prava.space/api-reference/mandate-cancel api-reference/openapi.json POST /v1/mandates/{id}/cancel Permanently stop future charges on a mandate. Cancel an active or paused mandate to permanently stop future charges. The request takes no body and returns the updated [mandate](/api-reference/mandate-list). `POST /v1/mandates/{id}/cancel` ## Path parameters The mandate id. ## Response Returns the updated mandate with `status: "cancelled"`. ## Notes * Cancellation is terminal and does not affect past charges. * Only an `active` or `paused` mandate can be cancelled. Any other transition returns `409 MANDATE_INVALID_TRANSITION`. * Only the mandate's owner can act on it; a mismatched caller gets `403 MANDATE_FORBIDDEN`. ```bash cURL theme={null} curl -X POST https://sandbox.api.prava.space/v1/mandates/mdt_123/cancel \ -H "Authorization: Bearer sk_test_..." ``` ```python Python theme={null} import requests response = requests.post( "https://sandbox.api.prava.space/v1/mandates/mdt_123/cancel", headers={"Authorization": "Bearer sk_test_..."}, ) ``` ```javascript JavaScript theme={null} const response = await fetch("https://sandbox.api.prava.space/v1/mandates/mdt_123/cancel", { method: "POST", headers: { Authorization: "Bearer sk_test_..." }, }); ``` ```json 200 Success theme={null} { "id": "mdt_123", "status": "cancelled", "state": "cancelled", "merchantScope": "listed", "approvedAmount": "40.00", "currency": "USD" } ``` ```json 409 Invalid Transition theme={null} { "error": { "code": "MANDATE_INVALID_TRANSITION", "message": "The mandate's current status forbids this action" } } ``` # Charge a Mandate Source: https://docs.prava.space/api-reference/mandate-charge api-reference/openapi.json POST /v1/mandates/{id}/charge Mint single-use card credentials against an active mandate — no passkey. Mint fresh single-use card credentials against an active mandate — **no passkey**. Merchant callers receive plaintext `credentials`; agent callers receive an `encrypted_payload` (credentials encrypted to the agent's public key). `POST /v1/mandates/{id}/charge` · Authenticated with your secret key (merchants) or Ed25519 signature (agents). Need to **create** a mandate first? Mandate creation is part of the Session APIs — [create a mandate](/api-reference/create-session#mandate-setup) with a `mandate_setup` block on Create Session. Charging is available to **merchants** (secret key) and **agents** (Ed25519 signature). It is deliberately **not** exposed over [MCP](/mcp/tools#mandates). See [Mandates](/concepts/mandates) for the model. ## Path parameters The mandate id (from [List Mandates](/api-reference/mandate-list)). ## Body Charge amount as a decimal string with up to 2 decimals, e.g. `"40.00"`. The per-charge cap is enforced by the card network — an over-cap charge is declined (`THRESHOLD_EXCEEDED`). Idempotency key (max 255). The same mandate id + `reference` returns the original charge (`deduplicated: true`) instead of minting again. Omit it and every call is a distinct charge. A *failed* charge clears its key, so a retry after failure is not deduplicated. Optional per-charge product details (exactly one entry, same shape as the `purchase_context.custom` entries in [Create Session](/api-reference/create-session)). Omit to reuse the mandate's setup context. Currency always comes from the mandate, never the charge. ## Response Pass this to [Report a Mandate Charge](/api-reference/mandate-report). `awaiting_result` or `failed`. `SUCCESS` or `FAILURE`. Merchant callers: `{ token, dynamicCvv, expiryMonth, expiryYear }`. Agent callers: `{ ephemeral_public_key, iv, auth_tag, data }`, decrypted client-side. e.g. `THRESHOLD_EXCEEDED` on an over-cap decline. True when a prior charge with the same `reference` was returned. ## Notes * The mandate must be `active`. Charging a non-active mandate returns `409 MANDATE_NOT_ACTIVE`. * An over-cap charge is a normal outcome: `status: "failed"` with an `errorMessage` such as `THRESHOLD_EXCEEDED`. The amount cap is enforced at the card-network level. * A charge outside a `listed` mandate's merchant returns `403 MANDATE_MERCHANT_NOT_ALLOWED`. * After checkout, settle the outcome with [Report a Mandate Charge](/api-reference/mandate-report). ## Error responses | Status | Code | Cause | | - | - | - | | 400 | `VAL_2001` | Missing or invalid `amount` | | 401 | `AUTH_REQUIRED` | Missing or invalid credentials | | 403 | `MANDATE_FORBIDDEN` | Caller does not own this mandate | | 403 | `MANDATE_MERCHANT_NOT_ALLOWED` | Listed-scope mandate; the charge names a merchant not on the allow-list | | 409 | `MANDATE_NOT_ACTIVE` | Mandate is not `active` | | 409 | `NO_INSTRUCTION` / `NO_ORDER` | Mandate is missing its instruction/order | | 500 | `NO_TOKEN` | Credential minting failed | ```bash cURL theme={null} curl -X POST https://sandbox.api.prava.space/v1/mandates/mdt_123/charge \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "amount": "40.00", "reference": "invoice_2026_07" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://sandbox.api.prava.space/v1/mandates/mdt_123/charge", headers={"Authorization": "Bearer sk_test_..."}, json={"amount": "40.00", "reference": "invoice_2026_07"}, ) ``` ```javascript JavaScript theme={null} const resp = await fetch("https://sandbox.api.prava.space/v1/mandates/mdt_123/charge", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ amount: "40.00", reference: "invoice_2026_07", }), }); ``` ```json 200 Success theme={null} { "mandateId": "mdt_123", "instructionId": "ins_1", "transactionId": "txn_9", "orderId": "ord_9", "status": "awaiting_result", "fetchStatus": "SUCCESS", "credentials": { "token": "4111111111111111", "dynamicCvv": "123", "expiryMonth": "12", "expiryYear": "2030" }, "deduplicated": false } ``` ```json 403 Forbidden theme={null} { "error": { "code": "MANDATE_FORBIDDEN", "message": "Caller does not own this mandate" } } ``` ```json 409 Mandate Not Active theme={null} { "error": { "code": "MANDATE_NOT_ACTIVE", "message": "Mandate is not active" } } ``` # Get a Mandate Source: https://docs.prava.space/api-reference/mandate-get api-reference/openapi.json GET /v1/mandates/{id} Retrieve one mandate with its balance and full charge history. Retrieve one mandate with its derived balance and full charge history. `GET /v1/mandates/{id}` · Authenticated with your secret key. Need to **create** a mandate? Mandate creation is part of the Session APIs — [create a mandate](/api-reference/create-session#mandate-setup) with a `mandate_setup` block on Create Session. ## Path parameters The mandate id. ## Response Everything in the [List Mandates](/api-reference/mandate-list) mandate object, plus: Cumulative amount charged. Idempotency reference supplied at charge time. ## Error responses | Status | Code | Cause | | - | - | - | | 401 | `AUTH_REQUIRED` | Missing or invalid credentials | | 403 | `MANDATE_FORBIDDEN` | Caller does not own this mandate | | 404 | `MANDATE_NOT_FOUND` | No such mandate | ```bash cURL theme={null} curl "https://sandbox.api.prava.space/v1/mandates/mdt_123" \ -H "Authorization: Bearer sk_test_..." ``` ```python Python theme={null} import requests resp = requests.get( "https://sandbox.api.prava.space/v1/mandates/mdt_123", headers={"Authorization": "Bearer sk_test_..."}, ) ``` ```javascript JavaScript theme={null} const resp = await fetch("https://sandbox.api.prava.space/v1/mandates/mdt_123", { headers: { Authorization: "Bearer sk_test_...", }, }); ``` ```json 200 Success theme={null} { "id": "mdt_123", "status": "active", "approvedAmount": "40.00", "currency": "USD", "spent": "40.00", "chargeCount": 1, "charges": [ { "transactionId": "txn_9", "amount": "40.00", "currency": "USD", "status": "completed", "reference": "invoice_2026_07", "createdAt": "2026-07-26T00:00:00Z" } ] } ``` ```json 403 Forbidden theme={null} { "error": { "code": "MANDATE_FORBIDDEN", "message": "Caller does not own this mandate" } } ``` ```json 404 Not Found theme={null} { "error": { "code": "MANDATE_NOT_FOUND", "message": "No such mandate" } } ``` # Pause a Mandate Source: https://docs.prava.space/api-reference/mandate-lifecycle api-reference/openapi.json POST /v1/mandates/{id}/pause Suspend future charges on an active mandate. Pause an active mandate so no new charges can be created until it is resumed. The request takes no body and returns the updated [mandate](/api-reference/mandate-list). `POST /v1/mandates/{id}/pause` Need to **create** a mandate? Mandate creation is part of the Session APIs — [create a mandate](/api-reference/create-session#mandate-setup) with a `mandate_setup` block on Create Session. ## Path parameters The mandate id. ## Response Returns the updated mandate object (see [List Mandates](/api-reference/mandate-list)). ## Notes * Only an `active` mandate can be paused. Any other transition returns `409 MANDATE_INVALID_TRANSITION`. * Only the mandate's owner can act on it — a mismatched caller gets `403 MANDATE_FORBIDDEN`. * To reactivate or permanently stop the mandate, use [Resume a Mandate](/api-reference/mandate-resume) or [Cancel a Mandate](/api-reference/mandate-cancel). ## Error responses | Status | Code | Cause | | - | - | - | | 403 | `MANDATE_FORBIDDEN` | Caller does not own this mandate | | 409 | `MANDATE_INVALID_TRANSITION` | The mandate's current status forbids this action | ```bash cURL theme={null} curl -X POST https://sandbox.api.prava.space/v1/mandates/mdt_123/pause \ -H "Authorization: Bearer sk_test_..." ``` ```python Python theme={null} import requests resp = requests.post( "https://sandbox.api.prava.space/v1/mandates/mdt_123/pause", headers={"Authorization": "Bearer sk_test_..."}, ) ``` ```javascript JavaScript theme={null} const resp = await fetch("https://sandbox.api.prava.space/v1/mandates/mdt_123/pause", { method: "POST", headers: { Authorization: "Bearer sk_test_...", }, }); ``` ```json 200 Success theme={null} { "id": "mdt_123", "status": "paused", "state": "paused", "merchantScope": "listed", "approvedAmount": "40.00", "currency": "USD" } ``` ```json 403 Forbidden theme={null} { "error": { "code": "MANDATE_FORBIDDEN", "message": "Caller does not own this mandate" } } ``` ```json 409 Invalid Transition theme={null} { "error": { "code": "MANDATE_INVALID_TRANSITION", "message": "The mandate's current status forbids this action" } } ``` # List Mandates Source: https://docs.prava.space/api-reference/mandate-list api-reference/openapi.json GET /v1/mandates List the standing mandates on an account, with approved and remaining amounts. List the caller's mandates. Merchant keys pass `customer_id`; control-plane callers (dashboard) scope by `dashboard_user_id` (+ `agent_id`). `GET /v1/mandates` · Authenticated with your secret key. Need to **create** a mandate? Mandate creation is part of the Session APIs — [create a mandate](/api-reference/create-session#mandate-setup) with a `mandate_setup` block on Create Session. ## Query parameters Filter to one customer — the `external_user_id` (the `user_id` used at session creation) or a `cus_` id. When `true`, return only standing mandates (exclude the transient per-checkout mandates that every ordinary checkout creates internally). ## Response Mandate id (`mdt_…`). Display-derived usability: `available`, `consumed`, `expired`, `paused`, `cancelled`, or `pending`. `pending`, `active`, `paused`, `consumed`, `cancelled`, or `expired`. `one_time`, `weekly`, `monthly`, `yearly`. `any` or `listed`. Authorized per-charge cap. Remaining spend this window — indicative for display; the enforced ceiling is the amount cap at the card-network level. Next cycle boundary for recurring mandates. `{ status, at }`. ## Notes * Use the returned `id` with [Get a Mandate](/api-reference/mandate-get), [Charge a Mandate](/api-reference/mandate-charge), or the [lifecycle actions](/api-reference/mandate-lifecycle). ## Error responses | Status | Code | Cause | | - | - | - | | 401 | `AUTH_REQUIRED` | Missing or invalid credentials | | 403 | `FORBIDDEN` | Caller may not list these mandates | ```bash cURL theme={null} curl "https://sandbox.api.prava.space/v1/mandates?customer_id=user_123&standing_only=true" \ -H "Authorization: Bearer sk_test_..." ``` ```python Python theme={null} import requests resp = requests.get( "https://sandbox.api.prava.space/v1/mandates", headers={"Authorization": "Bearer sk_test_..."}, params={"customer_id": "user_123", "standing_only": "true"}, ) ``` ```javascript JavaScript theme={null} const resp = await fetch( "https://sandbox.api.prava.space/v1/mandates?customer_id=user_123&standing_only=true", { headers: { Authorization: "Bearer sk_test_...", }, } ); ``` ```json 200 Success theme={null} { "mandates": [ { "id": "mdt_123", "status": "active", "state": "available", "recurringFrequency": "monthly", "merchantScope": "listed", "merchantName": "Acme Store", "approvedAmount": "40.00", "remaining": "40.00", "currency": "USD", "validUntil": "2027-07-26T00:00:00Z", "renewsAt": "2026-08-26T00:00:00Z", "lastCharge": null, "createdAt": "2026-07-26T00:00:00Z", "updatedAt": "2026-07-26T00:00:00Z" } ] } ``` ```json 401 Unauthorized theme={null} { "error": { "code": "AUTH_REQUIRED", "message": "Missing or invalid credentials" } } ``` ```json 403 Forbidden theme={null} { "error": { "code": "FORBIDDEN", "message": "Caller may not list these mandates" } } ``` # Report a Mandate Charge Source: https://docs.prava.space/api-reference/mandate-report api-reference/openapi.json POST /v1/mandates/{id}/charges/{txnId}/report Settle a mandate charge outcome (APPROVED / DECLINED) with the card network. Reconcile a mandate charge outcome and settle it with the card network. Call this after checking out with the credentials from [Charge a Mandate](/api-reference/mandate-charge). `POST /v1/mandates/{id}/charges/{txnId}/report` · Authenticated with your secret key (merchants) or Ed25519 signature (agents). Need to **create** a mandate? Mandate creation is part of the Session APIs — [create a mandate](/api-reference/create-session#mandate-setup) with a `mandate_setup` block on Create Session. ## Path parameters The mandate id. The `transactionId` returned from [Charge a Mandate](/api-reference/mandate-charge). ## Body `APPROVED` or `DECLINED`. `PURCHASE`. Authorization code from your processor, max 128. Optional; recorded when supplied. Processor response code, max 2. Optional. Amount paid, decimal string. Optional. ## Response `completed` or `failed`. The mandate's status after settlement. `SUCCESS` or `FAILURE`. ## Notes * Reporting a one-time mandate charge as `APPROVED` moves the mandate to `consumed`; recurring mandates stay `active`. * `authorization_code`, `response_code`, and `amount_paid` are optional and recorded when supplied. ## Error responses | Status | Code | Cause | | - | - | - | | 401 | `AUTH_REQUIRED` | Missing or invalid credentials | | 403 | `MANDATE_FORBIDDEN` | Caller does not own this mandate | | 404 | `MANDATE_NOT_FOUND` / `CHARGE_NOT_FOUND` | Mandate or charge not found | | 409 | `CHARGE_NOT_REPORTABLE` / `CHARGE_NO_TLI` / `NO_INSTRUCTION` | Charge is not in a reportable state | | 502 | `VISA_CONFIRMATION_FAILED` | Visa confirmation failed | | 500 | `MANDATE_CHARGE_REPORT_DB_FAILED` | Persistence failed | ```bash cURL theme={null} curl -X POST https://sandbox.api.prava.space/v1/mandates/mdt_123/charges/txn_9/report \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "txn_status": "APPROVED", "txn_type": "PURCHASE", "authorization_code": "OK123", "response_code": "00", "amount_paid": "40.00" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://sandbox.api.prava.space/v1/mandates/mdt_123/charges/txn_9/report", headers={"Authorization": "Bearer sk_test_..."}, json={ "txn_status": "APPROVED", "txn_type": "PURCHASE", "authorization_code": "OK123", "response_code": "00", "amount_paid": "40.00", }, ) ``` ```javascript JavaScript theme={null} const resp = await fetch( "https://sandbox.api.prava.space/v1/mandates/mdt_123/charges/txn_9/report", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ txn_status: "APPROVED", txn_type: "PURCHASE", authorization_code: "OK123", response_code: "00", amount_paid: "40.00", }), } ); ``` ```json 200 Success theme={null} { "mandateId": "mdt_123", "transactionId": "txn_9", "orderId": "ord_9", "status": "completed", "mandateStatus": "active", "visaConfirmation": "SUCCESS" } ``` ```json 404 Not Found theme={null} { "error": { "code": "CHARGE_NOT_FOUND", "message": "Charge not found" } } ``` ```json 409 Not Reportable theme={null} { "error": { "code": "CHARGE_NOT_REPORTABLE", "message": "Charge is not in a reportable state" } } ``` # Resume a Mandate Source: https://docs.prava.space/api-reference/mandate-resume api-reference/openapi.json POST /v1/mandates/{id}/resume Reactivate a paused mandate. Resume a paused mandate so it can create new charges again. The request takes no body and returns the updated [mandate](/api-reference/mandate-list). `POST /v1/mandates/{id}/resume` ## Path parameters The mandate id. ## Response Returns the updated mandate with `status: "active"`. ## Notes * Only a `paused` mandate can be resumed. Any other transition returns `409 MANDATE_INVALID_TRANSITION`. * Only the mandate's owner can act on it; a mismatched caller gets `403 MANDATE_FORBIDDEN`. ```bash cURL theme={null} curl -X POST https://sandbox.api.prava.space/v1/mandates/mdt_123/resume \ -H "Authorization: Bearer sk_test_..." ``` ```python Python theme={null} import requests response = requests.post( "https://sandbox.api.prava.space/v1/mandates/mdt_123/resume", headers={"Authorization": "Bearer sk_test_..."}, ) ``` ```javascript JavaScript theme={null} const response = await fetch("https://sandbox.api.prava.space/v1/mandates/mdt_123/resume", { method: "POST", headers: { Authorization: "Bearer sk_test_..." }, }); ``` ```json 200 Success theme={null} { "id": "mdt_123", "status": "active", "state": "available", "merchantScope": "listed", "approvedAmount": "40.00", "currency": "USD" } ``` ```json 409 Invalid Transition theme={null} { "error": { "code": "MANDATE_INVALID_TRANSITION", "message": "The mandate's current status forbids this action" } } ``` # API Reference Source: https://docs.prava.space/api-reference/overview Server-side REST API for Prava Payments *Not sure this is the right path? See [Choosing Your Integration](/choosing-your-integration).* ## Overview The Prava REST API is used for server-side operations: creating sessions, managing cards, retrieving payment results, and reporting status. All server-side requests are authenticated using your **secret key**. Never expose your secret key in client-side code, version control, or logs. Use it only in server-to-server requests. ## Base URLs | Environment | Base URL | Key Prefix | | - | - | - | | **Sandbox** | `https://sandbox.api.prava.space` | `sk_test_*` | | **Production** | `https://api.prava.space` | `sk_live_*` | Use the **sandbox** URL with your `sk_test_*` key during development. Switch to the **production** URL with `sk_live_*` when you're ready to go live. These are the only **API** hosts. [dashboard.prava.space](https://dashboard.prava.space) (developer console) and [pay.prava.space](https://pay.prava.space) (agent-owner wallet) are web UIs; never send API requests to them. ## Authentication All API requests require a Bearer token using your secret key: ```bash theme={null} Authorization: Bearer sk_test_xxx ``` ## Response Format All requests and responses are JSON. Monetary amounts are always decimal strings (e.g. `"49.99"`), never numbers. Every response includes an `X-Response-ID` header for debugging: ``` X-Response-ID: 8b74ce32-4682-4b17-b51f-d57808f99061 ``` Include this ID when contacting support to help trace issues. ## Error Response Format All errors follow a consistent structure: ```json theme={null} { "error": { "code": "ERROR_CODE", "message": "Human-readable error message", "details": {} } } ``` The `details` field is optional and included for validation errors to specify which fields failed. ### Common Error Codes | Status | Code | Description | | - | - | - | | 401 | `AUTH_1001` | Invalid API key | | 400 | `VAL_2001` | Validation error (check `details` for specifics) | | 404 | `NOT_FOUND` | Requested resource not found | | 400 | `INVALID_STATE` | Operation not allowed in current state | The full catalog (every code, per endpoint, with recovery steps) is on the [Errors](/api-reference/errors) page. ## The payment journey A non-quote payment is **three API calls and one non-API step**. Quote-backed checkouts use the same session and polling endpoints, but Prava completes the merchant checkout for you: Your backend pins the order: customer, merchant, amount, line items. Returns `session_token` and `iframe_url`. → [Create Session](/api-reference/create-session) This step happens on Prava's secure surface, not through the API: redirect the cardholder to the `iframe_url` (**hosted**) or mount it in your page with [`collectPAN`](/sdk/cards/collect-pan) (**embedded**). Card entry and passkey approval (a biometric confirmation on the cardholder's device) happen here. Prava handles authentication, mandate registration (the spending permission recorded with the card network), and tokenization (swapping the card number for a secure stand-in) for you. For a non-quote checkout, this returns one-time `token` and `dynamic_cvv` credentials. For a quote-backed checkout, it returns a credential-free transaction summary with the card actually used and the normalized outcome in the top-level `status` and `error`. When merchant lifecycle data exists, `merchant_res` also provides its normalized status and optional merchant order ID. → [Get Payment Result](/api-reference/get-payment-result) For non-quote checkouts, tell Prava whether checkout succeeded (`APPROVED`) or failed (`DECLINED`) so transaction records and the card network stay in sync. Quote callers do not call this public endpoint; poll their normalized payment result instead. → [Report Status](/api-reference/report-status) ### Supporting endpoints Provision a card only, or create an interactive card-and-passkey enrollment session Poll an interactive card-enrollment session without exposing payment credentials Cancel a session before it's used Retrieve a customer's enrolled cards # Report Status Source: https://docs.prava.space/api-reference/report-status api-reference/openapi.json POST /v1/sessions/{sessionId}/report-status Report payment execution outcome back to Prava Report the payment execution outcome back to Prava. Always report after you attempt the charge with the credentials — it updates transaction records and relays the outcome to the card network. `POST /v1/sessions/{sessionId}/report-status` · Authenticated with your secret key. **Always report status** after checkout execution. Report `DECLINED` if a payment token was used but checkout failed. ## Path parameters The session id from [Create Session](/api-reference/create-session). ## Body The line-item reference from [Get Payment Result](/api-reference/get-payment-result). `APPROVED` or `DECLINED`. Transaction type. Authorization code from your processor, max 128 chars. Processor response code, max 2 chars (e.g. `"00"` for approved). Override the total paid, if different from the original. Optional per-product status overrides; informational (the overall mandate status follows `txn_status`). `COMPLETED`, `FAILED`, `CANCELED`, `INPROGRESS`, `PENDING`, or `ONHOLD`. Your external product id, max 50. Provide this **or** `product_ref_id`. Prava's internal product ref, max 50. Provide this **or** `product_id`. Amount paid for this product. ## Response Always `"confirmed"`. `APPROVED` or `DECLINED`. `SUCCESS` or `FAILURE`. ## Notes * A mandate is the spending permission created when the cardholder approved the payment. Reporting a **one-time** mandate charge as `APPROVED` consumes the mandate (it can't be reused); a **recurring** mandate stays `active` for future charges. See [Mandates](/concepts/mandates). * `product_statuses` is optional and informational; the overall mandate status follows `txn_status`. ## Error responses | Status | Code | Cause | Resolution | | - | - | - | - | | 401 | `AUTH_1001` | Invalid API key | Check your secret key | | 401 | `AUTH_1002` | Missing or invalid Authorization header | Include `Authorization: Bearer sk_xxx` | | 404 | `NOT_FOUND` | Session / order / transaction reference not found | Verify the session ID and `txn_ref_id` | | 400 | `INVALID_STATE` | No transaction awaiting payment result | May already be reported, or not yet ready | | 400 | `MANDATE_EXPIRED` | Mandate has expired | Register a new intent | | 400 | `PRODUCT_NOT_FOUND` | Product not found by the given ID | Verify `product_id` or `product_ref_id` | | 502 | `VISA_CONFIRMATION_FAILED` | Card network confirmation failed | Retry or contact support | | 500 | `REPORT_STATUS_ERROR` | Internal error | Contact support with the `X-Response-ID` header | ```bash cURL theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions/sess_123/report-status \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "txn_ref_id": "tli_1", "txn_status": "APPROVED", "authorization_code": "OK123", "response_code": "00" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://sandbox.api.prava.space/v1/sessions/sess_123/report-status", headers={"Authorization": "Bearer sk_test_..."}, json={ "txn_ref_id": "tli_1", "txn_status": "APPROVED", "authorization_code": "OK123", "response_code": "00", }, ) ``` ```javascript JavaScript theme={null} const resp = await fetch("https://sandbox.api.prava.space/v1/sessions/sess_123/report-status", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Content-Type": "application/json", }, body: JSON.stringify({ txn_ref_id: "tli_1", txn_status: "APPROVED", authorization_code: "OK123", response_code: "00", }), }); ``` ```json 200 Success theme={null} { "status": "confirmed", "txn_ref_id": "tli_1", "txn_status": "APPROVED", "visa_confirmation": "SUCCESS" } ``` ```json 400 Invalid State theme={null} { "error": { "code": "INVALID_STATE", "message": "No transaction awaiting payment result" } } ``` ```json 404 Not Found theme={null} { "error": { "code": "NOT_FOUND", "message": "Session or txn_ref_id not found" } } ``` # Revoke Session Source: https://docs.prava.space/api-reference/revoke-session api-reference/openapi.json POST /v1/sessions/{id}/revoke Revoke an active payment session Revoke a payment session before it is used. Revocation is immediate and cannot be undone — a revoked session can no longer collect a card or produce credentials. `POST /v1/sessions/{id}/revoke` · Authenticated with your secret key. ## Path parameters The `session_id` returned by [Create Session](/api-reference/create-session). ## Response Always `true` on successful revocation. ## Notes * Once revoked, the session token can no longer be used for any SDK operations. * Any in-progress card collection or intent registration (the step that creates the payment's spending permission) within the session will fail. * Revocation is **immediate and irreversible**. ## Error responses | Status | Code | Cause | | - | - | - | | 401 | `AUTH_1001` | Invalid API key | | 401 | `AUTH_1002` | Missing or invalid Authorization header | | 404 | `NOT_FOUND` | Session not found or doesn't belong to your merchant account | ```bash cURL theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions/sess_123/revoke \ -H "Authorization: Bearer sk_test_..." ``` ```python Python theme={null} import requests resp = requests.post( "https://sandbox.api.prava.space/v1/sessions/sess_123/revoke", headers={"Authorization": "Bearer sk_test_..."}, ) ``` ```javascript JavaScript theme={null} const resp = await fetch("https://sandbox.api.prava.space/v1/sessions/sess_123/revoke", { method: "POST", headers: { Authorization: "Bearer sk_test_...", }, }); ``` ```json 200 Success theme={null} { "success": true } ``` ```json 401 Unauthorized theme={null} { "error": { "code": "AUTH_1002", "message": "Missing or invalid Authorization header" } } ``` ```json 404 Not Found theme={null} { "error": { "code": "NOT_FOUND", "message": "Session not found" } } ``` # Test Cards Source: https://docs.prava.space/api-reference/test-cards Sandbox test card numbers and the test OTP, organized by card network. Use these values to exercise the full payment flow in sandbox. They work like real cards but never move money. More networks will appear here as they're supported. **Sandbox only.** These values work only on `sandbox.api.prava.space` / `sandbox.collect.prava.space`. They are declined everywhere else. ## Visa | Card number | CVV | Expiry | | - | - | - | | `4622 9431 2313 7789` | `757` | `12/27` | | `4622 9431 2313 7797` | `640` | `12/27` | | `4622 9431 2313 7805` | `304` | `12/27` | | `4622 9431 2313 7847` | `698` | `12/27` | | `4622 9431 2313 7854` | `799` | `12/27` | | `4622 9431 2313 7862` | `938` | `12/27` | | `4622 9431 2313 7870` | `966` | `12/27` | | `4622 9431 2313 7888` | `408` | `12/27` | | `4622 9431 2313 7896` | `499` | `12/27` | | `4622 9431 2313 7904` | `890` | `12/27` | | `4622 9431 2313 7912` | `999` | `12/27` | ## Test OTP When card verification asks for an OTP (the one-time code your bank would normally text you), enter: ``` 456789 ``` ## Next The full sandbox test run, step by step. Every code you might hit, with recovery steps. # Testing in Sandbox Source: https://docs.prava.space/api-reference/testing How to exercise the full payment flow in sandbox before going live. Sandbox is **self-serve**: sign up at [dashboard.prava.space](https://dashboard.prava.space), create `pk_test_*`/`sk_test_*` keys, and you can create sessions immediately against `https://sandbox.api.prava.space`. ## Health check Confirm connectivity before anything else: ```bash theme={null} curl https://sandbox.api.prava.space/health # → { "status": "ok", "timestamp": "…" } ``` ## Test cards Test card numbers and the test OTP live on their own reference page, organized by card network: [Test Cards](/api-reference/test-cards). ## What behaves differently in sandbox | Area | Sandbox behavior | | - | - | | **Charges** | No real money moves; the card network runs in test mode | | **Card entry** | The secure surface is served from `sandbox.collect.prava.space` | | **Passkeys** | Real WebAuthn prompts: you'll use actual Touch ID / Face ID / a security key. Supported: Chrome 80+, Safari 14+, Firefox 80+, Edge 80+ | | **Sessions** | Same 15-minute expiry as production | | **Keys** | Only `sk_test_*` / `pk_test_*` work on the sandbox host | **Prava Pay (CLI) has no separate sandbox host.** The CLI talks to the live API; agent-linked payments use real cards. Sandbox environments apply to the **SDK/API integration path**. ## A non-quote checkout sandbox test run 1. [Create a session](/api-reference/create-session) with your `sk_test_*` key. 2. Open the returned `iframe_url` (hosted) or mount [`collectPAN`](/sdk/cards/collect-pan) (embedded) and enter a [test card](/api-reference/test-cards). 3. Approve with a passkey when prompted. 4. Poll [Get Payment Result](/api-reference/get-payment-result) until `status` is `awaiting_result`; the line items now carry `token` + `dynamic_cvv`. 5. [Report Status](/api-reference/report-status) with `APPROVED` or `DECLINED`. 6. Verify the final state: payment result `status` becomes `completed` (or `failed`). For a quote-backed session, poll the same result endpoint until its top-level status is terminal. When a transaction exists, the response contains `transactions[].card_id`, but no token, CVV, expiry, or line-item credentials. Read the top-level `status` and `error`; when merchant lifecycle data exists, `merchant_res` adds its normalized status and optional order ID. Prava performs the merchant checkout, so you do not call Report Status. ## Simulate successful payment completion Use sandbox simulation to test your success screen and completed-order handling when a quote-backed checkout reaches a confirmed merchant payment failure. Add `sandbox_simulation: { stage: "payment", outcome: "success" }` when creating the session. The request is fixed for that session; it is not an option on the result-polling request. Use an owned, unexpired sandbox quote. Replace the example quote ID, amount, and currency with its exact final values: ```bash theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions \ -H "Authorization: Bearer $PRAVA_SANDBOX_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "customer_123", "user_email": "customer@example.com", "total_amount": "49.99", "currency": "USD", "purchase_context": { "quote": true, "quote_id": "qte_your_sandbox_quote" }, "sandbox_simulation": { "stage": "payment", "outcome": "success" } }' ``` Open the returned `iframe_url`, complete card collection and verification, and poll [Get Payment Result](/api-reference/get-payment-result). The normal checkout still runs. After an eligible terminal merchant payment outcome, the effective result is `completed`. For example, a confirmed card decline can produce: ```json theme={null} { "session_id": "ses_example", "order_id": "ord_example", "status": "completed", "transactions": [ { "txn_id": "txn_example", "card_id": "card_example", "status": "completed" } ], "merchant_res": { "status": "paid" }, "observed_result": { "stage": "payment", "status": "failed", "error": { "code": "CARD_DECLINED", "message": "The payment was declined by the card issuer." } } } ``` The sandbox transaction, its line items, linked action, and order complete together. Associated one-time mandates are consumed; recurring mandates remain active. Repeated polling does not apply completion twice. Only after that completion succeeds does the result return `merchant_res.status: "paid"`. In a simulation, this is the effective test result; `observed_result` still reports what actually happened at the merchant. If the merchant reports success, `observed_result` contains `{ "stage": "payment", "status": "completed" }`. | Case | Behavior | | - | - | | Confirmed merchant payment failure | Effective `completed`; actual failure retained in `observed_result` | | Confirmed merchant payment success | `completed`; actual success retained in `observed_result` | | Pending or unknown payment | No simulated success; continue observation | | Preparation failure or additional verification required | Normal failure; simulation does not override it | | Production, custom checkout, or mandate setup | `400 SANDBOX_SIMULATION_NOT_ALLOWED` | | Unsupported stage, outcome, or extra simulation fields | Request validation error | The simulated completion changes only Prava's sandbox lifecycle. It does not settle a real merchant payment, change merchant fulfillment, or emit payment callbacks or webhooks. Use `observed_result` to distinguish the actual payment outcome from the effective test result. Anything unexpected? Check [Errors](/api-reference/errors). # Authentication & Environments Source: https://docs.prava.space/authentication Learn how to authenticate your requests and use Prava environments. **Two portals, two audiences.** [dashboard.prava.space](https://dashboard.prava.space) is the [**developer console**](/dashboard): sign up, create API keys, switch to production. [pay.prava.space](https://pay.prava.space) is the [**Prava Pay dashboard**](/prava-pay/your-wallet) for agent *owners*: approve agent links, enroll cards, set spending controls. A developer integrating the API only needs the console; an agent owner only needs the Prava Pay dashboard. ## Authentication Model Prava uses a **dual-key system** with two distinct authentication modes depending on where the request originates. ### Merchant Secret Key (Server-to-Server) Used for backend operations like creating sessions and listing cards. * Include the key as a Bearer token: `Authorization: Bearer sk_test_xxx` or `Authorization: Bearer sk_live_xxx` * **Never** expose secret keys in client-side code, version control, or logs. * Rotate keys immediately if compromised. ### Publishable Key (Client-Side) Used to initialize the SDK in the browser. * Passed during SDK initialization: `new PravaSDK({ publishableKey: 'pk_test_xxx' })` * Safe to include in frontend code — scoped to client-side operations only. | Key Type | Prefix | Usage | Location | | - | - | - | - | | **Publishable Key** | `pk_live_*` / `pk_test_*` | Initialize SDK, client-side operations | Frontend | | **Secret Key** | `sk_live_*` / `sk_test_*` | Create sessions, list cards, server operations | Backend only | ### Session-Based Auth After creating a session via the backend (`POST /v1/sessions`), the returned `session_token` authenticates all subsequent operations within that session (card collection, transactions, FIDO authentication; FIDO is the standard behind passkeys). Session tokens are: * **Short-lived**: expire after a configured duration. * **Single-use**: tied to a specific merchant, customer, and order. * **Revocable**: can be revoked via `POST /v1/sessions/:id/revoke`. ## Environments | Environment | Key Prefix | Base URL | Purpose | | - | - | - | - | | **Sandbox** | `pk_test_*` / `sk_test_*` | `https://sandbox.api.prava.space` | Development and testing | | **Production** | `pk_live_*` / `sk_live_*` | `https://api.prava.space` | Live transactions | Sandbox is self-serve: start building immediately. Switching to **production** is done from the [Prava Dashboard](https://dashboard.prava.space) and may require some additional verification; contact [support@prava.space](mailto:support@prava.space) when you're ready to go live. ## Response Headers Every API response includes an `X-Response-ID` header — a unique identifier for that request. Include this ID when contacting support to help us trace issues quickly. ``` X-Response-ID: resp_a1b2c3d4e5f6 ``` ## Webhooks Webhook **event delivery is coming soon**. Today you can already configure a `webhook_url` on your merchant account and you receive a `webhook_secret` (`whsec_…`) at merchant creation. Keep it safe; it will be used to verify event signatures once delivery ships. Until then, poll [Get Payment Result](/api-reference/get-payment-result) for payment outcomes; the [API journey](/api-reference/overview) is fully synchronous and complete without webhooks. # Changelog Source: https://docs.prava.space/changelog What's new in Prava and these docs. ## Find products more reliably during temporary provider issues Shopping search now retries short-lived catalog rate limits and service errors within the existing search deadline. If a relevance provider is temporarily unavailable, only the affected result batch is retried through a fallback model. Successful batches remain available, so a brief provider issue is less likely to interrupt the full search. Results and product details also use cached, verified merchant display names where available. This makes merchant choices clearer and keeps the name consistent as shoppers inspect an item. ## See import duties and fees in the checkout total When a supported merchant reports import duties or duty fees, the checkout amount breakdown now separates them from tax. It checks those charges against the merchant's final total before accepting the checkout amount, so cross-border charges are less likely to appear as an unexplained difference at payment. ## Resolve checkout address issues with clearer guidance Merchant checkout now returns a specific `DELIVERY_UNAVAILABLE_FOR_ADDRESS` error when the selected items cannot be delivered to the supplied address. Applications receive a stable, non-retriable result and a buyer-safe explanation instead of a generic temporary failure. Checkout also distinguishes an unsupported state or region with `INVALID_SHIPPING_REGION`. A merchant rejection of the buyer's phone number returns the existing, non-retriable `INVALID_PHONE_NUMBER` code rather than a generic checkout failure. When a merchant recommends adding an apartment or unit number but can still continue, the quote remains usable and carries that suggestion rather than failing the request. [Handle checkout errors →](/api-reference/errors) ## Remove another recognized paid protection offer before checkout Supported merchant checkout now recognizes an additional paid package-protection offer whose checkbox omits the price. It opts out only after the merchant's proposal confirms the charge, then waits for an updated proposal without the add-on before continuing. Other checkout preferences remain untouched. ## See where a payment attempt stopped Reseller order details now show a compact payment journey for each attempt. The journey separates Prava credential issuance, the merchant checkout result, and the final recorded outcome, making it easier to identify where an unsuccessful payment stopped. Only approved lifecycle facts appear in the dashboard. Payment credentials, provider confirmation details, and internal delivery metadata remain excluded. ## Open the Production request form from the environment switcher Choosing Production in the dashboard now takes merchants who need access directly to the Production request form. This fixes a detour to Business Details and makes the next step in going live easier to find. [Prepare your integration for Production →](/guides/go-live-checklist) ## Request and set up Production access from the dashboard Developers can now manage the Production access journey in the [Prava Dashboard](https://dashboard.prava.space). The guided request keeps saved progress, shows required business and identity information, and provides a receipt after submission. Request history and current status stay visible in the application workspace. If the review needs corrections, the dashboard identifies what to update before resubmission. Approved applications continue into Production setup and credential creation without leaving the dashboard. [Prepare your integration for Production →](/guides/go-live-checklist) ## Explore reseller portfolio analytics and orders Reseller workspaces now bring portfolio analytics, application health, and order discovery into the dashboard. Compare Total, Paid, and Failed orders across a chosen date range and merchant scope, inspect native-currency paid volume and failure rates, and add supporting metrics to the overview when needed. The Orders workspace supports order, reference, and session search alongside status, currency, and amount filters. Each ready application has its own Overview, Orders, Analytics, and Integration views, with contextual drill-downs into recent payment activity. Comparisons are hidden when historical evidence is incomplete rather than presenting an uncertain zero or trend. Current-period values and unaffected comparisons remain available. ## More reliable checkout completion and payment reporting Payment completion now uses the persisted transaction amount as its source of truth and applies lifecycle changes atomically. Duplicate completion calls, concurrent callbacks, and repeated result polling return the authoritative state without decrementing limits, consuming a mandate, or completing an order more than once. Approved totals must match the original payment amount after exact decimal normalization. Quote expiry and confirmed payment failures settle into durable failed results, while a saved successful outcome cannot be downgraded by a later retry or confirmation failure. Merchant checkout now gives supported preparation flows enough time to complete before reporting a timeout, reducing premature checkout failures. Card-network confirmation uses a bounded three-attempt delivery budget with shorter per-attempt timeouts, so an uncertain confirmation cannot retry indefinitely or overwrite the saved payment outcome. Payment reporting now distinguishes these recovery cases: * `PAYMENT_AMOUNT_MISMATCH` - the reported total or product amount differs from the amount persisted for the transaction. * `QUOTE_PAYMENT_CONFLICT` - the submitted report conflicts with the payment attempt already bound to the quote. * `VISA_CONFIRMATION_PENDING` - the payment outcome is saved and card-network confirmation is still pending. * `VISA_CONFIRMATION_RECONCILIATION_REQUIRED` - automatic confirmation attempts are exhausted; the saved payment outcome remains authoritative while confirmation is reconciled. [Handle payment results →](/api-reference/get-payment-result) · [Review error recovery →](/api-reference/errors#report-status-post-v1sessionssessionidreport-status) ## Keep active card verification from restarting Checkout now distinguishes an active card-verification session from one that failed to initialize. Once verification is ready, it has time to complete instead of being replaced by the shorter loading timeout. This prevents shoppers from being sent back to card selection while authentication is still in progress. If initialization actually fails, retrying creates a fresh verification session. Late events from the failed session are ignored, and a repeated failure keeps the error visible so the shopper can retry safely. ## Apply offer codes while preparing a shopping checkout Shopping checkout can now apply an offer code while preparing a product or supported merchant checkout. Accepted reductions appear in the discount breakdown and final amount. A code can also be retried on an active quote without rebuilding the shopping session. Invalid or expired codes leave the quote usable and return a safe warning so the shopper can continue without the discount. Raw offer codes are excluded from application logs and API responses. [Explore agentic shopping →](/prava-pay/shopping) ## Move through the dashboard faster without losing your place Merchants, applications, orders, and detail pages now open with less unnecessary loading because the dashboard fetches only the workspace being viewed. Returning to a list preserves its filters and navigation context, so users can investigate several records without rebuilding their view. Focused loading and error states keep the surrounding workspace visible while the selected view loads. Forms continue to warn before navigation when there are unsaved changes. ## Payment results include merchant lifecycle status `GET /v1/sessions/{sessionId}/payment-result` returns `merchant_res` when merchant lifecycle data exists. Merchant and agent results expose a normalized `status` and an optional merchant `orderId`. A completed checkout can include: ```json theme={null} { "status": "completed", "merchant_res": { "status": "paid", "orderId": "merchant_order_123" } } ``` Lifecycle states such as `pending`, `prepared`, `prepare_failed`, and `needs_human` remain visible. Non-quote results and sessions without merchant lifecycle data omit the field. Continue using the top-level `status` and canonical `error` for outcome handling. For [sandbox simulation](/api-reference/testing#simulate-successful-payment-completion), `merchant_res.status` becomes `paid` after sandbox completion succeeds. `observed_result` retains the actual merchant outcome, including a payment failure. [Read the payment-result response →](/api-reference/get-payment-result) ## Faster, more reliable merchant checkout Merchant checkout now reuses verified preparation and card-field checks when the page has not changed, reducing repeated setup before payment. Checkout information is gathered more efficiently, and payment outcome monitoring starts without waiting for unrelated navigation. Checkout also recognizes review-then-pay flows and payment buttons that include a currency total. Final amount, card-field, and consent checks still run, including opting out of marketing offers and card saving. Checkout requests human attention when the final payment action is ambiguous. ## Test successful checkout completion with sandbox simulation Exercise your application's success screen and completed-order handling even when a sandbox checkout ends in a merchant payment failure. You can now request a successful payment simulation when creating a quote-backed sandbox session. Add this field to your existing `POST /v1/sessions` request, using your sandbox secret key: ```json theme={null} { "sandbox_simulation": { "stage": "payment", "outcome": "success" } } ``` Complete card collection and verification, then poll the payment result as usual. Once an eligible terminal payment outcome is available, Prava returns `status: "completed"` and completes the sandbox order and active transaction together. Any associated one-time mandate is consumed; recurring mandates remain active. Repeated result polling is idempotent. `observed_result` preserves the actual payment outcome and its canonical error, so you can distinguish your application's simulated success from what happened at the merchant. **Sandbox only:** This supports payment-stage success for quote-backed checkout. It does not turn pending results, preparation failures, or requests for additional verification into success. The simulated completion does not settle a real merchant payment, trigger fulfillment, or emit payment callbacks or webhooks. Production, custom checkout, and mandate-setup sessions reject this option. [Try sandbox simulation →](/api-reference/testing#simulate-successful-payment-completion) ## Payment results use consistent error codes Merchant and agent payment-result responses now use the same terminal outcome format. Confirmed merchant payment failures return `status: "failed"` with a top-level `error`, including codes such as `CARD_DECLINED`, `INSUFFICIENT_FUNDS`, and `EXPIRED_CARD`. Unrecognized payment errors use `PAYMENT_FAILED` and a safe, consistent message. Explicit sandbox simulations additionally return `observed_result` for the actual terminal payment outcome. Responses use `Cache-Control: no-store`. [Read payment-result handling →](/api-reference/get-payment-result) · [Payment error codes →](/api-reference/errors) ## Match shopping requests to the right product and quantity Shopping discovery now recognizes equivalent measurements such as `0.75 L` and `750 ml`, `425 gm` and `425 g`, and different spellings of pack counts. It also preserves the shopper's product wording when interpreting a request, helping distinguish a feeding bottle from an accessory such as a bottle warmer. Results stay within the eligible merchant catalogue, including when broader matching is needed. These changes improve product matching without expanding merchant eligibility. ## Remove recognized paid protection add-ons before checkout Supported merchant checkouts now opt out of recognized paid package-protection and return-protection extras. If a charged line remains after opting out, checkout removes that line and waits for the merchant's updated total before continuing. The cleanup targets these recognized extras and preserves unrelated billing and required terms controls. This helps keep automatically added protection charges out of the amount presented for payment. ## Return to the same place in your dashboard Detail-page Back controls restore the view you came from, including its filters, selected tab, date range, and pagination. Browser Back and Forward remain usable, so reviewing an order or application no longer means rebuilding the previous dashboard view. ## Fill missing checkout names from verified billing details Retail checkout now preserves a name already filled by the payment provider and can fill a blank cardholder-name field from the checkout's visible billing details. Shipping details are used only when the checkout confirms that shipping is also the billing address. If a required name cannot be resolved, checkout requests human attention before clicking Pay. Hidden fields, email-derived names, and placeholder names are not used. ## Choose shipping before paying in the Playground The [Prava Playground](https://playground.prava.space/) now lets you select another available shipping option and review its updated delivery information and final total before paying. The selection is confirmed by the merchant before payment-session creation; checkout does not rely on a locally estimated shipping price. ## Try Mastercard enrollment and saved-card authentication in the Playground demo The [Prava Playground demo](https://playground.prava.space/demo) now offers a Mastercard sandbox flow alongside Visa. Add the Mastercard test card, complete its hosted authentication flow, and select the saved card to try payment authentication and credential preparation. The demo wallet can retain one Visa fixture and one Mastercard fixture for the current browser. Mastercard appears only when its demo configuration is available, and a successful authentication alone does not establish a completed merchant payment. This release also fixes Visa sandbox initialization in the demo. These changes apply to the configured demo experience; general Mastercard availability in the Core API is separate. ## Prevent cached assets from blocking checkout initialization The secure checkout iframe now bundles its logger with the versioned application asset. An older cached logger can no longer cause the `ensureIframeLogger is not defined` error during payment initialization. No integration change is required. ## Mexico-issued cards and enrollment retry improvements * **Card eligibility:** Mexico-issued cards now pass Prava's issuing-country check. Existing issuer restrictions still apply. * **Enrollment retries:** If Visa verification fails to initialize before a card is provisioned, **Try again** now starts a fresh readiness cycle in the same enrollment session. * **Card artwork:** Full-enrollment completion can display the card artwork when it is available. These improvements are available in production. [Integrate card enrollment →](/api-reference/enroll-card) ## Payment results identify the card used * `GET /v1/sessions/{sessionId}/payment-result` now returns `transactions[].card_id`, the source of truth for the card bound to the transaction whether preselected or chosen during collection. * Results can report `status: "processing"` while payment or merchant checkout is still running. For quote-backed sessions, keep polling for the merchant's terminal outcome; credential issuance alone does not mean the merchant order is paid. ## Enroll a card and set up its passkey without a payment Prepare a customer's card for future payments in a dedicated enrollment flow. `POST /v1/sessions/enroll-card` now accepts `enrollment_mode: "full_enrollment"` and returns a session that the customer completes on Prava's secure surface. Start with an existing `vault_ref_id`, or omit it to let the customer enter a card. Full enrollment provisions the card and completes its Visa enrollment and passkey setup without creating an order or transaction. The existing `provision_only` mode continues to return a `card_id` directly for an already-vaulted card. Create the session on your backend using your secret API key: ```json theme={null} { "enrollment_mode": "full_enrollment", "user_id": "customer_123", "user_email": "customer@example.com", "integration_type": "full_checkout" } ``` Open the returned `iframe_url` unchanged. Your backend can poll `GET /v1/sessions/{sessionId}/status` to distinguish a provisioned card from completed enrollment. Accept success when `status` is `completed` and `outcome` is `fully_enrolled`. Retryable failures can be retried inside the same session using the selected card. [Set up card enrollment →](/api-reference/enroll-card) · [Poll enrollment status →](/api-reference/get-card-enrollment-status) ## Clearer dashboard environment switching The developer dashboard now coordinates environment changes across open tabs and shows a transition while the selected view refreshes. Failed switches offer a recovery action. Switching checks for unsaved edits and one-time API keys, and entering the production view asks for confirmation. This update is deployed to the sandbox-hosted dashboard. The production view is available to accounts with production access. [Using dashboard environments →](/dashboard) ## Provision a vaulted card from your backend Merchant integrations can now call `POST /v1/sessions/enroll-card` with a secret API key to provision an already-vaulted customer card. Supply the customer identity, `vault_ref_id`, and consent reference; the response returns the resulting `card_id`. This server-to-server flow creates no order or transaction. It provisions the card; passkey setup and payment authorization are separate steps. [Provision a card →](/api-reference/enroll-card) ## New API keys identify their environment Newly issued merchant keys use `pk_test` / `sk_test` prefixes for sandbox and `pk_live` / `sk_live` for production. Production key creation in the dashboard requires production access. [Manage API keys →](/dashboard) ## Manage AI applications in enabled reseller workspaces Enabled reseller workspaces in the sandbox-hosted dashboard have dedicated Overview, Applications, and Team views. With merchant onboarding enabled, submit an application, follow its review status, and respond to requested changes before activation. Authorized workspaces can inspect their applications' orders and transactions and, with credential management enabled, create a replacement key before revoking the old one. The read-only Team view lists active members and pending invitations. Availability follows the reseller's enabled environment and modules. Each order retains its own currency; application analytics avoid combining amounts across currencies. [Open the dashboard →](/dashboard) ## Quote-backed embedded checkout uses the full-width layout Quote-backed embedded checkout uses the full-width, borderless presentation of standard embedded checkout, keeping the payment surface aligned with its host page. [Choose a checkout presentation →](/api-reference/create-session) ## Create payment sessions from an existing quote `POST /v1/sessions` supports a quote-based flow: pass the quote ID and Prava resolves its merchant, products, and final total before creating the payment session. From your backend, authenticate with your merchant secret key and send: ```json theme={null} { "user_id": "customer_123", "user_email": "customer@example.com", "total_amount": "49.99", "currency": "USD", "purchase_context": { "quote": true, "quote_id": "qte_..." } } ``` Use the exact final amount and currency of your quote. A mismatch returns `409 QUOTE_SESSION_MISMATCH`; an expired quote returns `410 QUOTE_EXPIRED`. Omit `custom` in quote mode. [Create a session →](/api-reference/create-session) ## CLI mandate setup uses the agent session API `@prava-sdk/cli` 3.2.0 fixes `prava mandate create`: it now sends the agent-session request format and displays the returned approval URL. Version 3.1.0 sent the request to the merchant-session endpoint and expected a different response field. **Action required:** If you use 3.1.0 to create mandates, update to 3.2.0 or later. API responses also expose `X-Min-CLI-Version` and `X-Min-Skill-Version`. Set `X-Skill-Name` to receive the requirement for that skill. The production CLI minimum is `3.2.0`. [Create a mandate with the CLI →](/prava-pay/mandates) ## Session purchase context has two explicit modes `POST /v1/sessions` now accepts `purchase_context` as an object with two mutually exclusive modes: `{ custom: [...] }` for caller-supplied merchant and product details, or `{ quote: true, quote_id: "qte_..." }` for an existing quote. **Action required for custom checkout integrations:** Change `purchase_context: [...]` to `purchase_context: { custom: [...] }` on the same endpoint. Keep your existing merchant and product fields inside that array; one merchant is supported per session. Omit `quote_id` in custom mode and omit `custom` in quote mode. [Create a session →](/api-reference/create-session) ## Catch invalid merchant URLs and email domains at session creation Session creation validates merchant URLs and customer email domains earlier. Supply an HTTPS merchant URL with a valid public domain and a customer email on a routable domain. Merchant URLs are stored as their origin, with paths, queries, and fragments removed; merchant names are normalized to Visa-supported characters. **Action required:** Replace HTTP, localhost, IP-address, or invalid-domain merchant URLs and non-routable email domains in affected session requests. [Create a session →](/api-reference/create-session) ## Card-art generation has bounded work and retries Card-art generation limits concurrent work and repeated attempts to reduce interference with payment requests, including when cards without artwork are listed again. [Saved cards →](/api-reference/list-cards) ## Edit your application profile and checkout brand color After setting up a merchant in the selected environment, use **Business Details** in the sandbox-hosted developer dashboard to update the application name, website, and embedded-checkout brand color. These edits are also available in the production view for approved accounts. Production access remains subject to approval. [Update business details →](/dashboard) ## Reset saved-card passkeys while keeping the card Cardholder session authentication can call `DELETE /v1/cards/{id}/device-bindings` to remove device bindings while keeping the saved card and network token. Local passkey state is cleared only after removal is verified. Shared-token bindings cannot be reset through this endpoint. Merchant default-card mutations are also scoped to the merchant's own customers. [Saved cards →](/api-reference/list-cards) ## More reliable card entry and currency handling * Mobile layouts keep expiry fields and card-entry controls visible on narrow screens. * Card-vault connection failures have bounded retries before provisioning. * Currency handling uses the order's supported currency throughout the payment flow. [Checkout flow →](/concepts/checkout-flow) ## Named API keys and staged rotation The sandbox-hosted developer dashboard lets you name API keys and see their creation date, last use, and status. New secret keys are shown once. Rotate a key by creating and saving its replacement, updating your integration, and then explicitly revoking the old key. The old key stays active until you revoke it. Key creation requires the developer role or higher; revocation requires admin access. Production views and actions require production access. [Manage API keys →](/dashboard) ## Encrypted agent charge results and standing-mandate filtering **Action required for direct agent charge integrations:** Read and decrypt `encrypted_payload` using the agent's private key instead of reading plaintext `credentials` or `dynamicData`. Merchant secret-key callers still receive `credentials`. `GET /v1/mandates` also accepts `standing_only=true` to exclude transient checkout mandates. Charging and reporting require a merchant secret key or a direct agent signature; charging is not exposed through MCP. [Charge a mandate →](/api-reference/mandate-charge) · [List mandates →](/api-reference/mandate-list) ## See card-check outcomes in order history Order details in the sandbox-hosted developer dashboard show recorded card-check outcomes per payment attempt and a readable activity timeline, making declined and retried attempts easier to follow. Production views require production access. [Inspect orders →](/dashboard) ## More Asia-Pacific issuing countries pass card eligibility checks Cards issued in Australia, Hong Kong, Japan, Malaysia, New Zealand, Singapore, South Korea, Taiwan, Thailand, and Vietnam now pass the issuing-country check, alongside Canada, the US, and its supported territories. Existing issuer restrictions still apply; Mexico is covered in the September 8 update. [Enroll a card →](/api-reference/enroll-card) ## Manage standing mandates and reconcile each charge Add `mandate_setup` with `intent: "mandate_setup"` to a session to collect approval for future charges. After activation, charge the mandate without another passkey prompt. One-time standing mandates are capped at seven days; a later `valid_until` does not extend that window. Check `validUntil` before scheduling a charge. * **Find the right mandate:** `GET /v1/mandates` accepts `customer_id` for merchant callers, using either their external customer ID or a Prava customer ID. Responses include `approvedAmount`, `remaining`, `validUntil`, `renewsAt`, and `lastCharge`; the detail endpoint includes charge history. Displayed balances are derived from recorded activity; Visa enforces the spending limit. * **Track each charge:** `POST /v1/mandates/{id}/charge` creates its own order and transaction. Send a decimal-string `amount` and an optional `reference` to deduplicate retries within that mandate. Failed charges release the reference for retry. Currency comes from the mandate; an optional per-charge `purchase_context` remains a one-entry array. * **Report the outcome:** Send `txn_status: "APPROVED"` or `"DECLINED"` to `POST /v1/mandates/{id}/charges/{txnId}/report` to reconcile the charge and report to Visa. `txn_type` defaults to `"PURCHASE"`. * **Control future charging:** `POST /v1/mandates/{id}/pause`, `/resume`, and `/cancel` manage the mandate's Prava state. Cancellation is terminal in Prava; these controls do not revoke the underlying Visa instruction. Charging and reporting require a merchant secret key or a direct agent signature. Charge requests are not exposed through MCP. [Set up a mandate →](/api-reference/create-session#mandate-setup) · [Charge →](/api-reference/mandate-charge) · [Report →](/api-reference/mandate-report) · [Lifecycle controls →](/api-reference/mandate-lifecycle) ## Checkout retries refresh saved cards and clear old errors Retrying checkout refreshes saved cards and clears stale error overlays so newly added cards are available on the next attempt. Embedded checkout retains the restored card list and add-card controls from the July 2 retry fix. [Checkout flow →](/concepts/checkout-flow) ## Prava MCP is live * Connect any MCP client to `https://mcp.pay.prava.space/mcp` and your agent can pay and shop with your card. One URL, OAuth sign-in, 12 tools, and a credential firewall: payment credentials never reach the agent. Start at [Prava MCP](/mcp/overview). ### Docs * New end-to-end guides: [Add Payments to Your AI App](/guides/add-payments-to-your-ai-app), [REST Checkout Walkthrough](/guides/rest-checkout-walkthrough), and the [Go-Live Checklist](/guides/go-live-checklist). * New reference pages: [Errors](/api-reference/errors) (every code in one catalog), [Testing in Sandbox](/api-reference/testing), and [Delete Card](/api-reference/delete-card). * New audience pages: [Your Prava Pay Dashboard](/prava-pay/your-wallet) for agent owners, [For Merchants](/integration/merchants), [Use Cases](/use-cases), and [Compliance & Verification](/guides/compliance). * Sandbox [test cards and test OTP](/api-reference/test-cards) published. * Navigation reorganized: SDK, MCP, and CLI are sibling integration methods under Developer Guide; Capabilities and FAQ have their own tabs. * [Payments](/concepts/payments) restructured around the public flow — what you drive vs. what Prava does for you — plus a new [Glossary](/concepts/glossary). * Fresh visual identity: brand fonts, light theme, imagery, and an interactive API playground backed by an OpenAPI spec. * Agent skills: `prava-pay`, `prava-shopping`, and `prava-sdk-integration` installable via `npx skills add`. * Docs MCP server at `docs.prava.space/mcp` and `llms.txt` for AI-assisted integration — see [Use Prava Docs with AI](/ai). ### API * `POST /v1/deleteCard` documented in the API reference and OpenAPI spec. ## Replay agent-session requests Direct signed-agent integrations can send an `Idempotency-Key` with `POST /v1/sessions/agent`. Reusing it for the same agent returns the original session with HTTP `200`; a new session returns `201`. A concurrent duplicate can return `409 DUPLICATE_EXTERNAL_ORDER_REF`; retry with the same key to retrieve the original. This endpoint accepts flat `total_amount`, `currency`, `merchant_name`, `merchant_url`, `merchant_country`, and `products` fields. Read the approval link from `payment_url`. Use a new idempotency key for each new purchase. [Use the current CLI →](/prava-pay/quickstart) ## Shopping commands in Prava CLI v3 `@prava-sdk/cli` 3.0.0 adds `prava shop search`, `product`, `quote`, and `checkout` commands alongside the existing agent-linking and payment-session commands. The package also includes commands to list, add, and choose a default delivery address. [Explore CLI shopping commands →](/prava-pay/shopping) ## Wait for settled shipping and tax before accepting checkout totals Merchant checkout now waits while the merchant is still calculating tax or has not yet selected an available shipping method. This avoids accepting a plausible total that later changes when shipping or tax is applied. Checkout can also recover displayed information when network data is unavailable, and corrected address-validation errors clear when the merchant accepts the updated details. ## Mobile verification screens fit embedded checkout Verification and OTP steps align to the top of embedded mobile checkout, keeping the active controls easier to reach. These changes build on the May 25 responsive footer update, including iPhone bottom safe-area handling. [Checkout flow →](/concepts/checkout-flow) ## Remove saved cards and refresh their lifecycle when re-saving `POST /v1/deleteCard` accepts `CUSTOMER_CONFIRMED` for ordinary customer-requested removal and returns `was_default` and `network_token_deleted`. Shared network tokens are retained for other customers. Re-saving a removed card refreshes reused enrollment and token records for the new saved card. [Delete a card →](/api-reference/delete-card) ## Compare delivery estimates across shipping options Merchant checkout now preserves delivery estimates for individual shipping options, including estimates supplied by merchant delivery-date apps. When an estimate is available only on the page, checkout can fill that missing information for the matching option. The selected delivery estimate stays aligned with its shipping method. Merchant-provided prices, tax, and totals remain unchanged by this delivery-information fallback. ## Branded embedded checkout Set `integration_type: "embedding"` when creating a session for checkout that uses your stored merchant brand color. The default `"full_checkout"` mode provides the standalone checkout presentation. [Choose a checkout presentation →](/api-reference/create-session) ## Automatic card defaults and Canadian card eligibility When a card list has no default, the earliest saved card becomes the fallback, including after an explicit clear. Card lists identify that selection with `is_default`. Canada joins the US and its supported territories in Prava's issuing-country check. Existing issuer restrictions still apply. [List cards →](/api-reference/list-cards) ## Accurate payment totals and verification recovery Payment authorization uses the full approved order total, including tax and shipping, rather than the sum of product prices alone. Visa verification initialization has timeout and retry handling so an initialization failure can surface a recoverable state. [Create a session →](/api-reference/create-session) ## Shorter agent approval links and clearer setup recovery `@prava-sdk/cli` 2.0.0 updates `prava setup` to register a signed request and print a short approval link containing a server-issued link ID. Agent metadata is submitted during registration instead of being carried in the approval URL. Setup reports expired or denied links and explains when an incorrect system clock prevents registration. Polling stops when a link expires and tells you to start setup again. [Link an agent →](/prava-pay/linking) ## Choose a default saved card `GET /v1/listCards` returns `is_default` and places the default first. Merchant secret-key callers can set it with `PATCH /v1/cards/{id}/default` or clear it with `PATCH /v1/cards/remove-default`, passing the customer's Prava ID in the `customer_id` query parameter. [List cards →](/api-reference/list-cards) ## More visible API-key creation errors API-key creation failures now appear as notifications in the sandbox-hosted dashboard, making unsuccessful requests easier to spot while the key-creation dialog remains open. [Manage API keys →](/dashboard) ## Live card enrollment verified in production Prava's first live cardholder enrollment was verified against production Visa services. The verified flow covers session creation, hosted card verification, and enrollment for the initial production integration. Related fixes restored production API requests that had blocked merchant onboarding and corrected the Visa environment used by hosted verification. [Understand the checkout flow →](/concepts/checkout-flow) ## The first Prava CLI package The first public `@prava-sdk/cli` package introduces the `prava` command for agent integrations. Version 0.0.1 includes `prava setup`, `prava status`, and `prava sessions create`, with polling for the payment-session result. [Get started with the current CLI →](/prava-pay/quickstart) # Choosing Your Integration Source: https://docs.prava.space/choosing-your-integration Three ways to build on Prava — and exactly when to use each. Prava has **three** integration paths. (A **merchant** wanting to *sell to* AI shoppers doesn't integrate at all; see [Agentic Commerce](/integration/overview).) Which path you want comes down to two questions: 1. **Who owns the interface**: an **AI agent**, or your own application? 2. If it's your application, **how do you want to present card entry**: inside your own UI, or hand off to a Prava-hosted page? With Prava the payment itself is **always AI/agent-mediated**; what differs is *who owns the interface* and *where the card UI lives*. All three paths share the same secure core: the card is entered on Prava's surface and turned into one-time, scoped credentials. ## Decide in 10 seconds ```mermaid theme={null} flowchart TD A{Who owns the
interface?} -->|AI agent| AGENT[Agent
Prava Pay] A -->|Your application| B{Card form in
your own UI?} B -->|Yes| EMB[Embedded
SDK + API] B -->|No| HOST[Hosted
Full API] ``` If the surface your user interacts with is an **AI agent platform** (OpenClaw, Hermes, ChatGPT; [supported identifiers](/prava-pay/linking)), connect Prava as that agent's payment capability: via **[MCP](/mcp/overview)** (Model Context Protocol: one URL, any MCP client) or the **[CLI](/prava-pay/overview)** (agents that run shell commands). You own an **AI application, or an app backed by a legal business entity**: * **Yes**: keep the user on your page → **[Embedded (SDK + API)](/sdk/integration-modes)**. * **No**: minimal frontend, redirect out and back → **[Hosted (full API)](/sdk/integration-modes#hosted-mode-default)**. **The MCP connector is live.** Add `https://mcp.pay.prava.space/mcp` to AI apps like Hermes, ChatGPT, or OpenClaw and pay with no CLI at all. See [Prava MCP](/mcp/overview). ## Compare the three | | **Embedded** (SDK + API) | **Hosted** (full API) | **Agent** (Prava Pay) | | - | - | - | - | | **Who owns the interface** | Your AI application / business | Your AI application / business | An AI agent platform: OpenClaw, Hermes, ChatGPT | | **Where the card is entered** | An iframe (embedded frame) **inside your page** | Prava-hosted page (redirect) | Prava-hosted page (the owner enters it) | | **What you build** | Session (backend) + SDK wiring (frontend) | Session (backend) + a redirect & `callback_url` route | Nothing: connect via [MCP](/mcp/connect) or run the `prava` CLI | | **What Prava hosts** | The secure card iframe | The full checkout page | The full checkout + agent control plane | | **Frontend footprint** | Small JS (`@prava-sdk/core`) | Almost none | None (MCP or CLI) | | **How you get the result** | `onSuccess` callback, in-app | Redirect to your `callback_url` | CLI output / exit codes | | **Best when** | You want a native-feeling checkout on your page | You want the fastest, lowest-maintenance integration | The interface is an AI agent that shops or pays | | **Start here** | [SDK Overview](/sdk/overview) | [API Reference](/api-reference/overview) | [Prava Pay](/prava-pay/overview) | ## Pick your path Card UI inside your own page. Use when you want checkout to feel native to your product. Redirect to a Prava-hosted page. Use when you want to ship fast with minimal frontend code. For **AI-agent-owned interfaces**. Connect via [MCP](/mcp/overview) or the CLI. Embedded and Hosted are two ways to use the **same session**; see [Integration Modes](/sdk/integration-modes) for the mechanics. **Agent** (Prava Pay) is a separate product for AI-agent-owned interfaces; start at its [overview](/prava-pay/overview). # Accounts & Agents Source: https://docs.prava.space/concepts/accounts How an owner's Prava account holds cards and authorizes AI agents to pay — and the controls that keep it safe. Prava doesn't have a standalone "wallet" object. What it has is an **account** owned by a person or business (the **agent owner**), managed from the [Prava Pay dashboard](https://pay.prava.space). The account holds the enrolled cards and saved addresses, and the owner authorizes one or more **AI agents** to pay on their behalf. **Two portals, two audiences.** [dashboard.prava.space](https://dashboard.prava.space) is the [**developer console**](/dashboard): sign up, create API keys, switch to production. [pay.prava.space](https://pay.prava.space) is the [**Prava Pay dashboard**](/prava-pay/your-wallet) for agent *owners*: approve agent links, enroll cards, set spending controls. A developer integrating the API only needs the console; an agent owner only needs the Prava Pay dashboard. With Prava, the agent never holds funds or a raw card. It acts against the **owner's account** under the owner's controls, and only ever receives one-time, scoped credentials at the moment of purchase. ## The model * **Agent owner**: a dashboard user who owns the account and manages everything at [pay.prava.space](https://pay.prava.space). * **Cards & addresses**: enrolled once and held on the owner's account. * **Agents**: the owner links and approves each agent; every agent gets its own secure identity. * **Shared access**: all of an owner's agents draw on the **same** cards. There are no per-agent card permissions. ## Authorizing agents An agent is connected through the [linking flow](/prava-pay/linking): it requests access, and the owner approves it in the browser. From then on an agent is either **active** or **revoked**. It's all-or-nothing, not a permissions matrix. Revoking an agent cuts off its access immediately. ## Spending controls These are the controls that actually exist and are enforced: | Control | What it does | Where it's enforced | | - | - | - | | **Checkout quota** | A set number of checkouts allowed per owner (a lifetime count), consumed as agents buy. | Prava (application level) | | **Concurrency limit** | Caps how many checkouts can be open at once (a small number). | Prava (application level) | | **Per-purchase approval** | Every spend requires explicit approval before it happens. The [Skills](/prava-pay/skills) enforce a hard stop for agents; SDK/hosted flows use passkey confirmation (a biometric or security-key approval on the user's device). | Owner / device | | **Per-merchant amount limits** | When a card is enrolled for agentic commerce, each authorization is locked to the merchant and amount. | Card network (Visa) | Quota and concurrency are **Prava application-level** limits. Merchant/amount locking is enforced by the **card network**. See [Guardrails](/concepts/guardrails) for the network-level detail. ## Cards on the account Cards are enrolled through Prava's secure collection ([`collectPAN`](/sdk/cards/collect-pan) or a hosted page) and tokenized: the number is replaced with a secure stand-in, and the raw card never touches the app or the agent. Each card carries: * a **status** (`active` / `deleted`), * an **agentic-commerce** flag (`isAgenticCommerceEnrolled`) indicating it's enabled for AI-initiated purchases, and is visible from the dashboard and the [List Cards](/api-reference/list-cards) API. ## Related How an owner approves an agent against their account. The network-level constraints on what an authorized purchase can do. # Anatomy of a Checkout Source: https://docs.prava.space/concepts/checkout-flow The full sequence behind a single payment — including the card-verification steps (passkey / device binding and issuer OTP) that a first-time integrator does not see coming. From your side a payment is [three API calls and one hand-off](/concepts/payments#the-lifecycle-you-drive). This page zooms into the hand-off — **step 2, card entry** — because that's where the human-facing verification happens, and those steps aren't visible in the API surface. If you're budgeting time for a demo, read this so nothing surprises you. ## The full sequence ```mermaid theme={null} sequenceDiagram participant You as Your backend participant User as Cardholder participant Prava as Prava surface participant Issuer as Card network / issuer You->>Prava: 1. POST /v1/sessions (pin merchant + amount) Prava-->>You: session_token + iframe_url You->>User: open iframe_url (hosted) / mount collectPAN (embedded) User->>Prava: enter card (PAN) alt New browser/device — first time (device binding) Prava->>Issuer: request step-up Issuer->>User: OTP challenge User-->>Prava: enter one-time code (sandbox: 456789) Prava->>User: register a new passkey User-->>Prava: biometric confirm (Touch ID / Face ID) else Returning browser/device — passkey already bound Prava->>User: verify with existing passkey User-->>Prava: biometric confirm (Touch ID / Face ID) end Prava->>Issuer: register mandate + tokenize Prava-->>You: (poll) token + dynamic_cvv You->>Issuer: 4. checkout with one-time credential You->>Prava: 5. POST …/report-status ``` ## The verification steps (step 2, expanded) What the cardholder sees between "enter card" and "credential issued" depends on whether **this browser/device has been used with this card before**. A passkey (WebAuthn / FIDO — Touch ID, Face ID) is always the final gate; the one-time OTP only appears the first time. Passkey registration and verification happen on **Card Network's own hosted page**. The cardholder authenticates with Card Network directly; neither you nor Prava render that page. Expect the hand-off — seeing Card Network's domain is what makes the approval verifiable rather than claimed. **Returning browser/device.** A passkey is already bound to this browser for this card, so the cardholder just **verifies with the existing passkey** — one biometric prompt, no OTP. This is the common case on repeat purchases. **New browser/device — first time (device binding).** There's no passkey on this browser yet, so Prava **binds the device**. Two steps, **in this order**: The card issuer sends a **one-time code** — the same 3-D Secure style step-up your bank does when it texts you a code. The cardholder enters it before anything else. **In sandbox**, enter the test code **`456789`** with any [test card](/api-reference/test-cards). Real codes only exist in production. Once the OTP checks out, Prava **registers a new passkey** (biometric — Touch ID / Face ID), bound to this browser/device. The signed passkey is what proves the cardholder approved *this* transaction. Passkeys are **bound per browser/device**, so the same cardholder on a new browser or device repeats device binding (OTP → new passkey) — that's the "device binding" referenced in [Guardrails](/concepts/guardrails): a security property, not a bug. For where issuer step-up sits in the regulatory picture, see [Compliance](/guides/compliance). After verification succeeds, Prava registers a [mandate](/concepts/mandates) with the network and [tokenizes](/concepts/payments) the card into the one-time credential you receive in step 3. None of that needs an API call from you. ## What you see from your side You never call anything during verification — you just poll [Get Payment Result](/api-reference/get-payment-result). The [transaction status](/concepts/payments#transactions) walks: | Status | What the cardholder is doing | | - | - | | `pending` | Hasn't opened the surface / started card entry yet | | `processing` | Entering card, verifying or registering a passkey (plus a one-time OTP on a new device); credentials not ready | | `awaiting_result` | Verified — credentials issued, checkout in progress | | `completed` / `failed` | Outcome reported back via [Report Status](/api-reference/report-status) | If `payment-result` sits at `pending` forever, the cardholder simply hasn't finished this sequence — see the [Developer FAQ](/developer-faq). ## Next Mandates, tokens, and the money-side machinery in full. The card numbers and the `456789` OTP to run the whole thing in sandbox. # Glossary Source: https://docs.prava.space/concepts/glossary Every Prava term in one place — and how the same thing is named across the API, CLI, and dashboard. ## Same thing, different names Prava's surfaces sometimes use different names for the same value. This table is the map: | Concept | REST API | Prava Pay CLI | Prose / Concepts | | - | - | - | - | | One-time virtual card number | `token` | `Token` | Payment token (virtual PAN) | | One-time security code | `dynamic_cvv` | `Cryptogram` | Single-use CVV | | Card expiry for the token | `expiry_month` + `expiry_year` | `Expiry` (`MM/YYYY`) | Token expiry | | Payment container | session (`session_id`, `ses_…`) | session (`--session-id`) | Session | | Spending permission | mandate (`/v1/mandates`) | `prava mandate` | Mandate | ## The portals **Two portals, two audiences.** [dashboard.prava.space](https://dashboard.prava.space) is the [**developer console**](/dashboard): sign up, create API keys, switch to production. [pay.prava.space](https://pay.prava.space) is the [**Prava Pay dashboard**](/prava-pay/your-wallet) for agent *owners*: approve agent links, enroll cards, set spending controls. A developer integrating the API only needs the console; an agent owner only needs the Prava Pay dashboard. ## Core terms | Term | Definition | | - | - | | **Session** | The container for one payment: customer + merchant + order, created by `POST /v1/sessions` (or `prava sessions create`). Expires after 15 minutes. | | **Transaction** | A single payment attempt within a session (`addCard` or `savedCard` flow). | | **Passkey** | WebAuthn biometric/device approval (Touch ID, Face ID, security key). Required for every payment; no fallback. | | **Mandate** | A standing card-network spending permission: merchant, amount cap, frequency (`one_time` or recurring `weekly`/`monthly`/`yearly`), duration. Approved once with a passkey, then charged within caps. See [Mandates](/concepts/mandates). | | **Payment token** | The single-use virtual card credentials (number + CVV + expiry) issued against an active mandate: merchant-locked, amount-scoped, short-lived. | | **Enrollment** | Securely collecting and tokenizing a card (via [`collectPAN`](/sdk/cards/collect-pan) or a hosted page). Yields an `enrollmentId`. | | **Agentic commerce enrollment** | A card flag (`isAgenticCommerceEnrolled`) enabling AI-initiated purchases with network-level (Visa) merchant/amount locking. | | **Agent owner** | The person/business whose account holds the cards and who approves agents; manages everything at [pay.prava.space](https://pay.prava.space). | | **Agent linking** | Connecting an agent to an owner's account: `prava setup` → owner approves in the browser → agent is `active` (or later `revoked`). | | **Quote / checkout (shopping)** | In agentic shopping, a quote locks a price (\~15 min) for a discovered product; checkout completes the purchase. See [Agentic Shopping](/prava-pay/shopping). | | **UCP** | Shopify's Universal Commerce Protocol: product search, catalogs, and quotes across participating merchants. See [UCP](/integration/ucp). | | **Browser Harness** | Prava's checkout automation: confirms the true final total on the live merchant checkout and pays with the one-time token. See [Browser Harness](/integration/browser-harness). | ## Key types | Key | Prefix | Lives | Used for | | - | - | - | - | | Publishable key | `pk_test_*` / `pk_live_*` | Frontend | Initializing the SDK | | Secret key | `sk_test_*` / `sk_live_*` | Backend only | Sessions, results, card management | | Session token | — | Per-session | Authenticates in-session operations (SDK/iframe) | ## Session vs mandate vs credential Easy to conflate; they nest: 1. A **session** (15 min) is the workspace for one payment. 2. Inside it, passkey approval creates a **mandate**, the permission (merchant + max amount + duration). 3. Against the mandate, Prava issues the **payment token**, the actual single-use card credentials you check out with. Expiry of one doesn't imply the others: a token can expire while its mandate is active (getting a new token requires a new invocation), and a mandate can outlive the card-entry window. # Guardrails Source: https://docs.prava.space/concepts/guardrails Rule-based controls that constrain what AI agents can spend and where. ## Guardrails **Guardrails** are rule-based controls enforced at multiple levels to ensure AI agents cannot exceed their authorized scope. They operate across four enforcement layers: ### 0. Owner-Set Account Controls Before any payment mechanics apply, the account owner's own controls do. These are set from the [Prava Pay dashboard](https://pay.prava.space) and enforced by Prava on every purchase: * **Checkout quota**: a set number of checkouts allowed per owner, consumed as agents buy. * **Concurrency limit**: caps how many checkouts can be open at once. * **Per-purchase approval**: every spend needs explicit approval before it happens. * **Agent revocation**: revoking an agent cuts off its access immediately. See [Accounts & Agents](/concepts/accounts) for the full model. ### 1. Mandate-Level Constraints Every payment intent creates a **mandate**, a spending permission registered at the card network level. The mandate itself is a guardrail. It specifies: | Constraint | What it controls | | - | - | | **Merchant** | The merchant the mandate is scoped to | | **Amount threshold** | Maximum amount for the transaction | | **Frequency** | `one_time`, or recurring `weekly`/`monthly`/`yearly` (one charge per cycle, single-merchant) | | **Effective duration** | When the mandate expires (`expiresAt`) | | **Product scope** | The products in the purchase (via mandate line items) | The mandate's **amount** is enforced at the card-network level through the tokenized credential (the substitute card number issued for the purchase): a transaction outside the mandate amount is declined. The remaining constraints are applied by Prava. ### 2. Session-Level Controls Each session is scoped to: * A specific **merchant** and **order** * A **time limit**: sessions expire and cannot be reused * **Idempotency**: duplicate transactions within a session are detected and rejected ### 3. Authentication Controls * **Passkey (WebAuthn)**: a biometric or security-key approval on the user's device, required for every intent mutation (register, update, delete). Prevents unauthorized agents from creating mandates. * **Device binding**: passkeys are registered per browser, so a passkey on one device cannot be used on another. * **WebAuthn required**: if the user's device does not support WebAuthn (passkeys), transactions cannot be performed. There is no fallback mechanism; this ensures the highest level of authentication security. ## Defense in Depth ``` Owner controls (quota, approval) → User approval (Passkey) → Mandate (amount-scoped) → Session (time-scoped) → one-time credential ``` Every layer must pass for a payment to succeed. An AI agent with a valid mandate but an expired session is blocked; a transaction outside the mandate amount is declined. This layered approach ensures no single point of failure. # How Prava Works Source: https://docs.prava.space/concepts/how-it-works The lifecycle behind every Prava payment — from intent to a completed checkout. Prava sits between the party that wants to pay (an app or an AI agent) and the merchant. Its job is to keep the sensitive parts (the real card, the spending rules, the final authorization) out of the caller's hands, while still letting the payment go through. ## The core idea An app or agent never holds a real card number. Instead it works with **permission to spend** and, at the moment of purchase, a **one-time credential** that only works for that exact purchase. ```mermaid theme={null} flowchart LR A["App or AI agent
proposes a purchase"] B["Prava
secure card entry
owner's limits applied
network authorization"] C["Merchant
checkout completes"] A -->|purchase request| B -->|one-time credential| C ``` ## The lifecycle An app creates a **session** for a specific order, or an agent creates a payment for a known merchant and total. The amount and merchant are pinned up front. The cardholder enters their card in Prava's secure surface: an embedded iframe (SDK) or a hosted page (Prava Pay). The raw card number never reaches the app, the agent, or their servers. Prava checks the purchase against the account's guardrails (spending limits, approvals, saved addresses) before anything is charged. See [Guardrails](/concepts/guardrails). Prava returns credentials scoped to that single purchase: locked to the merchant, the amount, and a short time window. They can't be reused or repurposed. The credential is used at the merchant's checkout to complete the payment. The outcome is confirmed from the payment itself, so the status you get back is the real one. ## Why it's safe by design The card is entered in Prava's secure surface and tokenized (the number is replaced with a secure stand-in). Apps and agents never see it. What the caller receives works for **one** purchase: right merchant, right amount, short-lived. Spending limits and approvals are enforced by Prava on every purchase, not by the caller. If an outcome is ever uncertain, Prava declines to guess rather than risk charging twice. ## Three ways to integrate The same lifecycle powers all three integration paths: | | [Embedded](/sdk/integration-modes) | [Hosted](/sdk/integration-modes#hosted-mode-default) | [Agent](/prava-pay/overview) | | - | - | - | - | | Runs in | Your web app (SDK) | Your backend (API) | An AI agent, via [MCP](/mcp/overview) or the CLI | | Card entry | Iframe inside your page | Prava-hosted page (redirect) | Prava-hosted page | | Best for | Native checkout in your product | Fast, minimal-frontend checkout | Agents buying on someone's behalf | Not sure which to pick? See [Choosing Your Integration](/choosing-your-integration). For the money-side details (mandates, credential scoping, and status), see [Payments](/concepts/payments). For the controls an owner sets, see [Guardrails](/concepts/guardrails). # Mandates Source: https://docs.prava.space/concepts/mandates Standing spend authorizations — approve once with a passkey, let an agent charge later within caps. A **mandate** is a standing spending authorization. The owner approves it **once** with a passkey; afterwards an agent can charge that card **again and again within caps, without re-approval**. It is the "authorize now, charge later" counterpart to a one-shot [payment session](/prava-pay/sessions). Reach for a mandate when spend repeats or is deferred — a monthly budget at one merchant, a "buy it when it's back in stock" instruction, an agent that pays several times against a fixed allowance. For a single known purchase, a [payment session](/prava-pay/sessions) is simpler. ## Setup vs. charge A mandate has two phases, and only the first involves the human. **Setup (once, with a passkey).** A mandate is created through a payment **session** carrying a `mandate_setup` block — there is no standalone create endpoint. Over REST, that's [Create Session → Mandate setup](/api-reference/create-session#mandate-setup). The response is an approval URL; the owner opens it and confirms with their passkey. That single approval is the standing consent. **Charge (later, no passkey).** Against an active mandate, an agent mints fresh [single-use credentials](/concepts/payments) with **no** new passkey — as many times as the caps allow. Charging is available through the [CLI](/prava-pay/mandates#prava-mandate-charge) and the [REST API](/api-reference/mandate-charge); it is deliberately **not** exposed over MCP. The passkey approval at setup is the only human-in-the-loop step. Every later charge is authorized by that original approval, bounded by the caps below. ## Lifecycle A mandate moves through these statuses: | Status | Meaning | | - | - | | `pending` | Created; awaiting the owner's passkey approval and network confirmation | | `active` | Live and chargeable | | `paused` | Temporarily suspended by the owner; no charges until resumed | | `consumed` | A one-time mandate whose single charge has settled | | `cancelled` | Revoked (by owner or agent); terminal | | `expired` | Past its validity window | ```mermaid theme={null} stateDiagram-v2 [*] --> pending: create (passkey pending) pending --> active: approved active --> paused: pause paused --> active: resume active --> consumed: one-time charge settled active --> cancelled: cancel paused --> cancelled: cancel active --> expired: window lapses ``` Recurring mandates stay `active` across cycles; a one-time mandate becomes `consumed` after its charge settles. Illegal transitions (e.g. resuming a cancelled mandate) are refused with `409 MANDATE_INVALID_TRANSITION`. ## Guardrails A mandate is itself a guardrail. Every charge is bounded by: | Guardrail | What it controls | | - | - | | **Amount cap** | Maximum per charge. Enforced at the card-network level through the tokenized credential — an over-cap charge is declined. | | **Merchant scope** | `listed` locks the mandate to one named merchant; `any` allows any merchant (one-time only). A charge at a non-allowed merchant is refused (`403 MANDATE_MERCHANT_NOT_ALLOWED`). | | **Frequency** | `one_time`, or recurring `weekly` / `monthly` / `yearly` — one charge per cycle, always locked to a single merchant. | | **Validity window** | One-time mandates are valid up to **7 days**; recurring mandates run for a multi-cycle horizon. | Recurring *frequencies* are available today: approve once, and the agent charges within caps each cycle without a fresh passkey. **Scheduled subscriptions** — where Prava auto-charges on the cycle — are coming next; for now the agent still initiates each charge within the cycle. This sits inside Prava's [layered guardrails](/concepts/guardrails): owner account controls, then the passkey approval, then the mandate's own caps, then the single-use credential each charge produces. ## Where each surface fits | Surface | Use it to | Reference | | - | - | - | | **CLI** | Set up, charge, and settle mandates from an agent | [Mandates (CLI)](/prava-pay/mandates) | | **MCP** | Create, list, and manage mandates from an MCP client (no charging) | [Mandate tools](/mcp/tools#mandates) | | **REST API** | Create (through session creation), charge, report, and manage server-side | [Create a mandate](/api-reference/create-session#mandate-setup) · [Charge a mandate](/api-reference/mandate-charge) | | **Dashboard** | View mandates, spend, and charge history; pause / resume / cancel | [pay.prava.space](https://pay.prava.space) | Over REST, mandate creation is part of the Session APIs — create one through [Create Session](/api-reference/create-session#mandate-setup) with a `mandate_setup` block. # Payments Source: https://docs.prava.space/concepts/payments Understand the payment lifecycle — from session creation to settlement. ## The non-quote checkout lifecycle you drive For a non-quote checkout, a complete payment is **three API calls and one hand-off**. Everything else on this page is what Prava does for you in between: ```mermaid theme={null} flowchart LR A["1 Create session
POST /v1/sessions"] --> B["2 Card entry
Prava's secure surface"] B --> C["3 Payment result
GET …/payment-result"] C --> D["4 Checkout
use token at merchant"] D --> E["5 Report status
POST …/report-status"] ``` | Step | What happens | Who does it | | - | - | - | | **1. Session** | Pin the order: customer, merchant, amount, line items | Your backend ([Create Session](/api-reference/create-session)) | | **2. Card entry** | Cardholder enters the card + approves with a passkey | The user, on Prava's surface (embedded [`collectPAN`](/sdk/cards/collect-pan) or hosted page) | | **3. Payment result** | Receive the one-time credentials (`token` + `dynamic_cvv`) | Your backend ([Get Payment Result](/api-reference/get-payment-result)) | | **4. Checkout** | Use the credentials like a normal card at the merchant | Your app / AI agent | | **5. Report status** | Tell Prava the outcome (`APPROVED` / `DECLINED`) | Your backend ([Report Status](/api-reference/report-status)) | In a quote-backed checkout, Prava performs the merchant checkout. Poll the same payment-result endpoint for the normalized top-level `status`, `error`, and `transactions[].card_id`. When merchant lifecycle data exists, `merchant_res` adds its status and optional merchant order ID; token, CVV, expiry, and line-item credentials are not returned, and quote callers do not call the public Report Status endpoint. ### How a payment starts: two examples **Human-in-the-loop (how every payment works today).** A user tells their agent "buy this $34 book from Bookshop". The agent (or your backend) creates a session for exactly $34 at Bookshop. The user gets Prava's secure surface, enters or picks their card, and approves with a passkey. Only then does a one-time credential exist, and it works only for \$34 at Bookshop. No approval, no credential, no charge. **Recurring.** The same shape, approved once: a user approves "up to \$12/month at this merchant" a single time, and the agent charges monthly within the limits without a fresh passkey per charge. This ships today as a [mandate](/concepts/mandates). *Scheduled* subscriptions — where Prava auto-charges each cycle — are coming next; for now the agent initiates each charge within the cycle. ### 1. Session A **session** is the starting point. Created server-to-server by the merchant using a secret key (`POST /v1/sessions`), it bundles: * **Customer identity**: `user_id`, `user_email` * **Order details**: `total_amount`, `currency`, product line items * **Merchant context**: merchant name, URL, country code * **Purchase context**: product descriptions, unit prices, quantities The session returns a `session_token` and `iframe_url` used to initialize the SDK on the frontend (or to redirect the cardholder in hosted mode). Sessions expire after **15 minutes**. ## What Prava does for you in the middle Between card entry (step 2) and the payment result (step 3), Prava runs the secure machinery below. **None of it requires an API call from you.** It's described here so you can interpret the states you see in [payment results](/api-reference/get-payment-result) and understand the guarantees. ### Transactions A **transaction** represents a single payment attempt within a session. It can be one of two flow types: | Flow Type | Description | | - | - | | `addCard` | User enrolls a new card and pays in one flow | | `savedCard` | User pays with a previously enrolled card | Each transaction is deduplicated using a deterministic idempotency key derived from the order, flow type, and card fingerprint, so retrying the same request cannot cause a double charge. **Top-level payment-result statuses:** | Status | Meaning | | - | - | | `pending` | Session created; no transaction started yet | | `processing` | Payment in flight — a non-quote checkout is authenticating or tokenizing, or a quote-backed merchant checkout is not yet terminal | | `awaiting_result` | Non-quote checkout credentials issued; awaiting your [status report](/api-reference/report-status) | | `completed` | Payment authorized and completed | | `failed` | Payment failed (declined, timeout, or error) | In a quote-backed result, `transactions[].status` can remain `awaiting_result` while the top-level status is `processing`, `completed`, or `failed`; the top-level status is the normalized checkout outcome. ### Authentication (FIDO / Passkey) Before a payment can be authorized, the user authenticates via **Passkey** (WebAuthn) on Prava's surface: Prava's backend generates a challenge, and the user confirms via biometric (Touch ID, Face ID) or security key. The signed assertion proves the user explicitly approved the transaction. ### Mandates A **mandate** is a card-network-level spending permission. Once the user authenticates, Prava registers a mandate with the card network that specifies: * **Merchant**: who can charge the card * **Amount threshold**: maximum per-transaction amount * **Frequency**: `one_time`, or recurring `weekly` / `monthly` / `yearly` * **Effective duration**: how long the mandate remains active (`effective_until_minutes`, default 15) Each mandate also carries **line items** (product IDs, descriptions, unit prices, quantities) that break down the purchase at a product level; the amount threshold and quantity limit apply at the **mandate level**, not per line item. For **standing** mandates — approve once, then charge repeatedly within caps — see [Mandates](/concepts/mandates). **Mandate statuses** (surfaced in payment results and status reports): | Status | Meaning | | - | - | | `pending` | Created, awaiting network confirmation | | `active` | Live and usable for payment token generation | | `paused` | Temporarily suspended by the owner; no charges until resumed | | `consumed` | Fully used (all allowed invocations exhausted) | | `cancelled` | Revoked by user or system | | `expired` | Past its effective date | ### Payment tokens — the non-quote checkout credential you receive Against an active mandate, Prava generates **payment tokens**: a virtual card number (PAN), expiry, and CVV, scoped to the mandate constraints. These are what step 3 hands back for non-quote checkouts: * **Single-use**: each token can only be used once * **Merchant-locked & amount-scoped**: enforced at the network level and by Prava; transactions outside the mandate are declined * **Short-lived**: use tokens promptly after they're issued Same credential, different names: the API calls these `token` + `dynamic_cvv`; the Prava Pay CLI prints `Token` + `Cryptogram`. See the [Glossary](/concepts/glossary). ## Merchant Network & Shopify App Prava integrates with merchant platforms and card networks to complete payments. The **Prava Shopify app** is available by invite: **This step needs Prava's help — everything else is self-serve.** Email [support@prava.space](mailto:support@prava.space) with your account email, your entity/merchant name, and what you're requesting. We'll take it from there. ## Settlement & Refunds * **Settlement** follows standard card network flows. Prava supports multiple settlement models; details are confirmed during merchant onboarding. * **Refunds** follow standard refund flows and can be issued through the API. * **Disputes** are routed to the responsible parties as per the settlement agreement. # Your Developer Dashboard Source: https://docs.prava.space/dashboard What dashboard.prava.space is for: keys, environments, and your payment activity. The **Prava Dashboard** at [dashboard.prava.space](https://dashboard.prava.space) is the console for **developers and merchants** integrating Prava. Everything your integration needs from Prava's side lives here, and all of it is self-serve. **Two portals, two audiences.** [dashboard.prava.space](https://dashboard.prava.space) is the [**developer console**](/dashboard): sign up, create API keys, switch to production. [pay.prava.space](https://pay.prava.space) is the [**Prava Pay dashboard**](/prava-pay/your-wallet) for agent *owners*: approve agent links, enroll cards, set spending controls. A developer integrating the API only needs the console; an agent owner only needs the Prava Pay dashboard. ## What you do here | Task | Where it fits | | - | - | | **Sign up** with email (one-time code) or Google | First step of the [Quickstart](/quickstart) | | **Create API keys** — enter your entity name, get `pk_test_*` + `sk_test_*` instantly | [Authentication](/authentication) explains each key | | **Switch environments** — sandbox and production are separate views with separate keys | [Testing in Sandbox](/api-reference/testing) · [Go-Live Checklist](/guides/go-live-checklist) | | **See your payment activity** — transactions and orders as they flow through your integration | Statuses match the [payment lifecycle](/concepts/payments) | | **Go live** — enable production; may require entity verification | [Compliance & Verification](/guides/compliance) | ## Sandbox first, production when ready New accounts start in **sandbox**: keys work immediately against `sandbox.api.prava.space`, no waiting and no verification. Production is a separate environment you enable from the dashboard when you're ready; going live may require verification of your entity ([what's needed](/guides/compliance)). ## Not the dashboard you're looking for? If your agent pays with **your own card** and you want to approve agents, enroll cards, or set spending limits, that's the **Prava Pay dashboard** at [pay.prava.space](https://pay.prava.space) — see [Your Prava Pay Dashboard](/prava-pay/your-wallet). ## Next Sign up, create keys, and make your first session. Everything between sandbox and production. # Developer FAQ Source: https://docs.prava.space/developer-faq Common SDK and REST API integration questions, answered. *Questions about agentic commerce, merchants, or refunds? See the [Agentic Commerce FAQs](/integration/faqs). CLI issues? See [Prava Pay Troubleshooting](/prava-pay/troubleshooting).* ## Keys & environments **Which key goes where?** `pk_*` (publishable) initializes the SDK in the browser; `sk_*` (secret) stays server-side for sessions, results, and card management. Full table: [Authentication](/authentication). **My key returns `AUTH_1001` but it's correct.** Check environment pairing: `sk_test_*` only works against `sandbox.api.prava.space`, `sk_live_*` only against `api.prava.space`. **Where do I get keys?** Self-serve at [dashboard.prava.space](https://dashboard.prava.space); see the [Quickstart](/quickstart). No invite or waiting. ## Sessions **How long does a session last?** 15 minutes (`expires_at` in the create response). Create sessions when the user is ready to pay; once expired, create a new one rather than reusing it. **The checkout page says "Authentication Failed" out of nowhere.** Check `expires_at` first — a session that expires mid-flow can surface as a generic authentication error instead of a "Session Expired" screen. If the session is past its 15 minutes, it isn't an auth problem: create a fresh session and retry before debugging keys or passkeys. **Can I create multiple sessions per user?** Technically yes, but create one per checkout flow and complete or [revoke](/api-reference/revoke-session) it before starting another. **Does the session `currency` have to match the card's country?** No. `currency` is your pricing currency and is independent of where the card was issued — a US-issued card can pay an INR-denominated session, and the network settles the conversion. `country_code_iso2` on the session is the **merchant's** location, not the cardholder's. Price in your own currency; no re-denomination needed. **`payment-result` stays `pending` forever.** The cardholder hasn't completed card entry + passkey approval (Touch ID / Face ID) on the Prava surface. Confirm they opened the `iframe_url` (hosted) or that [`collectPAN`](/sdk/cards/collect-pan) mounted without errors (embedded). ## The iframe / collectPAN **The iframe won't load (`IFRAME_LOAD_ERROR`).** Verify you passed the `iframe_url` from the session response verbatim. **`Session preview failed: 404`.** The `?session=` value in the iframe URL isn't a valid `session_id`. Two causes: (a) you rebuilt the URL and passed the `session_token` (JWT) instead of using `iframe_url` verbatim — the identifier in `?session=` must be the `session_id` (`sess_…`); (b) environment mismatch, e.g. a **sandbox** session opened on the **prod** collect host (or vice-versa). **Can I style the card form?** The card fields live inside Prava's PCI-scoped iframe and aren't arbitrarily styleable from your page. Position and size the container; the iframe handles the rest. **Do I ever touch the card number?** No. You receive `{ enrollmentId, last4, brand, expMonth, expYear }`, never the PAN (the full card number). That's what keeps you out of PCI scope, the security compliance that applies once you handle card data. ## Tokens & checkout **Can I reuse a token?** No — payment tokens are single-use, merchant-locked, amount-scoped, and short-lived. If a checkout fails, report `DECLINED` and start a new session. **What's a `dynamic_cvv`?** The single-use CVV paired with the token (the CLI calls it a `Cryptogram`). Terminology map: [Glossary](/concepts/glossary). **Do I have to call report-status?** Yes, always — `APPROVED` or `DECLINED`. It closes the loop with transaction records and the card network. Unreported checkouts leave the transaction in `awaiting_result`. ## Going further **Webhooks?** Coming soon: configuration exists today, delivery is rolling out. Poll [Get Payment Result](/api-reference/get-payment-result) meanwhile. See [Authentication → Webhooks](/authentication#webhooks). **Test cards?** Published on [Test Cards](/api-reference/test-cards), together with the test OTP, organized by card network. Sandbox only. **Pricing?** Contact [support@prava.space](mailto:support@prava.space) for current pricing. **Something not covered here?** [support@prava.space](mailto:support@prava.space); include the `X-Response-ID` header of any failing request. # Add Payments to Your AI App Source: https://docs.prava.space/guides/add-payments-to-your-ai-app The end-to-end tutorial: from a fresh dashboard account to a completed, reported payment inside your own application. This is the complete integration, start to finish. When you're done, your AI app can take a purchase your agent proposes, collect the user's card **without touching it**, receive one-time credentials, and complete the checkout. **What you'll build:** one backend endpoint that creates sessions, one frontend surface that collects the card, and one backend callback that reads the result and reports the outcome. Prefer zero frontend code? The [REST checkout walkthrough](/guides/rest-checkout-walkthrough) does the same flow with hosted card entry and nothing but cURL. ## Prerequisites (all self-serve) 1. Sign up at [dashboard.prava.space](https://dashboard.prava.space) and create an API key; you get `pk_test_*` + `sk_test_*` instantly ([Quickstart](/quickstart) has the details). 2. Install the SDK: `npm install @prava-sdk/core`. Building with an AI coding agent? Install the **prava-sdk-integration skill** and your agent knows this whole flow, with ready-made templates: ```bash theme={null} npx --yes skills add https://github.com/Prava-Payments/prava-skills \ --skill prava-sdk-integration --global --yes --full-depth ``` ## Step 1 — Backend: create a session Your server pins the order and returns the session to your frontend. The secret key never leaves the server. ```typescript Next.js (app router) theme={null} // app/api/prava/session/route.ts export async function POST(req: Request) { const { userId, email, amount, merchant, items } = await req.json(); const res = await fetch('https://sandbox.api.prava.space/v1/sessions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.PRAVA_SECRET_KEY}`, // sk_test_… 'Content-Type': 'application/json', }, body: JSON.stringify({ user_id: userId, user_email: email, total_amount: amount, // e.g. "49.99" currency: 'USD', integration_type: 'embedding', // card UI inside your page purchase_context: { custom: [{ merchant_details: merchant, // destination merchant, not your app: { name, url, country_code_iso2 } product_details: items, // [{ description, unit_price, quantity }] }], }, }), }); const session = await res.json(); // → { session_id, session_token, iframe_url, order_id, expires_at } return Response.json({ sessionId: session.session_id, sessionToken: session.session_token, iframeUrl: session.iframe_url, }); } ``` ```typescript Express theme={null} // server.ts import express from 'express'; const app = express(); app.use(express.json()); app.post('/api/prava/session', async (req, res) => { const { userId, email, amount, merchant, items } = req.body; const r = await fetch('https://sandbox.api.prava.space/v1/sessions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.PRAVA_SECRET_KEY}`, // sk_test_… 'Content-Type': 'application/json', }, body: JSON.stringify({ user_id: userId, user_email: email, total_amount: amount, currency: 'USD', integration_type: 'embedding', purchase_context: { custom: [{ merchant_details: merchant, product_details: items, }], }, }), }); const session = await r.json(); res.json({ sessionId: session.session_id, sessionToken: session.session_token, iframeUrl: session.iframe_url, }); }); app.listen(3000); ``` Full request schema: [Create Session](/api-reference/create-session). Sessions expire after **15 minutes**: create them when the user is ready to pay, not earlier. ## Step 2 — Frontend: collect the card Mount Prava's PCI-compliant iframe in your page. The raw card number never touches your DOM, JavaScript, or servers. ```tsx React theme={null} import { useEffect, useRef } from 'react'; import { PravaSDK } from '@prava-sdk/core'; export function PayWithPrava({ order }: { order: OrderInput }) { const prava = useRef(new PravaSDK({ publishableKey: 'pk_test_xxx' })); useEffect(() => () => prava.current.destroy(), []); // cleanup on unmount async function collect() { const { sessionId, sessionToken, iframeUrl } = await fetch('/api/prava/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(order), }).then((r) => r.json()); await prava.current.collectPAN({ sessionToken, iframeUrl, container: '#card-form', onSuccess: async (card) => { // card = { enrollmentId, last4, brand, expMonth, expYear } await fetch(`/api/prava/complete?sessionId=${sessionId}`, { method: 'POST' }); }, onError: (err) => console.error(err.code, err.message), }); } return ( <>
); } ``` ```html Vanilla JS theme={null}
``` The user enters the card and approves with a **passkey** (Touch ID / Face ID) inside the iframe. Prava handles the authentication, mandate registration (recording the user's permission to pay), and tokenization (swapping the real card for one-time credentials); [none of that is your code](/concepts/payments#what-prava-does-for-you-in-the-middle). Every option and callback: [Collect Card Details](/sdk/cards/collect-pan). ## Step 3 — Backend: get the credentials and check out After collection, poll the payment result. When a line item reaches `awaiting_result` it carries the one-time credentials. ```typescript theme={null} // Poll until the credentials are ready (they arrive right after card entry completes) async function getCredentials(sessionId: string) { const r = await fetch( `https://sandbox.api.prava.space/v1/sessions/${sessionId}/payment-result`, { headers: { Authorization: `Bearer ${process.env.PRAVA_SECRET_KEY}` } }, ); const result = await r.json(); if (result.status !== 'awaiting_result') return null; // keep polling ('pending') const transaction = result.transactions[0]; const li = transaction.line_items[0]; return { cardId: transaction.card_id, // source-of-truth card bound to this transaction txnRefId: li.txn_ref_id, cardNumber: li.token, // one-time virtual card number cvv: li.dynamic_cvv, // single-use CVV expMonth: li.expiry_month, expYear: li.expiry_year, }; } ``` Use these exactly like normal card details at the merchant's checkout: your agent fills the payment form, or your backend calls the merchant's payment API. They're **single-use, merchant-locked, and amount-scoped**, so use them promptly. ## Step 4 — Backend: report the outcome Always close the loop, whether the checkout succeeded or failed: ```typescript theme={null} await fetch( `https://sandbox.api.prava.space/v1/sessions/${sessionId}/report-status`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.PRAVA_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ txn_ref_id: credentials.txnRefId, txn_status: 'APPROVED', // or 'DECLINED' if the checkout failed }), }, ); ``` This updates transaction records and relays the outcome to the card network; the payment result's `status` becomes `completed`. Details: [Report Status](/api-reference/report-status). ## Step 5 — Test it Run the whole flow in sandbox with a [test card](/api-reference/test-cards). See [Testing in Sandbox](/api-reference/testing) for the checklist and what behaves differently. ## Ship it Swap to live keys and what needs Prava's sign-off. Every code you might hit along this flow, with recovery steps. # Compliance & Verification Source: https://docs.prava.space/guides/compliance What verification each integration path needs, and what Prava is (and is not) as a regulated matter. Two questions come up before production: "what do you need from me?" and "what exactly is Prava, legally?" Both answers are short. ## Verification by integration path ### SDK / API integrators: KYB KYB (Know Your Business) is business identity verification. If you integrate the [SDK + API](/sdk/overview) and take live payments in your product, your business gets verified when you [switch to production](/guides/go-live-checklist). Have these ready (indicative; we confirm the exact list during verification): * Legal entity name and country of incorporation * Registration / incorporation number and date * Registered business address * Beneficial owners (name, ownership stake) * Business website and a working contact Sandbox needs none of this. Build and test first; verify when you go live. Specifics: [support@prava.space](mailto:support@prava.space). ### MCP / CLI users: KYC via card verification KYC (Know Your Customer) is personal identity verification. If you connect an agent through [MCP](/mcp/overview) or the [CLI](/prava-pay/overview), there is no separate KYC form. Identity verification is inherited from your card: when you enroll it, the card is verified with your issuing bank (including OTP verification, a one-time code, where the issuer requires it), and every payment is approved with a passkey (Touch ID / Face ID) on your device. Your bank has already verified you; Prava builds on that. ## What Prava is — and is not From our [Terms & Conditions](https://www.prava.space/terms-conditions): > Prava is not a bank, card issuer, acquirer, payment processor, money transmitter, stored value > provider, or deposit-taking institution. Prava is a **technology service provider for agentic commerce**, operated by **Prava Payments Inc** (Delaware, USA). Payment processing, authorization, clearing, settlement, money movement, refunds, and chargebacks are handled by the card networks, banks, and licensed processors in the flow. Prava never holds your money. On data security: * Prava maintains **PCI DSS Level 2 / SAQ-D** compliance (PCI DSS is the card industry's data-security standard) and uses **Skyflow, a PCI DSS Level 1 vault provider**, for card vaulting. * Prava does not store raw card numbers or CVV in its application databases. * AI agents and apps never receive the user's underlying card number or CVV. Full texts: [Terms & Conditions](https://www.prava.space/terms-conditions) · [Privacy Policy](https://www.prava.space/privacypolicy) · [Security](https://www.prava.space/security) # Go-Live Checklist Source: https://docs.prava.space/guides/go-live-checklist Everything between a working sandbox integration and live payments — and exactly which steps need Prava. You've completed the flow in sandbox ([tutorial](/guides/add-payments-to-your-ai-app) · [REST walkthrough](/guides/rest-checkout-walkthrough)). Here's the full path to production. ## The checklist Run one complete payment in sandbox: session → card entry → passkey → credentials → checkout → report-status → `completed`. If any step surprises you, fix it now; [Errors](/api-reference/errors) has every code. Production is a separate environment you enable at [dashboard.prava.space](https://dashboard.prava.space) (the [developer dashboard](/dashboard)). Going live may require additional verification of your entity: **This step needs Prava's help — everything else is self-serve.** Email [support@prava.space](mailto:support@prava.space) with your account email, your entity/merchant name, and what you're requesting. We'll take it from there. | | Sandbox | Production | | - | - | - | | Keys | `pk_test_*` / `sk_test_*` | `pk_live_*` / `sk_live_*` | | Base URL | `https://sandbox.api.prava.space` | `https://api.prava.space` | Keys are environment-bound: a `sk_test_*` key against the production host fails with `AUTH_1001`. `sk_live_*` lives only in server-side env/secret storage, never in client code, version control, or logs. Rotate immediately if exposed. * Report **every** outcome via [Report Status](/api-reference/report-status), including `DECLINED`. * Handle session expiry (15 min): create sessions late, and re-create rather than reuse. * Log the `X-Response-ID` header from every Prava response; it's how support traces issues. ## Quick self-audit | Check | Where | | - | - | | One full sandbox payment reached `completed` | [Testing in Sandbox](/api-reference/testing) | | Production enabled + entity verified | dashboard.prava.space | | Live keys deployed server-side only | your infra | | Declines and expiry handled + reported | your code | Questions at any step: [support@prava.space](mailto:support@prava.space). Include the `X-Response-ID` of any failing request. # REST Checkout Walkthrough Source: https://docs.prava.space/guides/rest-checkout-walkthrough The complete payment flow with nothing but cURL — hosted card entry, zero frontend code. The fastest way to see a full Prava payment: **hosted mode**, where Prava serves the card-entry page and your side is three HTTP calls. Everything below is copy-pasteable; swap in your own `sk_test_*` key. ## 1. Create the session ```bash theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions \ -H "Authorization: Bearer sk_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "user_id": "user_123", "user_email": "user@example.com", "total_amount": "49.99", "currency": "USD", "integration_type": "full_checkout", "callback_url": "https://yourapp.com/payment-done", "purchase_context": { "custom": [{ "merchant_details": { "name": "Example Store", "url": "https://examplestore.com", "country_code_iso2": "US" }, "product_details": [{ "description": "Wireless Headphones", "unit_price": "49.99", "quantity": 1 }] }] } }' ``` Response: ```json theme={null} { "session_id": "ses_abc123", "session_token": "eyJhbGciOi…", "iframe_url": "https://sandbox.collect.prava.space?session=…", "order_id": "ord_xyz789", "expires_at": "2026-01-01T00:15:00Z" } ``` The session expires at `expires_at`, **15 minutes** after creation. Don't create it until the cardholder is ready. ## 2. Send the cardholder to the hosted page Redirect (or link) the cardholder to the `iframe_url`. On Prava's hosted page they enter the card and approve with a **passkey** (Touch ID / Face ID); Prava then redirects them to your `callback_url`. No SDK, no frontend code, no card data anywhere near you. On a **new browser/device** the cardholder is first asked for an **issuer OTP** (one-time code) and then to **register a passkey** — in that order (this is device binding). On a **returning** browser they just verify their existing passkey, with no OTP. In sandbox the test OTP is `456789`. Full sequence: [Anatomy of a Checkout](/concepts/checkout-flow). Behind the scenes Prava runs authentication, mandate registration (recording the cardholder's permission to pay), and tokenization (swapping the real card for one-time credentials): [what Prava does for you](/concepts/payments#what-prava-does-for-you-in-the-middle). ## 3. Poll the payment result ```bash theme={null} curl https://sandbox.api.prava.space/v1/sessions/ses_abc123/payment-result \ -H "Authorization: Bearer sk_test_xxx" ``` While the cardholder is still on the hosted page you'll see `"status": "pending"`. Once they finish: ```json theme={null} { "session_id": "ses_abc123", "order_id": "ord_xyz789", "status": "awaiting_result", "transactions": [{ "txn_id": "txn_001", "card_id": "card_42", "status": "awaiting_result", "line_items": [{ "txn_ref_id": "tli_001", "merchant_name": "Example Store", "merchant_url": "https://examplestore.com", "total_amount": "49.99", "status": "credentials_generated", "token": "4323126882557932", "dynamic_cvv": "957", "expiry_month": "12", "expiry_year": "2028", "products": [{ "product_ref_id": "prd_001", "external_product_id": null, "name": "Wireless Headphones", "unit_price": "49.99", "quantity": 1 }] }] }] } ``` `card_id` is the source of truth for the card bound to the transaction, whether it was preselected or chosen by the cardholder. `token` + `dynamic_cvv` + expiry are the **one-time card credentials**. Use them at the merchant's checkout like a normal card; they're single-use, merchant-locked, and amount-scoped. ## 4. Report the outcome After the checkout runs (note the `txn_ref_id` from step 3): ```bash theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions/ses_abc123/report-status \ -H "Authorization: Bearer sk_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "txn_ref_id": "tli_001", "txn_status": "APPROVED" }' ``` Report `DECLINED` if the checkout failed; either way, always report. Re-poll the payment result and the status is now `"completed"` (or `"failed"`). ## That's the whole flow ``` create session → cardholder pays on hosted page → poll result → checkout with token → report status ``` The same flow with Prava's card UI inside your own page. What you need to run this walkthrough end to end. # Introduction Source: https://docs.prava.space/index An overview of the Prava Payments API Prava — the payment stack for agentic checkout Prava turns a user's permission into a one-time, merchant-scoped card credential, so your agent can check out anywhere without ever touching raw card data. In-chat, in-voice, or any AI-native interface. Go from zero to a working session in minutes. ## What do you want to do? The interface is an AI agent (OpenClaw, Hermes, ChatGPT). Connect Prava Pay via MCP (Model Context Protocol) or the CLI. No card data, no crypto. You own the interface. Embed the card UI in your page (SDK) or redirect to a Prava-hosted page (API). Pick your integration path. You're a merchant or marketplace. Let agents discover, quote, and check out, with network-level enforcement. See Agentic Commerce. Your agent pays with your card. See exactly what it can and can't do, and the controls that keep you in charge. No code required. See how Prava works in a live demo. No setup required. ## The core ideas in 60 seconds Think of Prava as a **programmable payment proxy**. Instead of your AI agent handling raw credit card numbers (which creates massive compliance and security risks), it handles **permissions to spend**: approved by the user with a passkey and exchanged for a one-time, scoped credential. It asks to buy a specific thing: merchant, amount, constraints. The user approves on their device with a **Passkey** (Touch ID / Face ID). Prava trades that permission with the **card network** for a one-time, merchant-specific credential. The agent uses that ephemeral credential to complete the checkout at the merchant. This decouples the *agent's ability to buy* from the *user's sensitive data*. Four terms carry the whole system. A **session** is the workspace for one payment. A **passkey** is the user's biometric approval (Touch ID / Face ID). A **mandate** is the resulting permission: this merchant, up to this amount, for this long. A **payment token** is the single-use card credential issued against it. Deeper reading: [How Prava Works](/concepts/how-it-works) · [Payments](/concepts/payments) · [Glossary](/concepts/glossary). ## What Prava provides PCI scope is the security compliance you take on when handling card data. No sensitive card data exposure. Your agent never sees real card numbers. Users authorize specific purchases via Passkey authentication. Merchant-specific, amount-scoped credentials enforced at the card network level. Cards can be enrolled for AI-agent-initiated transactions with network-level enforcement. Works with any merchant checkout: Stripe, Braintree, Adyen, or custom PSPs. A single SDK for complex payment orchestration. ## Who benefits This documentation is for **developers building AI-powered applications** that need to make purchases on behalf of users. For the commerce capabilities behind that (discovery, quoting, and checkout), see [Agentic Commerce](/integration/overview). * AI shopping/stylist apps (in-chat discovery → buy) * Travel and booking agents (flight/hotel reservations) * Personal assistant apps (scheduling, recurring purchases) * Marketplaces and merchants who want to accept purchases triggered by LLMs or agents ## Start here Three paths, decided in 10 seconds. Go from zero to a working session. The full flow, end to end. # Browser Harness Source: https://docs.prava.space/integration/browser-harness Prava's checkout automation completes a real merchant checkout and confirms the true total before charging. The **Browser Harness** is the second half of the agentic-commerce pipeline: it **starts from a [UCP](/integration/ucp) quote** and turns that estimate into a placed, verified order. Most merchants don't expose a direct payment API, so to actually place an order, someone has to go through the merchant's own checkout. The Browser Harness does exactly that: it drives the real Shopify checkout, works out the true final total, and pays with the one-time tokenized card (a single-use stand-in for the real card). An AI application completes the purchase without ever touching the raw card or the merchant's checkout code. The Browser Harness is the **checkout** step after a UCP [quote](/integration/ucp). It powers [`prava shop checkout`](/prava-pay/shopping#checkout) and hosted agentic purchases. ## What it does Fills the merchant's checkout (contact, shipping address, delivery option) and submits payment with the one-time token. No merchant integration required. Reconciles subtotal, shipping, and tax against the live checkout and waits for the amount to settle **before** charging, so there are no surprise totals. On success you get an **order id** and a payment **status**: a checkable confirmation the order was placed. It learns a merchant's checkout on the first run and completes later runs consistently, healing automatically if the page changes. ## Why it matters The amount is **reconciled and stable** before payment is attempted. If the total shifts mid-checkout (late shipping, tax, or add-ons), reconciliation restarts rather than charging the wrong amount. A checkout runs as a single, tracked flow rather than a blind retry loop, so a purchase either completes and returns an order, or fails cleanly for you to re-quote. It pays with the **single-use, merchant-scoped** credential from the session. Even inside the checkout, the credential can't be reused or redirected to another merchant. ## How it fits the flow ```mermaid theme={null} flowchart LR U["UCP quote
price the item"] B["Browser Harness
confirm true total"] R["Order placed
order id + status"] U --> B -->|pays with one-time token| R ``` The [UCP](/integration/ucp) quote gives an estimate; the Browser Harness turns it into a **completed, verified order**. Today it targets **Shopify** checkouts. ## Next How products are discovered and quoted before checkout. The full discover → quote → checkout → pay pipeline. # Dining & Reservations Source: https://docs.prava.space/integration/dining Agent-booked tables and food orders. Coming soon. **Coming soon.** Dining is on the Prava roadmap and is not live yet. The plan: your agent books tables and orders food through platforms such as OpenTable and Toast, paying deposits or bills with one-time, amount-locked credentials. Same approval model as every Prava flow: the agent proposes, you approve with a passkey, Prava executes. Food delivery is already possible today in supported regions via merchant skills (for example Swiggy and Zepto) paired with [Prava Pay](/prava-pay/overview). Building in dining and want early access? [support@prava.space](mailto:support@prava.space). # Commerce & Merchant FAQ Source: https://docs.prava.space/integration/faqs Common questions about agentic commerce, merchants, refunds, and security. *This FAQ covers **Agentic Commerce** (UCP + Browser Harness) and merchant questions. For SDK/REST API integration questions, see the [Developer FAQ](/developer-faq).* ## General **Q: Do merchants need to change their checkout?** A: No. Prava tokenizes payments and routes them to the merchant through existing card/acquirer flows. The merchant's checkout remains unchanged: Prava's payment tokens work like regular card details at any standard checkout. **Q: Who is the merchant-of-record (MoR)?** A: The merchant-of-record is the merchant providing goods or services to the user; that is, the merchant where the AI application is performing the transaction. Prava acts as a payment infrastructure layer and does not take on MoR responsibilities. **Q: What about refunds and disputes?** A: Refunds follow the same flow as existing payment rails. The merchant can issue refunds directly through their existing systems, or the AI application can initiate refunds on behalf of the user through the merchant. Prava does not expose separate refund endpoints. Disputes are handled through the standard card network dispute process. **Q: How does card data stay secure?** A: Card data is collected via a PCI DSS compliant iframe served from Prava's domain. Raw card numbers never touch your servers, DOM, or JavaScript. All tokenization happens in an isolated, sandboxed iframe with origin-locked PostMessage communication. **Q: What happens if a payment token is used at the wrong merchant?** A: The card network will decline the transaction. Payment tokens are merchant-locked: they can only be used at the merchant specified in the mandate (the user-approved authorization that fixes a payment's merchant, amount, and frequency). This is enforced both at the network level and by Prava. **Q: Can AI agents make purchases without user approval?** A: No. Every payment requires explicit user authentication via Passkey (biometric/device confirmation). The agent cannot bypass this step. Once a mandate is approved, the agent can invoke it within the mandate's constraints (amount, merchant, frequency), but the user approved those constraints upfront. **Q: What card networks are supported?** A: Prava supports major card networks. Contact us at [support@prava.space](mailto:support@prava.space) for the latest list of supported networks. ## Technical **Q: What currencies does Prava support?** A: Prava supports 47+ currencies including USD, EUR, GBP, INR, CAD, AUD, JPY, SGD, AED, and many more. The full list is validated at session creation: if you pass an unsupported ISO 4217 currency code, you'll receive a validation error. **Q: How long does a session last?** A: Sessions expire after **15 minutes** by default. The `expires_at` field in the [Create Session](/api-reference/create-session) response tells you exactly when. Once expired, the session token can no longer be used for any SDK operations. You can also configure mandate duration using the `effective_until_minutes` field in `purchase_context.custom` (also defaults to 15 minutes). **Q: What happens if the user closes the browser mid-flow?** A: The session remains valid until it expires. If the user returns within the session window, they can resume. If they need to start over, create a new session. Duplicate transactions are prevented by idempotency: the system generates a deterministic key from the order, flow type, and card, so retries won't create duplicate charges. **Q: What happens if the user's device doesn't support passkeys (WebAuthn)?** A: Transactions cannot be performed without WebAuthn support. Passkey authentication is a hard requirement; there is no fallback mechanism. This ensures the highest level of security for user authorization. Supported devices include those with Touch ID, Face ID, Windows Hello, or a FIDO2 security key. **Q: Are payment tokens single-use?** A: Yes. Each payment token (virtual card number + CVV) can be used exactly **once**. If checkout fails, you must invoke the intent again to get a fresh token (subject to the mandate's constraints). Tokens are also merchant-locked, amount-scoped, and time-limited. **Q: How does mandate expiry work?** A: When a session is created, the `effective_until_minutes` field (default: 15 minutes) sets how long the mandate is valid. Prava checks mandate expiry when you call [Get Payment Result](/api-reference/get-payment-result) or [Report Status](/api-reference/report-status). If the mandate has expired, the status will be reflected in the response and further invocations will be rejected. **Q: Can I create multiple sessions for the same user at the same time?** A: Technically yes, but it's not recommended. Each session is independent with its own token and expiry. In practice, create one session per checkout flow and either complete it or revoke it before creating another. **Q: Is there idempotency protection for transactions?** A: Yes. Each transaction gets a deterministic idempotency key derived from the order ID, flow type, and card identifier. If a duplicate request is detected, the existing transaction is returned instead of creating a new one. This prevents double charges from network retries or repeated submissions. **Q: What is the maximum number of products per session?** A: There is no hard upper limit. You must include at least 1 product in the `product_details` array. However, only 1 entry in `purchase_context.custom` (i.e., one merchant) is supported per session; multi-merchant sessions are not currently available. ## Merchants **Q: Are you a Shopify merchant?** A: Prava can already reach Shopify stores through [UCP](/integration/ucp) (Shopify's agent-commerce protocol) and the [Browser Harness](/integration/browser-harness), with no setup needed on your end. If you'd like a direct integration, the **Prava Shopify app** is available by **invite only**. Contact **[support@prava.space](mailto:support@prava.space)** to request merchant onboarding. ## Support * **Developer Support**: [support@prava.space](mailto:support@prava.space) # For Merchants Source: https://docs.prava.space/integration/merchants Selling to AI shoppers: what already works with zero changes, and what to request from Prava. Agents paying with Prava look like **normal card customers** to you. This page tells you what works today with no integration at all, and what to ask Prava for if you want more. ## Works today — zero changes on your end Prava's payment tokens are real card credentials: they work at any standard checkout through your existing payment service provider, or PSP (Stripe, Braintree, Adyen, custom). Nothing to install, nothing to change. If you're a Shopify store advertising [UCP](/integration/ucp) (Shopify's protocol that lets agents shop a store), agents can already discover your products, get quotes, and complete checkout via the [Browser Harness](/integration/browser-harness). No setup on your end. Payments arrive through your normal card/acquirer flow: you stay merchant-of-record, refunds and disputes work exactly as they do today. Details: [FAQs](/integration/faqs). ## By request — email us Two things are currently provisioned by Prava per merchant: | What | Why you'd want it | How to get it | | - | - | - | | **Prava Shopify app** (direct integration, invite-only) | Native order attribution and richer agent checkout on your Shopify store | Request "Shopify app onboarding" | | **Settlement model confirmation** | Confirmed during merchant onboarding | Ask when onboarding | **This step needs Prava's help — everything else is self-serve.** Email [support@prava.space](mailto:support@prava.space) with your account email, your entity/merchant name, and what you're requesting. We'll take it from there. When you write, include: your store name and URL, your platform (Shopify or other), and roughly the volume of agent-driven orders you expect. That's everything we need to start. ## How an agent purchase reaches you ```mermaid theme={null} flowchart LR A["Agent
discovers & quotes
(UCP)"] --> B["Browser Harness
confirms true total
at your checkout"] B --> C["Your checkout
one-time card credential
via your existing PSP"] C --> D["You
order + payment,
business as usual"] ``` The credential the agent pays with is **single-use, locked to you, and amount-scoped**: an agent can't reuse it elsewhere or change the total after you quoted it. ## Next The full discover → quote → checkout → pay pipeline. MoR, refunds, disputes, security, supported currencies. # Agentic Commerce Source: https://docs.prava.space/integration/overview The commerce capabilities that let an AI application discover, quote, checkout, and pay. Prava is more than a payment token. It provides the **commerce capabilities** an AI application needs to turn an intent to buy into a completed order: discovering products, pricing them accurately, and completing a real checkout on the merchant's site. ## The capability pipeline ```mermaid theme={null} flowchart LR D["Discover
UCP search & catalog"] Q["Quote
live price, reconciled"] C["Checkout
Browser Harness"] P["Pay
one-time tokenized card"] D --> Q --> C --> P ``` Two capabilities do the heavy lifting, and they work as a pair: Search products, read catalogs, and open quotes across participating Shopify merchants via the Universal Commerce Protocol. Prava's checkout automation confirms the true total from a UCP quote and completes the real merchant checkout. ## Who this is for AI applications and agents that want to **find and buy** on a user's behalf rather than only process a payment. These same capabilities power [Prava Pay's agentic shopping](/prava-pay/shopping) (`prava shop search → product → quote → checkout`); this section explains what's behind those commands. Whichever surface you use, the card is always collected on Prava's secure surface and turned into a one-time, merchant-scoped credential; the AI application never handles a raw card. ## How the pieces fit Search across participating Shopify merchants and read product/variant details. See [UCP](/integration/ucp). UCP opens a checkout for an initial price; the [Browser Harness](/integration/browser-harness) reconciles it against the live checkout so the amount is correct before anyone commits. The Browser Harness fills the checkout, confirms the final total, pays with the one-time token, and returns a verifiable order. # Travel Source: https://docs.prava.space/integration/travel Agent-booked flights, hotels, and trips. Coming soon. **Coming soon.** Travel booking is on the Prava roadmap and is not live yet. The plan: your agent searches and books travel (flights, hotels, packages) through providers such as Duffel and lastminute, and pays with the same one-time, amount-locked credentials used everywhere else in Prava. Same approval model: the agent proposes the itinerary and total, you approve with a passkey, the booking completes. Until then, agents can already pay travel merchants that accept standard card checkout using a [payment session](/prava-pay/sessions) for a known total. Building in travel and want early access? [support@prava.space](mailto:support@prava.space). # UCP Integration Source: https://docs.prava.space/integration/ucp How Prava discovers and prices products across Shopify merchants with the Universal Commerce Protocol. **UCP (Universal Commerce Protocol)** is Shopify's agentic-commerce protocol: a standard way for an agent to **search products, read catalogs, and open a checkout** across participating Shopify merchants. Prava integrates with UCP so an AI application can discover *what* to buy and get an initial price, before the [Browser Harness](/integration/browser-harness) confirms the real total and completes the order. UCP is Shopify-specific. Any Shopify store that advertises UCP is reachable through it. ## What UCP gives you Query a single merchant, or Shopify's **global catalog** across all participating merchants at once. Full product details: variants, options (size/color), pricing, availability, and images. Open a checkout and get an initial price (subtotal, tax estimate, and shipping options where the merchant exposes them). ## How Prava integrates with UCP Prava speaks UCP to Shopify stores that advertise it, and layers a consistent discovery-to-purchase pipeline on top: A Shopify store that advertises UCP exposes a commerce endpoint Prava can call. Prava can target **one merchant** directly, or search Shopify's **global catalog** spanning every participating merchant with a single query. Prava runs the search, normalizes the results into a consistent shape (product id, title, price estimate, image, merchant), and fetches a product's **variants** (options, prices, and availability) so the exact item can be chosen. For the chosen variant and destination, Prava asks UCP to **open a checkout**. That returns a checkout and an **initial proposal**: subtotal, a tax estimate, and (for merchants that expose it) shipping options. This is an *estimate*, not the binding total. Prava drives that live checkout with the [Browser Harness](/integration/browser-harness), which reads and **reconciles the true final total** (shipping + tax settled) and completes the purchase with the one-time tokenized card. This is the first half of the [pipeline](/integration/overview): exactly the chain the CLI exposes as [`prava shop search → product → quote → checkout`](/prava-pay/shopping). ## Supported merchants UCP reaches **participating Shopify merchants** (for example Fashion Nova, SKIMS, Alo Yoga, Steve Madden, Glossier, Everlane, Harry's, and Ridge) plus any Shopify store that advertises UCP. **Coverage varies by merchant.** Some merchants expose shipping options and tax through UCP; others don't. Treat UCP prices as an **estimate**: the **binding final total** is always confirmed at checkout by the [Browser Harness](/integration/browser-harness) before any charge. ## Next How the true total is confirmed and the order is completed from a UCP quote. Run this discovery-to-purchase flow from the command line. # Connect Your Agent Source: https://docs.prava.space/mcp/connect Add the Prava MCP to your client, sign in once, and your agent can pay. Connecting takes about a minute. You add the server URL to your MCP client, the client opens a browser window, and you sign in to approve. No API keys, no manual registration: the server supports automatic client registration (OAuth 2.1 dynamic client registration with PKCE), so any standard MCP client can connect itself. ## Add the server ```bash Claude Code theme={null} claude mcp add --transport http prava https://mcp.pay.prava.space/mcp ``` ```json Cursor (mcp.json) theme={null} { "mcpServers": { "prava": { "url": "https://mcp.pay.prava.space/mcp" } } } ``` ```text Any MCP client theme={null} Server URL: https://mcp.pay.prava.space/mcp Transport: Streamable HTTP Auth: OAuth (automatic — the client discovers and registers itself) ``` ## Sign in and approve 1. Your client opens a browser window to Prava's sign-in. 2. Sign in with your **Prava Pay account** (the same account as [pay.prava.space](https://pay.prava.space); create one there if you're new). 3. Review the requested scopes (`payments:read`, `payments:write`, `checkout:run`) and approve. 4. Done. The connection appears as an agent in your [Prava Pay dashboard](https://pay.prava.space), alongside any CLI-linked agents. Ask your agent to call the `ping` tool to confirm the connection. First time with Prava? You'll also add a card and a delivery address. Your agent can walk you through both: card entry happens on Prava's secure page (via a `payment_url`), and addresses via the `shop_add_address` tool. The agent never sees the card. ## Staying connected * **Tokens refresh automatically.** The client holds a refresh token; you won't re-approve on every session. * **Revoking access**: revoke the agent in your [Prava Pay dashboard](https://pay.prava.space). Its tokens stop working immediately. * **Reconnecting**: remove and re-add the server in your client, then sign in again. ## Troubleshooting | Symptom | Cause | Fix | | - | - | - | | `401` / "Missing or malformed Authorization" | The client isn't sending a bearer token | Re-run the OAuth flow (remove and re-add the server) | | `403 insufficient_scope` on a tool call | The token lacks that tool's scope | Reconnect and approve all requested scopes | | "This token has no agent context" | The connection wasn't fully approved | Reconnect; finish the sign-in and approval step | | Tool calls fail after you revoked in the dashboard | Expected: revocation is immediate | Reconnect if it was a mistake | Still stuck? [support@prava.space](mailto:support@prava.space). ## Next What each tool does and the order to call them in. Approvals, cards, spending controls, revocation. # Prava MCP Overview Source: https://docs.prava.space/mcp/overview Connect any MCP-capable AI agent to Prava payments. No CLI, no SDK, no card data. The **Prava MCP** is a hosted [Model Context Protocol](https://modelcontextprotocol.io) server. MCP is the standard way to give an AI agent new abilities: you add a server URL to your agent, and the agent gains that server's tools. Adding Prava's server gives your agent the ability to pay and shop with your card, under your approval. ``` https://mcp.pay.prava.space/mcp ``` Works with any MCP client: Claude Code, Cursor, Poke, or your own app. Setup is one URL and one sign-in. See [Connect](/mcp/connect). This is the **payments** MCP. It is not the same as the **docs** MCP (`docs.prava.space/mcp`), which only lets an agent read these docs. See [Use Prava Docs with AI](/ai). ## What your agent can do | Ability | Tools | | - | - | | Pay a known total at a named merchant | `create_payment_session`, `get_payment_status` | | Set up & manage standing mandates | `create_mandate`, `list_mandates`, `get_mandate`, `pause_mandate`, `resume_mandate`, `cancel_mandate` | | Find and buy products | `shop_search`, `shop_product`, `shop_quote`, `shop_checkout` | | Manage delivery addresses | `shop_list_addresses`, `shop_add_address`, `shop_set_default_address` | | See your cards and agents | `list_cards`, `list_agents` | Full parameters and flows: [Tools reference](/mcp/tools). ## The credential firewall The MCP is built so that payment credentials never reach the agent. This is stronger than the CLI path, where the agent handles a one-time token itself. Example: your agent buys you a phone case. 1. The agent finds the case and locks a price (`shop_quote`): \$18.50. 2. It creates a payment session for exactly \$18.50 and sends you a `payment_url`. 3. You open the link and approve with your passkey (a fingerprint or face confirmation on your device). The agent only ever learns the *status*: `get_payment_status` returns `completed`, never the card token. 4. The agent calls `shop_checkout`. Prava's gateway pulls the one-time credentials server-side, completes the merchant checkout, and returns only the outcome: `{ status: "paid", order_id: … }`. At no point does the agent, the LLM, or the MCP client see a card number, token, or CVV. ## Scopes Access is scoped with OAuth. A scope is a named permission: your MCP client requests these when you connect, and you approve them once. | Scope | Grants | | - | - | | `payments:read` | Read cards (masked), agents, payment status, product search, masked addresses, and mandates | | `payments:write` | Lock quotes, add or change delivery addresses, and set up or manage mandates (pause / resume / cancel) | | `checkout:run` | Create payment sessions and execute checkouts | ## MCP, CLI, or SDK? | | **Prava MCP** | **[Prava CLI](/prava-pay/overview)** | **[Prava SDK](/sdk/overview)** | | - | - | - | - | | You are | An agent owner or agent-platform user | An agent owner on a machine with a terminal | A developer building an AI app | | Setup | Add one URL, sign in | Install `@prava-sdk/cli`, link via browser | API keys + backend + frontend | | Agent sees credentials | Never (gateway-side) | Yes, one-time scoped token | Your backend handles them | | Best when | Your agent platform supports MCP | Your agent runs shell commands | You own the product surface | All three share the same account, cards, guardrails, and approval model. An agent connected via MCP shows up in your [Prava Pay dashboard](https://pay.prava.space) next to CLI-linked agents, and you can revoke it the same way. ## Next One URL, one sign-in. Claude Code, Cursor, and generic clients. Every tool, its parameters, and the buy-flow chain. # Tools Reference Source: https://docs.prava.space/mcp/tools Every Prava MCP tool: parameters, scopes, and the order to call them in. The server exposes 18 tools. Your MCP client discovers them automatically; this page is the map of what they do and how they chain together. ## The buy-flow chain To buy a product, tools must run in this order. The key rule: `create_payment_session` authorizes payment but never places an order. Only `shop_checkout` buys. ```mermaid theme={null} flowchart LR A[shop_search] --> B[shop_product] --> C[shop_quote] C --> D[create_payment_session] D --> E{{User approves
payment_url}} E --> F[shop_checkout] ``` 1. `shop_search` finds products. `shop_product` gets the exact variant. 2. `shop_quote` locks a live total and returns a `checkout_session_id`. 3. `create_payment_session` for that exact total returns a `payment_url`. 4. The user opens the `payment_url` and approves with their passkey (a biometric confirmation on their device). 5. `shop_checkout` places the order. Credentials stay server-side. Paying a known total at a named merchant (a bill, an invoice, a checkout the user is already on) skips discovery: `create_payment_session` → user approves → done. ## Payments ### create\_payment\_session — `checkout:run` Creates a payment session and returns a `payment_url` for the user to approve. Charges nothing by itself. | Parameter | Type | Notes | | - | - | - | | `total_amount` | string, required | Decimal string, e.g. `"49.99"` | | `currency` | string, required | 3-letter ISO 4217, e.g. `USD` | | `merchant_name` | string, required | Display name shown to the user | | `merchant_url` | string, required | Merchant website URL | | `merchant_country` | string, required | 2-letter ISO 3166-1, e.g. `US` | | `products` | array, required | Line items: `{ description, unit_price, quantity? }` | | `idempotency_key` | string, optional | Same key returns the original session instead of a duplicate | Returns `{ session_id, payment_url, expires_at, replayed }`. ### get\_payment\_status — `payments:read` Checks a payment session. Returns **status only**: `pending`, `completed`, `failed`, or `not_found`. Payment credentials never leave the gateway. | Parameter | Type | Notes | | - | - | - | | `session_id` | string, required | From `create_payment_session` | ## Mandates Manage [standing mandates](/concepts/mandates) — spend authorizations the owner approves once with a passkey, then an agent charges within caps. Setting one up returns an approval URL; it charges nothing by itself. **Charging a mandate is not available over MCP** — use the [CLI](/prava-pay/mandates#prava-mandate-charge) or the [REST API](/api-reference/mandate-charge). ### create\_mandate — `payments:write` Starts mandate setup and returns an `approval_url` for the owner to approve with a passkey. Spends nothing. | Parameter | Type | Notes | | - | - | - | | `amount` | string, required | Per-charge cap, e.g. `"120.00"` | | `currency` | string, required | 3-letter ISO 4217, e.g. `USD` | | `merchant_name` | string | Required unless `merchant_scope` is `any` | | `merchant_url` | string | Required unless `merchant_scope` is `any` | | `merchant_country` | string | 2-letter ISO 3166-1, e.g. `US` | | `recurring_frequency` | string, optional | `one_time` (default), `weekly`, `monthly`, `yearly` | | `merchant_scope` | string, optional | `listed` (default) or `any` (one-time only) | | `valid_until` | string, optional | ISO 8601; one-time is capped at 7 days | | `max_charges` | integer, optional | Maximum number of charges allowed against the mandate | | `products` | array, optional | Line items: `{ description, unit_price, quantity? }` | Returns `{ approval_url, ... }`. Send the URL to the owner; after they approve, call `list_mandates` to confirm the mandate is active before spending against it. ### list\_mandates — `payments:read` Returns the owner's **standing** mandates (transient per-checkout mandates are excluded). Each entry includes merchant, frequency, approved/remaining amount, status, and validity. No required parameters. ### get\_mandate — `payments:read` Returns one mandate with its spend and charge history. | Parameter | Type | Notes | | - | - | - | | `mandate_id` | string, required | From `list_mandates` (e.g. `mdt_…`) | ### cancel\_mandate — `payments:write` Revokes a mandate; stops all future charges. Terminal. | Parameter | Type | Notes | | - | - | - | | `mandate_id` | string, required | From `list_mandates` (e.g. `mdt_…`) | ### pause\_mandate — `payments:write` Temporarily suspends a mandate. No charges succeed until resumed. | Parameter | Type | Notes | | - | - | - | | `mandate_id` | string, required | From `list_mandates` (e.g. `mdt_…`) | ### resume\_mandate — `payments:write` Reactivates a paused mandate. | Parameter | Type | Notes | | - | - | - | | `mandate_id` | string, required | From `list_mandates` (e.g. `mdt_…`) | ## Shopping ### shop\_search — `payments:read` | Parameter | Type | Notes | | - | - | - | | `query` | string, required | e.g. `"running shoes size 10"` | | `merchant` | string, optional | Merchant domain to scope the search | | `cursor` | string, optional | Pagination cursor from a prior search | Returns product listings: `product_id`, price estimate, merchant. ### shop\_product — `payments:read` | Parameter | Type | Notes | | - | - | - | | `product_id` | string, required | From `shop_search` | | `merchant` | string, optional | Merchant domain from the search result | Returns purchasable offers and variants: `variant_id`, price, availability. ### shop\_quote — `payments:write` Opens a checkout and locks a live price. Requires a delivery address on file (check with `shop_list_addresses`). | Parameter | Type | Notes | | - | - | - | | `variant_id` | string, required | From `shop_product` | | `merchant` | string, required | Merchant domain from that offer | | `quantity` | number, optional | Defaults to 1 | | `address_id` | string, optional | Defaults to your default address | Returns `checkout_session_id` plus the exact total to pay. ### shop\_checkout — `checkout:run` The required final step. Places the order for a prior quote, paying with an **approved** payment session. The session's amount must match the quote total. | Parameter | Type | Notes | | - | - | - | | `checkout_session_id` | string, required | From `shop_quote` | | `payment_session_id` | string, required | An approved session from `create_payment_session` | Returns `{ status, order_id, amount, replayed }`. If the payment isn't approved yet, the tool says so; poll `get_payment_status` until `completed`, then retry. ## Addresses Address reads are masked. Full address details stay server-side and go only to the merchant. ### shop\_list\_addresses — `payments:read` No parameters. Returns masked summaries (id, label, short summary, which is default) plus whether a contact phone is on file. ### shop\_add\_address — `payments:write` | Parameter | Type | Notes | | - | - | - | | `first_name`, `last_name` | string, required | | | `street` | string, required | Line 1; `street2` optional | | `city`, `region`, `postal_code` | string, required | | | `country` | string, required | 2-letter ISO 3166-1 | | `label` | string, optional | e.g. `"Home"` | | `phone` | string, optional | Contact phone, saved to the profile | | `set_default` | boolean, optional | Make this the default | ### shop\_set\_default\_address — `payments:write` | Parameter | Type | Notes | | - | - | - | | `address_id` | string, required | From `shop_list_addresses` | ## Account ### list\_cards — `payments:read` No parameters. Returns the user's saved cards, masked: last4, brand, expiry. Never full numbers. ### list\_agents — `payments:read` No parameters. Returns the user's connected agents, including this connection. ### ping — no scope Health probe. Returns `{ pong: true }` and the server time. Use it to confirm the connection after [setup](/mcp/connect). # Linking an Agent Source: https://docs.prava.space/prava-pay/linking Connect an agent to a Prava account with a one-time, owner-approved link. Before an agent can pay, its owner has to **link and approve** it. Linking establishes a secure identity for the agent on this machine, and approval happens in the owner's browser, so nothing can spend money without a human explicitly allowing it. ## The linking lifecycle `prava setup` creates the agent's identity locally and prints a one-time approval URL. The owner opens the URL, signs in to Prava, and approves the agent. `prava setup poll` waits for the approval and finalizes the link. ## `prava setup` ```bash theme={null} prava setup --name "Claude Code" --platform claude-code --description "Anthropic's coding agent" ``` | Option | Required | Description | | - | - | - | | `--name ` | Yes | Human-friendly agent name shown on the approval screen (e.g. `"Claude Code"`). | | `--platform ` | Recommended | The agent's platform identifier (see below). | | `--description ` | No | Short description shown to the owner when approving. | It prints the approval URL and **exits immediately**; it does not block: ``` To link this agent, open this URL and approve: https://pay.prava.space/link-agent?lid=lk_xxxxxxxx Link expires in 15 minutes. Run `prava setup poll` to wait for approval. ``` Give the agent a **specific** name and platform (`"Claude Code"`, `claude-code`) rather than a generic `"My Agent"`. The owner sees this when deciding whether to approve. Use the identifier that matches the agent's runtime. If none fits, use `custom`. `claude-code` · `codex` · `cursor` · `gemini-cli` · `hermes` · `aider` · `goose` · `copilot-cli` · `github-copilot` · `windsurf` · `cline` · `continue` · `amazon-q` · `roo-code` · `kilo-code` · `sourcegraph-cody` · `tabnine` · `augment-code` · `amp` · `zed` · `kiro` · `blackbox` · `opencode` · `qwen-code` · `kimi-cli` · `mistral-vibe` · `warp` · `coro-code` · `devin` · `openhands` · `jules` · `swe-agent` · `manus` · `openai-operator` · `claude-computer-use` · `replit-agent` · `bolt` · `v0` · `lovable` · `custom` ## Approve in the browser The owner opens the printed URL, signs in at [pay.prava.space](https://pay.prava.space), and clicks **Approve**. The link is valid for **15 minutes**. If it lapses, run `prava setup` again. ## `prava setup poll` After the owner approves, confirm the link: ```bash theme={null} prava setup poll ``` ``` Waiting for approval of "Claude Code"... ... Linked! Agent ID: aa_7kMnP2 Ready to create sessions. ``` `poll` waits up to 15 minutes. Other outcomes: | Output | Meaning | | - | - | | `Setup denied by user.` | The owner rejected the agent in the browser. | | `Link expired. Run \`prava setup\` again.\` | The 15-minute window passed before approval. | | `No agent configured. Run: prava setup …` | No link was started on this machine yet. | ## `prava status` Check the current link state at any time: ```bash theme={null} prava status ``` A linked, active agent prints: ``` Agent: Claude Code (aa_7kMnP2) Status: active Linked: 2026-07-06 ``` Other states: ``` Agent: Claude Code Status: pending Link: https://pay.prava.space/link-agent?lid=lk_xxxxxxxx ``` Open the link and approve, then run `prava setup poll`. ``` Link expired. Run `prava setup` again. ``` Start over with `prava setup`. ``` No agent configured. Run: prava setup --name "" ``` Nothing is linked on this machine yet. Run `prava setup`. ## One agent per machine Each machine holds a **single** linked agent identity. That identity is stored locally in your home directory and is what proves requests come from *this* approved agent. * To **switch** to a different agent, remove the local identity and run `prava setup` again. * Keep the identity private: anyone who can read it can act as this agent (within the owner's guardrails). Prefer a non-default location for the identity file? Set `PRAVA_STATE_DIR` to a directory you control before running `prava setup`. ## Owner controls Approval isn't the end of the owner's control. From the Prava dashboard, an owner can **revoke** any agent (after which its requests stop working immediately) or suspend their whole account. Spending limits and saved addresses are configured there too, and Prava enforces them on every purchase. See [Guardrails](/concepts/guardrails). # Mandates Source: https://docs.prava.space/prava-pay/mandates Authorize a card once, then charge it later within caps — no passkey per charge. A [**mandate**](/concepts/mandates) lets an agent charge the owner's card **again and again within caps** after a single passkey approval. Use it for repeat or deferred spend; for a one-off purchase use a [payment session](/prava-pay/sessions) instead. The agent must be [linked](/prava-pay/linking) first. If it isn't, mandate commands exit with `Agent not linked. Run: prava setup --name ""`. The lifecycle is: **create** (owner approves once) → **poll** (wait for it to go active) → **charge** (mint credentials, no passkey) → **report** (settle the outcome). **cancel** revokes it. `list` shows what's authorized. ## `prava mandate create` Set up a mandate. This prints a passkey approval URL — it authorizes, it does not charge. ```bash theme={null} prava mandate create \ --merchant-name "Nike" \ --merchant-url "https://nike.com" \ --merchant-country US \ --amount 120.00 \ --currency USD \ --frequency one_time \ --scope listed \ -y ``` | Option | Required | Description | | - | - | - | | `--amount ` | Yes | Per-charge cap as a string, e.g. `"120.00"`. | | `--currency ` | Yes | ISO 4217 code, e.g. `USD`, `EUR`, `GBP`, `INR`. | | `--merchant-name ` | Unless `--scope any` | Merchant display name (shown to the owner). | | `--merchant-url ` | Unless `--scope any` | Full merchant URL **including `https://`**. | | `--merchant-country ` | Unless `--scope any` | ISO 3166-1 alpha-2 country, e.g. `US`. | | `--frequency ` | No | `one_time` (default), `weekly`, `monthly`, or `yearly`. | | `--scope ` | No | `listed` (default, one merchant) or `any` (any merchant). **Forced to `listed` for any recurring frequency.** | | `--product ` | No | A product line item as JSON. **Repeat** the flag per item. Defaults to a single synthesized item. | | `--valid-until ` | No | Validity hint (ISO 8601). One-time mandates are capped at 7 days regardless. | | `--max-charges ` | No | Advisory hint on the number of charges; the enforced ceiling is the amount cap, not this count. | | `-y, --yes` | No | Skip the confirmation prompt (required when running non-interactively). | | `--json` | No | Emit raw JSON instead of formatted output. | On success: ``` Mandate setup started. Ask the user to approve with their passkey: https://pay.prava.space/... One-time mandates are valid up to 7 days. After the user approves, run: prava mandate poll --merchant https://nike.com --amount 120.00 ``` Give the approval URL to the owner. They open it and approve with their passkey; that single approval authorizes every later charge within the caps. For a generic budget not tied to one merchant (e.g. "\$200 for groceries this week"), drop the three `--merchant-*` flags and add `--scope any`. Generic mandates are `one_time` only. ## `prava mandate poll` Wait for a just-created mandate to become active (i.e. for the owner to approve): ```bash theme={null} prava mandate poll --merchant https://nike.com --amount 120.00 ``` | Option | Required | Description | | - | - | - | | `--merchant ` | No | Disambiguate when polling among several mandates. | | `--amount ` | No | Disambiguate by authorized amount. | | `--json` | No | Emit raw JSON. | It waits up to **10 minutes**. Once the owner approves: ``` ✓ Mandate active — Nike, 120.00 USD, expires 2026-07-26. ``` ## `prava mandate list` List the mandates authorized on this account: ```bash theme={null} prava mandate list ``` | Option | Required | Description | | - | - | - | | `--merchant ` | No | Filter to one merchant (also matches `any`-scope mandates). | | `--json` | No | Emit raw JSON, **including mandate ids** for chaining into `charge`. | ``` • Nike — 120.00/120.00 USD, expires 2026-07-26 [available] • Any store — 150.00/200.00 USD, renews 2026-07-25 [active] ``` Each line reads ` — / , expires|renews []`. Use `--json` to get the mandate id you'll pass to `charge`. ## `prava mandate charge` Charge an approved mandate — **no passkey** — and receive single-use credentials. ```bash theme={null} prava mandate charge --mandate-id --amount 40.00 --reference order-8842 -y ``` | Option | Required | Description | | - | - | - | | `--mandate-id ` | Yes | The mandate id from `mandate list --json`. | | `--amount ` | Yes | Charge amount as a string; must be within the mandate's cap. | | `--reference ` | No | Idempotency key. The same `--mandate-id` + `--reference` returns the original charge instead of a duplicate. | | `--product ` | No | Per-charge product line item as JSON. **Repeat** per item. | | `-y, --yes` | No | Skip the confirmation prompt. | | `--json` | No | Emit raw JSON. | On success: ``` Card tokenized from mandate. Token: 4323126882557932 Cryptogram: 957 Expiry: 12/2028 Transaction: txn_9f2a Use these to check out at the merchant, then run: prava mandate report --mandate-id --txn-id txn_9f2a --status APPROVED ``` These credentials are **single-use and expire quickly**. Check out immediately; don't cache, log, or reuse them. A charge over the cap (or otherwise refused by the network) is a **normal** outcome — it prints `✗ Charge declined: THRESHOLD_EXCEEDED` (or similar) and exits `1`. ## `prava mandate report` Settle a charge outcome after checking out: ```bash theme={null} prava mandate report --mandate-id --txn-id txn_9f2a --status APPROVED ``` | Option | Required | Description | | - | - | - | | `--mandate-id ` | Yes | The mandate id. | | `--txn-id ` | Yes | The `Transaction` id from `charge`. | | `--status ` | Yes | `APPROVED` or `DECLINED`. | | `--authorization-code ` | No | The merchant's authorization code, if you have it. | | `--response-code ` | No | The network response code, if you have it. | | `--amount-paid ` | No | The amount actually captured. | | `--json` | No | Emit raw JSON. | ``` ✓ Charge reported. ``` ## `prava mandate cancel` Revoke a mandate. This stops all future charges and is terminal. ```bash theme={null} prava mandate cancel --mandate-id -y ``` | Option | Required | Description | | - | - | - | | `--mandate-id ` | Yes | The mandate id. | | `-y, --yes` | No | Skip the confirmation prompt. | | `--json` | No | Emit raw JSON. | ``` ✓ Mandate cancelled. ``` The owner can also revoke a mandate from the dashboard at [pay.prava.space](https://pay.prava.space). Cancelling stops future charges; past charges are unaffected. ## Exit codes Mandate commands use exit codes so an agent can branch reliably: | Code | Meaning | | - | - | | `0` | Success. | | `1` | Error or declined charge (e.g. `THRESHOLD_EXCEEDED`, timeout). | | `2` | Not linked / no agent configured, or confirmation required and not given; run [`prava setup`](/prava-pay/linking). | ## Common outcomes | Output | What it means | What to do | | - | - | - | | `Mandate setup started…` | Setup created; awaiting the owner's passkey approval. | Share the URL, then `prava mandate poll`. | | `✓ Mandate active …` | The owner approved; the mandate is chargeable. | Run `prava mandate charge` when you need to pay. | | `✗ Charge declined: THRESHOLD_EXCEEDED` | The charge exceeded the mandate's amount cap. | Charge within the remaining cap, or set up a new mandate. | | `Agent not linked. Run: prava setup …` | No linked agent on this machine. | [Link the agent](/prava-pay/linking) first. | Next: use the returned token to [complete a checkout](/prava-pay/shopping#checkout), then `report` the outcome. # Prava Pay Overview Source: https://docs.prava.space/prava-pay/overview Give an AI agent the ability to pay at real merchants — safely, from the command line. *Not sure this is the right path? See [Choosing Your Integration](/choosing-your-integration).* **Prava Pay** lets an AI agent (Claude Code, Cursor, Codex, or any coding agent) make real purchases on a person's behalf. This section covers the **CLI**: a single command-line tool, `@prava-sdk/cli`, that the agent runs directly. No servers to stand up, no card data to handle. Agent platform supports MCP? MCP is the standard protocol for giving an agent tools. You may not need the CLI at all: connect via the [Prava MCP](/mcp/overview) instead (one URL, same account and guardrails). Builders of autonomous or human-in-the-loop agents that need to **buy something** (order a product, pay a merchant, complete a checkout) without ever touching a raw card number. ## How it fits together Prava Pay sits between the agent and the merchant. The agent describes *what* it wants to buy; Prava handles the sensitive parts: collecting the card, issuing **one-time, merchant-scoped credentials**, and completing the payment. ```mermaid theme={null} flowchart LR A["Agent (CLI)"] P["Prava
secure card entry
owner's limits enforced"] M["Merchant
checkout completes"] A -->|buy this| P -->|one-time token| M ``` The person who owns the account (the **agent owner**) stays in control the whole time: * They **link and approve** an agent once, from their browser. An unapproved agent can do nothing. * They **enter card details themselves** in Prava's secure page. The agent never sees the real card. * They set the **guardrails** (spending limits, saved addresses) that Prava enforces on every purchase. The owner does all of this from the **Prava Pay dashboard** at [pay.prava.space](https://pay.prava.space): **Two portals, two audiences.** [dashboard.prava.space](https://dashboard.prava.space) is the [**developer console**](/dashboard): sign up, create API keys, switch to production. [pay.prava.space](https://pay.prava.space) is the [**Prava Pay dashboard**](/prava-pay/your-wallet) for agent *owners*: approve agent links, enroll cards, set spending controls. A developer integrating the API only needs the console; an agent owner only needs the Prava Pay dashboard. The agent only ever receives a **one-time payment credential** scoped to a single, already-priced purchase. It cannot reuse it, change the amount, or pay a different merchant. ## The two ways to pay Prava Pay supports two flows, depending on how much the agent needs to do: You already know the merchant, items, and total. Create a **payment session**, the owner enters the card, and you receive credentials to complete checkout. See [Payment sessions](/prava-pay/sessions). The agent **discovers** the product too: search across merchants, compare offers, get a live quote, then pay. See [Agentic shopping](/prava-pay/shopping). ## Install Prava Pay is a global CLI. The **owner** installs it once on the machine the agent runs on: ```bash theme={null} npm install -g @prava-sdk/cli ``` Verify the install: ```bash theme={null} prava --version # 3.0.3 ``` Always install the CLI **yourself** as the owner. Don't have the agent install or update it silently: installation is a trust action, and keeping it explicit keeps you in the loop. Alongside the CLI, install the matching **skill** so the agent knows which command to run when: ```bash theme={null} npx --yes skills add https://github.com/Prava-Payments/prava-skills \ --skill prava-shopping --global --yes --full-depth ``` ## Guided by Skills The CLI does the work; **Skills guide the agent on *which* command to call and *when*.** A skill encodes the correct flow, paces the agent (one decision per turn), and forces a confirmation before any spend, keeping the human in control. The CLI runs without a skill, but will nudge you to install one if it's missing. * **`prava-pay`** pays for a known item: `setup` → `sessions create` → `poll` → checkout. * **`prava-shopping`** finds *and* buys: `search` → `product` → `quote` → `checkout`. See **[Skills](/prava-pay/skills)** for the full list, install commands, and how they keep the agent safe. ## What you'll do next Run `prava setup`, approve the agent in your browser, and confirm the link. See [Linking an agent](/prava-pay/linking). Create a payment session (or shop for a product), enter your card, and let the agent complete the checkout. See the [Quickstart](/prava-pay/quickstart). Know what each message means and how to recover. See [Troubleshooting](/prava-pay/troubleshooting). ## Prava Pay vs. the Prava SDK Prava Pay (CLI) and the [Prava SDK](/sdk/overview) solve different problems: | | **Prava Pay (CLI)** | **Prava SDK** | | - | - | - | | Runs where | On the agent's machine | In a merchant's web app | | Audience | AI agents & their owners | Merchants embedding Prava at checkout | | Card entry | Owner enters it in Prava's page | Shopper enters it in an embedded iframe | | You get | A tool the agent calls | A JS library ([`collectPAN`](/sdk/cards/collect-pan)) | They share the same secure backbone. Pick Prava Pay when the **interface is owned by an AI agent**. # Quickstart Source: https://docs.prava.space/prava-pay/quickstart From zero to a completed purchase in one sitting. This walks through the **happy path** end to end: link an agent, create a payment session, enter a card, and complete a purchase. Every command and output below is exactly what the CLI prints. Steps marked **You** are done by the account owner (a human, in a browser). Everything else is run by the agent (or you, at a terminal). ## Prerequisites * The CLI installed: `npm install -g @prava-sdk/cli` (see [Overview](/prava-pay/overview)). * A Prava account at [pay.prava.space](https://pay.prava.space). ## 1. Link the agent ```bash theme={null} prava setup --name "Claude Code" --platform claude-code ``` The CLI prints a link and exits immediately: ``` To link this agent, open this URL and approve: https://pay.prava.space/link-agent?lid=lk_xxxxxxxx Link expires in 15 minutes. Run `prava setup poll` to wait for approval. ``` **You:** open that URL, sign in, and click **Approve**. Then wait for the approval to land: ```bash theme={null} prava setup poll ``` ``` Waiting for approval of "Claude Code"... .. Linked! Agent ID: aa_7kMnP2 Ready to create sessions. ``` Run `prava status` any time to confirm the agent is linked and active. ## 2. Create a payment session You know the merchant and items, so create a session for the exact order: ```bash theme={null} prava sessions create \ --total-amount "8.50" \ --currency USD \ --merchant-name "Blue Bottle Coffee" \ --merchant-url "https://bluebottlecoffee.com" \ --merchant-country US \ --product '{"description":"Latte","unit_price":"5.00","quantity":1}' \ --product '{"description":"Croissant","unit_price":"3.50","quantity":1}' ``` ``` Session created. Session ID: ses_abc123 Payment URL: https://collect.prava.space?session=… Share this URL to complete card entry. Run `prava sessions poll --session-id ses_abc123` to wait for card entry. ``` ## 3. Enter the card **You:** open the **Payment URL**, enter your card in Prava's secure page, and confirm. Meanwhile, the agent waits for the card to be tokenized (swapped for a one-time card number and code that stand in for the real card): ```bash theme={null} prava sessions poll --session-id ses_abc123 ``` ``` Waiting for card entry on session ses_abc123... .... Card tokenized. Token: 4323126882557932 Cryptogram: 957 Expiry: 12/2028 ``` These credentials are **single-use and short-lived**: use them right away to complete the checkout. Don't store or reuse them. ## 4. Complete the checkout Use the token and cryptogram at the merchant's checkout to finish the purchase. If you're using **agentic shopping** (Prava discovered the product), the last step is a single command: ```bash theme={null} prava shop checkout \ --checkout-session-id ches_xxx \ --token 4323126882557932 \ --cryptogram 957 \ --expiry-month 12 --expiry-year 2028 \ --yes ``` ``` ✓ Paid. Amount: $8.50 USD Order: ord_abc123 ``` That's the whole loop. 🎉 ## Where to go next The full linking lifecycle, platforms, and status states. Every flag for `sessions create` and `sessions poll`. Search, compare, quote, and pay: the discovery flow. What each message means and how to recover. # Payment Sessions Source: https://docs.prava.space/prava-pay/sessions Create a payment for a known order and receive one-time card credentials. A **payment session** is the direct way to pay when you already know the merchant, the items, and the total. You create the session, the owner enters their card in Prava's secure page, and you receive **one-time credentials** to complete the checkout. Use this flow when *you* found the product. If you want Prava to help **discover** the product too, see [Agentic shopping](/prava-pay/shopping) instead. The agent must be [linked](/prava-pay/linking) first. If it isn't, session commands exit with `Agent not linked. Run: prava setup --name ""`. ## `prava sessions create` ```bash theme={null} prava sessions create \ --total-amount "8.50" \ --currency USD \ --merchant-name "Blue Bottle Coffee" \ --merchant-url "https://bluebottlecoffee.com" \ --merchant-country US \ --product '{"description":"Latte","unit_price":"5.00","quantity":1}' \ --product '{"description":"Croissant","unit_price":"3.50","quantity":1}' ``` | Option | Required | Description | | - | - | - | | `--total-amount ` | Yes | Total as a string, e.g. `"8.50"`. Must equal the sum of `unit_price × quantity` across all products. | | `--currency ` | Yes | ISO 4217 code, e.g. `USD`, `EUR`, `GBP`, `INR`. | | `--merchant-name ` | Yes | Merchant display name (shown to the owner). | | `--merchant-url ` | Yes | Full merchant URL **including `https://`**. | | `--merchant-country ` | Yes | ISO 3166-1 alpha-2 country, e.g. `US`. | | `--product ` | Yes | A product line item as JSON. **Repeat** the flag for each distinct item. | Each `--product` is a JSON object: ```json theme={null} { "description": "Latte", "unit_price": "5.00", "quantity": 1 } ``` For multiple units of the same item, set `"quantity"`; don't repeat `--product` or bake counts into the description. The total must reconcile with the line items. On success: ``` Session created. Session ID: ses_abc123 Payment URL: https://collect.prava.space?session=… Share this URL to complete card entry. Run `prava sessions poll --session-id ses_abc123` to wait for card entry. ``` Give the **Payment URL** to the owner. They open it and enter their card in Prava's secure page; the card details never pass through the agent. ## `prava sessions poll` Wait for the card to be entered and tokenized (exchanged for one-time credentials that stand in for the real card): ```bash theme={null} prava sessions poll --session-id ses_abc123 ``` | Option | Required | Description | | - | - | - | | `--session-id ` | Yes | The `Session ID` from `sessions create`. | While waiting it prints dots; once the owner completes card entry: ``` Waiting for card entry on session ses_abc123... .... Card tokenized. Token: 4323126882557932 Cryptogram: 957 Expiry: 12/2028 ``` `poll` waits up to **10 minutes** for the owner to enter the card; that's the CLI's card-entry window. The underlying session expires **15 minutes** after creation; once it does, create a new session. ### What you receive | Field | Format | Use it as | | - | - | - | | **Token** | 16-digit number | The card number at checkout. | | **Cryptogram** | 3-digit number | The CVV / security code at checkout. | | **Expiry** | `MM/YYYY` | The card expiry at checkout. | These credentials are **single-use and expire quickly**. Complete the checkout immediately after polling; don't cache, log, or reuse them. If they expire, create a new session. ## Exit codes Session commands use exit codes so an agent can branch reliably: | Code | Meaning | | - | - | | `0` | Success. | | `1` | Error or timeout (e.g. session expired, tokenization failed). | | `2` | Not linked / no agent configured; run [`prava setup`](/prava-pay/linking). | ## Common outcomes | Output | What it means | What to do | | - | - | - | | `Session expired. Run \`prava sessions create\` again.\` | The owner didn't finish card entry within 10 minutes. | Re-create the session; have the card ready. | | `Tokenization failed.` | The card couldn't be processed. | Try again, or a different card. See [Troubleshooting](/prava-pay/troubleshooting). | | `Invalid product JSON: …` | A `--product` value isn't valid JSON. | Fix the JSON: `'{"description":"…","unit_price":"…","quantity":1}'`. | | `Failed to create session: …` | The order was rejected. | Check the amount reconciles with the line items and the URL includes `https://`. | Next: use the token to [complete a checkout](/prava-pay/shopping#checkout), or drive the merchant's checkout directly with the returned credentials. # Agentic Shopping Source: https://docs.prava.space/prava-pay/shopping Discover a product, compare offers, get a live quote, and pay — all from the CLI. When the agent needs to **find** the product too, Prava Pay adds a discovery flow on top of payments: **search → product → quote → checkout**. Each step hands you exactly what you need to run the next. For the richest guided experience (pacing, confirmations, masked previews), install the `prava-shopping` skill alongside the CLI. The `shop` commands work without it, but the skill adds the guardrails that keep the agent honest with the user. ```bash theme={null} npx --yes skills add https://github.com/Prava-Payments/prava-skills \ --skill prava-shopping --global --yes --full-depth ``` The agent must be [linked](/prava-pay/linking) before any `shop` command works. ## Delivery addresses Physical goods need a delivery address and a contact phone. These are stored on the **owner's account** and hydrated server-side at purchase time: the agent only ever sees **masked summaries**, never full street addresses or phone numbers. The agent never handles the buyer's full address or phone. It selects *which* saved address by id; Prava fills in the real details privately. See [Guardrails](/concepts/guardrails). ### List addresses ```bash theme={null} prava shop address list ``` ``` 2 saved addresses (masked): 1. Home [default] San Francisco, CA 94103, US address-id: addr_home1 2. Work Oakland, CA 94612, US address-id: addr_work2 ``` ### Add an address The primary place to add addresses is the Prava dashboard; this is the CLI fallback. ```bash theme={null} prava shop address add \ --first-name John --last-name Doe \ --line1 "123 Main St" --city "San Francisco" \ --region CA --postal 94103 --country US \ --phone "+1 415 555 0100" --label "Home" --default ``` | Option | Required | Description | | - | - | - | | `--first-name`, `--last-name` | Yes | Recipient name. | | `--line1` | Yes | Street address. `--line2` optional. | | `--city`, `--region`, `--postal`, `--country` | Yes | City, state/province, ZIP, ISO country. | | `--phone` | No | Contact phone **with country code**. Stored on the account, not per-address. | | `--label` | No | e.g. `"Home"`. | | `--default` | No | Make this the default delivery address. | ### Set the default ```bash theme={null} prava shop address set-default --address-id addr_work2 ``` ## Search ```bash theme={null} prava shop search --query "dark roast coffee" --intent "gift for a coffee lover" --limit 10 --ships-to US ``` | Option | Required | Description | | - | - | - | | `--query ` | Yes | Tight keyword query. | | `--intent ` | No | The user's full natural-language request (occasion, budget); improves ranking. | | `--limit ` | No | Max results (default 10). | | `--cursor ` | No | Next-page cursor from a previous search. | | `--merchant ` | No | Restrict to one merchant domain. | | `--ships-to ` | No | ISO destination country, e.g. `US`. | | `--json` | No | Raw JSON output for chaining. | ``` 3 results for "dark roast coffee": 1. Hair Bender Whole Bean $17.00 USD · stumptowncoffee.com product-id: prod_abc Next: prava shop product --product-id --merchant ``` ## Product Show a product's variants and offers across sellers: ```bash theme={null} prava shop product --product-id prod_abc --merchant stumptowncoffee.com ``` | Option | Required | Description | | - | - | - | | `--product-id ` | Yes | `product-id` from search results. | | `--merchant ` | No | Merchant domain from the search result. | | `--json` | No | Raw JSON output. | Offers are listed **orderable first, then cheapest**. Prices shown are item-only: **shipping and tax are added at the quote step**. ``` 2 offers from 2 sellers, 2 orderable (orderable first, then cheapest) — prices are item-only; shipping is added at quote: 1. 12 oz — $17.00 USD [whole bean] · stumptowncoffee.com variant-id: var_123 Next: prava shop quote --variant-id --merchant ``` ## Quote Pricing a variant opens a **checkout session** with the *final*, binding total: subtotal + shipping * tax. This is a spend-adjacent step, so it asks for confirmation. ```bash theme={null} prava shop quote --variant-id var_123 --merchant stumptowncoffee.com --quantity 1 --address-id addr_home1 --yes ``` | Option | Required | Description | | - | - | - | | `--variant-id ` | Yes | `variant-id` from product details. | | `--merchant ` | Yes | Merchant domain. | | `--quantity ` | No | Quantity (default 1). | | `--email ` | No | Buyer email (optional if on file). | | `--address-id ` | No | Which saved address to ship to (default: the owner's default address). | | `--retries ` | No | Retry on timeout / server error (default 1; `0` to disable). | | `-y, --yes` | No | Confirm. Pass **only** after the user has approved the seller, variant, and price. | | `--json` | No | Raw JSON output. | ``` Quote for stumptowncoffee.com Total: $22.45 USD (subtotal $17.00 + shipping $4.50 + tax $0.95) Shipping: Standard (5–7 days) Expires: in ~15m checkout-session-id: ches_xxx Next: mint a card session for $22.45 USD (prava sessions create … → approve → prava sessions poll), then: prava shop checkout --checkout-session-id ches_xxx --token --cryptogram --expiry-month --expiry-year ``` A quote **expires** (about 15 minutes) and locks the price. Get the card credentials and check out promptly. If it lapses, quote again. Between the quote and the checkout, mint the card credentials with a [payment session](/prava-pay/sessions): `prava sessions create` for the quoted total → owner enters the card → `prava sessions poll` to get the token and cryptogram. ## Checkout Pay the quoted total with the card credentials. This is the actual charge, so it also confirms. ```bash theme={null} prava shop checkout \ --checkout-session-id ches_xxx \ --token 4323126882557932 --cryptogram 957 \ --expiry-month 12 --expiry-year 2028 \ --cardholder-name "John Doe" --yes ``` | Option | Required | Description | | - | - | - | | `--checkout-session-id ` | Yes | From `shop quote`. | | `--token ` | Yes | Network token from `prava sessions poll`. | | `--cryptogram ` | Yes | Dynamic CVV from `prava sessions poll`. | | `--expiry-month `, `--expiry-year ` | No | Card expiry (defaults from the session if omitted). | | `--cardholder-name ` | No | Cardholder name. | | `-y, --yes` | No | Confirm the charge. Pass **only** after the user approves the final total. | | `--json` | No | Raw JSON output. | On success: ``` ✓ Paid. Amount: $22.45 USD Order: ord_abc123 ``` The total charged is **bound to the quote**: it can't drift between quote and payment. Prava pays exactly the amount you saw, at exactly the merchant you quoted. ## Confirmation & safety Both `quote` and `checkout` are gated by a confirmation: * At an interactive terminal, they prompt `[y/N]`. * For an agent (non-interactive), they **refuse** unless `--yes` is passed, making a skipped confirmation a deliberate, visible act. Pass `--yes` **only after the user has actually approved** the seller, item, and total. It exists to record the user's approval, not to bypass it. ## Exit codes | Code | Meaning | | - | - | | `0` | Success. | | `1` | Error, declined, or not confirmed. | | `2` | Not linked / confirmation refused without `--yes`. | For the full list of failure messages and how to recover, see [Troubleshooting](/prava-pay/troubleshooting). # Skills Source: https://docs.prava.space/prava-pay/skills How Prava skills guide an AI agent to call the right CLI command at the right time. The `prava` CLI does the work; **skills tell the agent *when* to call *what*.** A skill is a small instruction file installed into the agent's runtime that encodes the correct flow, the pacing, and the safety stops, so the agent invokes `setup`, `sessions`, and `shop` commands in the right order and pauses for the user's approval before spending. The CLI works **without** any skill: every `prava` command runs on its own. The skill adds the *guidance*: which command comes next, when to stop and confirm, and how to keep the user in control. If the matching skill is missing, the CLI nudges you (once) to install it. ## Why skills matter Left to itself, an agent might chain commands or spend without checking. Skills prevent that by encoding: * **One decision per turn**: after `search`, wait for the user to pick; after `product`, wait for them to confirm the seller/variant; after `quote`, wait for them to approve the total. * **Confirmation hard-stops**: the agent must present the merchant, item, and final amount and get an explicit "yes" before `sessions create` / `checkout`. *"Buy me X"* is intent, not price approval. * **PII masking**: the agent works with masked address summaries only; full details stay server-side. * **Move promptly**: card credentials are single-use and short-lived, so mint → poll → checkout without pausing. ## The skills | Skill | Version | What it guides | | - | - | - | | **`prava-pay`** | 2.3.1 | Completing a payment once merchant, item, and price are known: `setup` → `status` → `sessions create` → `sessions poll` → checkout. | | **`prava-shopping`** | 1.5.1 | Discover **and** buy a product: `shop search` → `shop product` → `shop quote` → `shop checkout`, with pacing and confirmation gates. | | **`prava-sdk-integration`** | 1.1.0 | For coding agents *integrating the [SDK](/sdk/overview)* into an app (sessions, iframe, polling); not for CLI payments. | | **`swiggy-prava-skill`** | — | Order from Swiggy via its MCP (Swiggy's tool server for agents), pay with Prava credentials. | | **`zepto-prava-skill`** | — | Order from Zepto via its MCP, pay with Prava credentials. | Most agents want **`prava-pay`** (to pay for a known item) or **`prava-shopping`** (to find *and* buy). ## Install a skill Install into the agent's global skills directory (e.g. `~/.claude/skills//SKILL.md`): ```bash One skill (recommended) theme={null} npx --yes skills add https://github.com/Prava-Payments/prava-skills \ --skill prava-shopping --global --yes --full-depth ``` ```bash All skills theme={null} npx --yes skills add https://github.com/Prava-Payments/prava-skills \ --global --yes --full-depth ``` Pass **`--full-depth`** with `--skill `. Otherwise sibling skills are pulled in alongside the one you asked for. Valid `--skill` names: `prava-pay`, `prava-shopping`, `prava-sdk-integration`, `swiggy-prava-skill`, `zepto-prava-skill`. ## Keeping skills current The CLI checks your installed skill version on every call. If it's behind the server minimum, it prints: ``` Skill update required (minimum: 2.3.1). Run: npx skills update prava-pay -g ``` Update, then re-run your command. If the notice repeats, restart the agent session so it reloads the skill. See [Troubleshooting](/prava-pay/troubleshooting#keeping-the-cli-up-to-date). ## Platforms Skills work across coding agents: Claude Code, Cursor, Codex, Gemini CLI, Hermes, and [many more](/prava-pay/linking#prava-setup). The agent's platform is set with `--platform` when you run [`prava setup`](/prava-pay/linking). ## Next steps Connect and approve the agent before it can pay. The search → quote → checkout flow the shopping skill guides. # Troubleshooting Source: https://docs.prava.space/prava-pay/troubleshooting What each message means and exactly how to recover. Prava Pay is designed to fail **loudly and safely**: every error tells you what happened and what to do next, and no failure ever leaves you double-charged. This page maps the messages you'll see to a recovery step. **Exit codes** let an agent branch without parsing text: `0` = success · `1` = error/declined · `2` = not linked or confirmation refused. ## Linking problems Nothing is linked on this machine yet. Run [`prava setup`](/prava-pay/linking) and approve in the browser. The approval window (15 minutes) passed. Start over with `prava setup`, open the new URL, and approve promptly. The owner rejected the agent on the approval screen. If that was a mistake, run `prava setup` again and approve it. Linking is time-sensitive. Sync the machine's clock (enable automatic network time) and re-run `prava setup`. A network issue. Check connectivity and retry. If you're behind a proxy or firewall, make sure outbound HTTPS to `prava.space` is allowed. A session or shop command ran before the agent was approved. Complete linking first (`prava setup` → approve → `prava setup poll`), then retry. ## Card entry & payment sessions The owner didn't finish card entry within the 10-minute window. Re-create the session and have the card ready before opening the payment URL. The card couldn't be processed. Try the payment session again, or use a different card. If it keeps failing, contact [support](mailto:support@prava.space). A `--product` value isn't valid JSON. Use the exact shape, quoted for your shell: `'{"description":"…","unit_price":"…","quantity":1}'`. The order was rejected. Check that `--total-amount` equals the sum of `unit_price × quantity` and that `--merchant-url` includes `https://`. The token and cryptogram (the one-time card number and security code) from `sessions poll` are **single-use and short-lived**. If you wait too long to check out, mint a fresh session; don't try to reuse old credentials. ## Shopping: quotes & checkout Quotes and checkouts drive a live merchant checkout and can take 20–40 seconds. For **quotes**, retry (the CLI retries once by default; raise it with `--retries`). For **checkout**, see the note on unknown outcomes below before retrying. A quote is valid for about 15 minutes and locks the price. Once it lapses, run `prava shop quote` again to get a fresh `checkout-session-id` and total. You've hit the limit on concurrent open quotes. Wait for existing quotes to be paid or to expire (\~15 min), then quote again. The account's purchase limit has been reached. This is an owner-level guardrail: the owner needs to raise the limit or contact [support](mailto:support@prava.space). See [Guardrails](/concepts/guardrails). Physical goods need a delivery address and a contact phone. Add them in the Prava dashboard, or via `prava shop address add … --phone "+1 …"`. See [Agentic shopping](/prava-pay/shopping#delivery-addresses). `quote` and `checkout` require confirmation. At a terminal, answer `y`. For an agent, pass `--yes` **only after the user has approved** the seller, item, and total. ## Payment outcomes The result of a checkout is read from the payment itself rather than the network response alone, so the status you see is the real one. Success. The charge went through; an order id is shown when the merchant provides one. The payment was declined or the merchant rejected it. This checkout is done; start a new [quote](/prava-pay/shopping#quote) to try again. This checkout was already completed earlier. Prava recognized the repeat and did **not** charge again. Treat the earlier success as authoritative. **Unknown outcome (rare).** If a checkout times out or the merchant response is lost, Prava does **not** guess: it leaves the payment un-finalized rather than risk a double charge, and asks you to wait rather than blindly retry. Do **not** immediately re-run checkout with the same credentials; re-quote if you need to try again, and reconcile the earlier attempt first if in doubt. ## Keeping the CLI up to date Prava tells you when a version is behind: The CLI is too old to talk to the server. Update it, then retry the command (you do **not** need to re-link): ```bash theme={null} npm update -g @prava-sdk/cli ``` An optional bug-fix update is available. Not required, but recommended: ```bash theme={null} npm update -g @prava-sdk/cli ``` A Prava skill (e.g. `prava-pay`, `prava-shopping`) is behind. Update it, then retry. If the notice repeats, restart the agent session so it reloads the skill: ```bash theme={null} npx skills update prava-shopping -g ``` ## Still stuck? Run `prava status` to confirm the agent is linked and active, and re-check the command flags against the [Payment sessions](/prava-pay/sessions) and [Agentic shopping](/prava-pay/shopping) references. For anything else, reach us at [support@prava.space](mailto:support@prava.space). # Your Prava Pay Dashboard Source: https://docs.prava.space/prava-pay/your-wallet For agent owners: what your AI agent can and can't do with your card, and how you stay in control. No code required. This page is for the **person whose card gets used**: the agent owner. You don't need to read any other page in these docs to use Prava safely. Your home base is the **Prava Pay dashboard** at [pay.prava.space](https://pay.prava.space). That's where you approve agents, add your card, and control spending. (If you're a developer looking for API keys, you want [dashboard.prava.space](https://dashboard.prava.space) instead. Different product.) ## What your agent can never do You enter card details only on Prava's secure page. The agent receives a **one-time credential** for a single approved purchase, never your real card number. Every purchase needs your explicit approval: a **passkey** (Touch ID / Face ID) and/or an in-chat confirmation before any money moves. The credential is locked to the merchant and amount you approved. It can't be reused, resized, or pointed at another store. Revoke an agent any time from the wallet; its access ends immediately. ## Setting up (once, \~2 minutes) Your agent asks to link by showing you a URL (it ran `prava setup`). Open it, sign in at [pay.prava.space](https://pay.prava.space), check the agent's name, and click **Approve**. The link expires after 15 minutes, so approve promptly. When the agent makes its first payment, you'll get a secure Prava page to enter your card; it's saved to your account for future purchases. Your card details go to Prava, never to the agent. If your agent shops for physical goods, add a delivery address and contact phone in the dashboard once. The agent only ever sees a masked summary. ## Every purchase, from your side 1. The agent finds or is told what to buy, and shows you the **merchant, items, and total**. 2. You confirm — in the conversation, and with a **passkey** for card actions. 3. Prava issues the one-time credential and the checkout runs. 4. The result shows up in your dashboard's transaction history. If the agent skips the confirmation, the purchase doesn't happen: the confirmation is enforced by Prava, not by the agent's goodwill. ## Your controls | Control | What it does | | - | - | | **Approve / revoke agents** | An agent is either approved by you or it can do nothing. Revocation is instant. | | **Checkout quota** | A cap on how many purchases your agents can make in total. | | **Concurrency limit** | How many checkouts can be open at once. | | **Per-purchase approval** | Nothing is charged without your confirmation. | | **Passkey security** | Card actions require your device's biometrics; there is no password fallback to phish. | More detail on how these are enforced: [Guardrails](/concepts/guardrails) · [Accounts & Agents](/concepts/accounts). ## Common questions **What if my agent goes rogue or gets compromised?** Revoke it at [pay.prava.space](https://pay.prava.space). Anything already approved is still merchant- and amount-locked; anything new requires approvals it can no longer get. **Can my agent see my address or phone?** No. It sees masked summaries. Full details stay on Prava's servers and go only to the merchant fulfilling your order. **What if a payment looks wrong?** Check the transaction in your dashboard, then contact [support@prava.space](mailto:support@prava.space). Refunds follow the standard card-network process. **Do I need to understand the developer docs?** No. This page plus the dashboard is everything an owner needs. The curious can read [How Prava Works](/concepts/how-it-works). # Quickstart Source: https://docs.prava.space/quickstart Start building with Prava Getting started with Prava is **self-serve**. Sign up at the [Prava Dashboard](https://dashboard.prava.space) ([what it offers](/dashboard)), create an API key, and you'll have sandbox credentials in a few minutes, with no invite code and no waiting. **Want to see Prava in action first?** Try the [Interactive Playground](https://playground.prava.space/) to visualize the full payment flow before you build. ## Set up your account Go to [dashboard.prava.space](https://dashboard.prava.space) and create an account with your email (one-time code) or Google. You'll land in the dashboard right away. In the dashboard, create an API key by entering your **entity name** (and optional website). Prava provisions your merchant and issues your keys instantly: * A **publishable key** (`pk_test_*`) — safe for the browser, used to initialize the SDK. * A **secret key** (`sk_test_*`) — server-side only, used to create sessions. New keys are **sandbox** by default. See [Authentication & Environments](/authentication) for how keys and environments work. **Going to production?** Production is a separate environment you switch to from the dashboard, and going live may require some additional verification. Reach out at [support@prava.space](mailto:support@prava.space) when you're ready. You can build and test everything in sandbox first. ## Create your first session A session is the workspace for a single payment. Once you have your credentials, create a session from your backend: ```bash theme={null} curl -X POST https://sandbox.api.prava.space/v1/sessions \ -H "Authorization: Bearer sk_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "user_id": "user_123", "user_email": "user@example.com", "total_amount": "49.99", "currency": "USD", "purchase_context": { "custom": [{ "merchant_details": { "name": "Your Store", "url": "https://yourstore.com", "country_code_iso2": "US" }, "product_details": [{ "description": "Example Product", "unit_price": "49.99", "quantity": 1 }] }] } }' ``` The response gives you a `session_token` and an `iframe_url`. ## Choose how to collect the card From that one session you can collect the card two ways: * **Hosted (default)**: redirect the cardholder to the `iframe_url`; Prava returns them to your `callback_url` when done. No frontend SDK needed. * **Embedded**: set `integration_type: "embedding"` on the session and mount Prava's card UI inside your own page with the SDK (below). See [Integration Modes](/sdk/integration-modes) for a side-by-side comparison and the hosted snippet. ## Embed the card form (embedded mode) ```typescript theme={null} import { PravaSDK } from '@prava-sdk/core'; const prava = new PravaSDK({ publishableKey: 'pk_test_xxx', }); await prava.collectPAN({ sessionToken: session.session_token, iframeUrl: session.iframe_url, container: '#card-form', onSuccess: (data) => console.log(`Collected: ${data.brand} •••• ${data.last4}`), onError: (err) => console.error(err.code, err.message), }); ``` See the [SDK Overview](/sdk/overview) for the full API reference. ## Where next The complete integration in your app: backend, frontend, credentials, reporting. The same flow with hosted card entry and nothing but cURL. Test cards, sandbox behaviors, and a full test run. # Collect Card Details Source: https://docs.prava.space/sdk/cards/collect-pan Securely collect and tokenize user payment cards ## Overview The `collectPAN()` method securely collects credit card details from users through an isolated iframe. The card data is tokenized via Prava's PCI DSS vault and returns only safe metadata — your servers never see the raw PAN. ## Method Signature ```typescript theme={null} prava.collectPAN(options: CollectPANOptions): Promise ``` ## Parameters Configuration options for card collection The `session_token` (JWT) from your backend session response — it authenticates in-session SDK calls. Note: the session identifier the iframe loads is already embedded in `iframe_url` (`?session=`); you never build that URL yourself. The `iframe_url` from your backend session response — pass it **verbatim**. It already contains the `?session=` identifier; do not rebuild it or add your own query params. CSS selector or DOM element to mount the card form (e.g., `#card-form`) Called when the iframe is loaded and ready for input Real-time validation state on every keystroke Called when card provisioning succeeds. In a `full_enrollment` session, passkey work may still be in progress; use `onEnrollmentComplete` for the final enrollment result. Called only for an enrollment-only session after the card and passkey are ready. The payload contains `session_id`, `card_id`, `status: "completed"`, `outcome: "fully_enrolled"`, `passkey: "registered" | "existing"`, and `callback_url`. Called when the cardholder cancels an enrollment-only session. `card_id` can be non-null when card provisioning completed before cancellation. Called on error Called when the user dismisses the payment flow Custom styles for the card form. Supports `base`, `invalid`, and `focus` states as CSS property maps. ## Return Value Unique identifier for the collected card Last 4 digits of the card number Card brand (`visa`, etc.) Card expiration month (1-12) Card expiration year (4 digits) The `enrollmentId` identifies the collected card on the server side. Use your backend and the [API Reference](/api-reference/overview) to read payment results and report status for that card. ## Full card-enrollment sessions When `collectPAN()` receives a session created by [Enroll Card](/api-reference/enroll-card) with `enrollment_mode: "full_enrollment"`, its normal `onSuccess` callback still reports the earlier card-provisioning milestone. Keep the SDK and iframe mounted until `onEnrollmentComplete` or `onEnrollmentCancelled`. ```typescript theme={null} void prava.collectPAN({ sessionToken: session.session_token, iframeUrl: session.iframe_url, container: '#card-form', onSuccess: (card) => console.log('Card provisioned', card.enrollmentId), onEnrollmentComplete: (result) => { console.log('Full enrollment complete', result.card_id, result.passkey); }, onEnrollmentCancelled: (result) => { console.log('Enrollment cancelled', result.card_id); }, onError: (error) => console.error(error.code, error.message), }); ``` Use the callback for frontend coordination, but confirm the durable server-side result with [Get Card Enrollment Status](/api-reference/get-card-enrollment-status). A retryable iframe error does not require a new SDK call: leave the iframe mounted while the cardholder selects **Try again**. ## Example ```typescript React theme={null} import { useEffect, useRef, useState } from 'react'; import { PravaSDK } from '@prava-sdk/core'; import type { CollectPANResult } from '@prava-sdk/core'; function CardEnrollment({ session }: { session: { session_token: string; iframe_url: string } }) { const containerRef = useRef(null); const sdkRef = useRef(null); const [enrolled, setEnrolled] = useState(null); useEffect(() => { const sdk = new PravaSDK({ publishableKey: 'pk_test_xxx' }); sdkRef.current = sdk; return () => sdk.destroy(); }, []); const handleEnroll = async () => { if (!sdkRef.current || !containerRef.current) return; const result = await sdkRef.current.collectPAN({ sessionToken: session.session_token, iframeUrl: session.iframe_url, container: containerRef.current, onReady: () => console.log('Form loaded'), onChange: (state) => { console.log('Complete:', state.isComplete); }, onSuccess: (data) => { console.log(`Enrolled: •••• ${data.last4}`); setEnrolled(data); }, onError: (err) => console.error(err.code, err.message), }); }; return (
{enrolled &&

Card enrolled: {enrolled.brand} •••• {enrolled.last4}

}
); } ``` ```typescript Vanilla JavaScript theme={null} import { PravaSDK } from '@prava-sdk/core'; const prava = new PravaSDK({ publishableKey: 'pk_test_xxx' }); // Get session from your backend const session = await fetch('/api/session').then(r => r.json()); // Collect card via secure iframe const card = await prava.collectPAN({ sessionToken: session.session_token, iframeUrl: session.iframe_url, container: '#card-form', onReady: () => console.log('Form loaded'), onChange: (state) => { document.getElementById('submit-btn').disabled = !state.isComplete; }, onSuccess: (data) => { console.log(data.enrollmentId, data.last4, data.brand); }, onError: (err) => console.error(err.code, err.message), }); ``` ```html theme={null}
``` ## Flow Diagram ```mermaid theme={null} sequenceDiagram participant App as Your App participant SDK as Prava SDK participant Iframe as Secure Iframe participant Backend as Prava Backend participant Network as Card Network App->>SDK: collectPAN(sessionToken, iframeUrl) SDK->>Iframe: Mount iframe Iframe-->>SDK: onReady() SDK-->>App: Form ready Note over Iframe: User enters card details Iframe->>Backend: Tokenize via PCI DSS vault Backend->>Network: Provision token Network-->>Backend: Enrollment complete Backend-->>Iframe: Success Iframe-->>SDK: onSuccess(result) SDK-->>App: CollectPANResult ``` ## Under the Hood When you call `collectPAN()`, here's what happens: The SDK injects a secure, sandboxed iframe into your specified container. The iframe is served from Prava's domain — card data never touches your DOM or servers. The iframe validates the session token with Prava's backend to ensure the request is legitimate and not expired. The user enters their card details (number, expiry, CVV) in the iframe form with real-time validation. When the user submits, the iframe tokenizes the PAN via Prava's PCI DSS vault. Your servers never see the raw card number. The enrollment result (with `enrollmentId`, `last4`, `brand`, `expMonth`, `expYear`) is returned to your app. ## Validation States The `onChange` callback receives a `CardValidationState` object on every keystroke: ```typescript theme={null} interface CardValidationState { cardNumber: FieldState; expiry: FieldState; cvv: FieldState; isComplete: boolean; // true when all fields are valid } interface FieldState { isEmpty: boolean; isValid: boolean; isFocused: boolean; error?: string; } ``` Use `isComplete` to enable/disable your submit button: ```typescript theme={null} onChange: (state) => { submitButton.disabled = !state.isComplete; } ``` ## Error Handling ### Common Errors | Code | Cause | Resolution | | - | - | - | | `SDK_ALREADY_ACTIVE` | `collectPAN` called while another collection is in progress | Call `destroy()` first | | `INVALID_CONFIG` | Missing `iframeUrl` (or a bad `publishableKey`) | Pass the `iframe_url` from your backend session response | | `IFRAME_LOAD_ERROR` | Iframe failed to load | Check network, verify `iframeUrl` | | `Session preview failed: 404` | The `?session=` value isn't a valid `session_id` (e.g. the `session_token`/JWT was passed instead) | Pass `iframe_url` verbatim; don't set your own `session`/`session_token` param; verify sandbox vs prod host | | `SDK_INIT_ERROR` | SDK initialization failed | Verify browser compatibility and config | ## Security Notes **Never** attempt to bypass the iframe or collect card data directly. The iframe is sandboxed with `allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox` and a permissions policy of `payment; publickey-credentials-get; publickey-credentials-create`. Card data never touches your DOM, JS, or servers. PostMessage communication is origin-locked. The iframe resolves its backend from its own hostname — merchants cannot inject a fake backend URL. ## Next Steps Create a provision-only or full card-enrollment session Retrieve a customer's enrolled cards server-side Read the outcome after the card is collected # Integration Modes Source: https://docs.prava.space/sdk/integration-modes Two ways to collect a card from one session — embedded (SDK) or hosted (redirect). Every card collection starts the same way: your backend creates a **session** ([`POST /v1/sessions`](/api-reference/create-session)) and gets back a `session_token` and an `iframe_url`. From there you choose **how** the cardholder enters their card — and you pick by setting one field, `integration_type`, on the session. Enrollment-only sessions created by [`POST /v1/sessions/enroll-card`](/api-reference/enroll-card) use these same two presentation modes. Their backend result comes from [Get Card Enrollment Status](/api-reference/get-card-enrollment-status), not Payment Result. Prava's card UI runs **inside your app** via the [SDK](/sdk/overview). You control the surrounding page and get the result in-app. You **redirect** the cardholder to a Prava-hosted page. No SDK, minimal code — Prava returns them to your `callback_url` when done. Either way, the card is entered on **Prava's secure surface** and tokenized — your app never touches the raw card number. There is no mode where you collect the PAN yourself. ## At a glance | | **Embedded** (`"embedding"`) | **Hosted** (`"full_checkout"`, default) | | - | - | - | | SDK required | Yes (`@prava-sdk/core`) | No | | Where the card UI renders | An iframe inside your page | A full Prava-hosted page you redirect to | | What you build | A container + SDK wiring | A redirect + a `callback_url` route | | How you get the result | Payments: `onSuccess`; full enrollment: `onEnrollmentComplete` | Payments: `callback_url`; full enrollment: callback or final screen | | Best when | You want the flow to feel native to your product | You want the fastest, lowest-maintenance integration | Both modes use the **same session** and the **same `iframe_url`** — embed it, or open it directly. For full enrollment, your backend confirms either UI result by polling [Get Card Enrollment Status](/api-reference/get-card-enrollment-status). **Use `iframe_url` exactly as returned.** It already carries the `?session=` identifier. Don't rebuild the URL or add your own `session` / `session_token` query params — the value in `?session=` must be the **`session_id`** (`sess_…`), never the `session_token` (the JWT). Passing the token there makes the iframe report `Session preview failed: 404`. ## Embedded mode Create the session with `integration_type: "embedding"`, then mount the card UI with the SDK. ```typescript theme={null} // 1. Backend: create the session const session = await createSession({ /* user_id, user_email, total_amount, currency, purchase_context … */ integration_type: 'embedding', }); // 2. Frontend: mount Prava's secure iframe in your page import { PravaSDK } from '@prava-sdk/core'; const prava = new PravaSDK({ publishableKey: 'pk_test_xxx' }); await prava.collectPAN({ sessionToken: session.session_token, iframeUrl: session.iframe_url, container: '#card-form', onSuccess: (data) => console.log(`Collected •••• ${data.last4}`), onError: (err) => console.error(err.code, err.message), }); ``` ```html theme={null}
``` The result arrives in `onSuccess` without leaving your page. See [Collect Card Details](/sdk/cards/collect-pan) for every option and callback, and remember to call `prava.destroy()` on cleanup. ## Hosted mode (default) Leave `integration_type` off (it defaults to `"full_checkout"`) and give Prava a `callback_url`. Then send the cardholder to the returned `iframe_url` — no SDK needed. ```typescript theme={null} // 1. Backend: create the session (hosted is the default) const session = await createSession({ /* user_id, user_email, total_amount, currency, purchase_context … */ callback_url: 'https://yourapp.com/checkout/complete', // must be https }); // 2. Frontend: send the user to the hosted page. // iframe_url already contains ?session= — use it verbatim. window.location.href = session.iframe_url; // or window.open(session.iframe_url, '_blank') ``` When the cardholder finishes, Prava redirects them back to your `callback_url`. Your backend then reads the outcome with [Get Payment Result](/api-reference/get-payment-result) and reports the final status with [Report Status](/api-reference/report-status). `callback_url` must be an **https** URL. Without it, a hosted checkout has nowhere to return the user. A hosted **card-enrollment-only** session is the exception: its callback is optional, and without one Prava keeps the completion screen visible so the cardholder can safely close the window. ## Which should I use? You want the card step to feel native, keep the user on your page, and react to the result in-app. You want to ship fast with minimal frontend code and are happy to redirect out and back. ## Next steps The library behind embedded mode. `integration_type`, `callback_url`, and the full request/response. # SDK Overview Source: https://docs.prava.space/sdk/overview Embed secure, PCI-safe card collection into your web app with the Prava SDK. *Not sure this is the right path? See [Choosing Your Integration](/choosing-your-integration).* ## What is the Prava SDK? The **Prava SDK** (`@prava-sdk/core`) is a lightweight browser library for **securely collecting a card** in your web app. It mounts a PCI-compliant iframe, handles the card entry and authentication inside it, and hands you back a safe result — the raw card number never touches your page, your JavaScript, or your servers. The SDK is the **client-side** piece of a session-based integration. Your **backend** creates the session (see the [API Reference](/api-reference/overview)); the SDK renders the secure card form for that session. If instead you want an **AI agent** to pay from the command line, use [Prava Pay](/prava-pay/overview). The SDK powers the **embedded** integration mode — Prava's card UI inside your own page. Don't want to embed? Use **hosted** mode (a redirect, no SDK). See [Integration Modes](/sdk/integration-modes) to choose. ## What it does (and doesn't) The SDK's job is deliberately small — one secure operation, done well: | The SDK handles | Your backend handles | | - | - | | Rendering the secure card iframe | Creating the session (`POST /v1/sessions`) | | Card entry, validation, and in-iframe authentication | Reading the payment result | | Returning a safe, tokenized result | Reporting the final transaction status | Everything money-related — mandates, one-time payment credentials, spending limits — is enforced by Prava and the card network, not by code running in your page. See [Payments](/concepts/payments) and [Guardrails](/concepts/guardrails) for how that works. ## Install ```bash npm theme={null} npm install @prava-sdk/core ``` ```bash yarn theme={null} yarn add @prava-sdk/core ``` ```bash pnpm theme={null} pnpm add @prava-sdk/core ``` ## Quick start ```typescript theme={null} import { PravaSDK } from '@prava-sdk/core'; const prava = new PravaSDK({ publishableKey: 'pk_test_xxx', // from the Prava Dashboard (sandbox key) }); ``` The publishable key is safe to ship to the browser. Your **secret key** stays on your server and is never used here. Your server creates a session with your secret key and returns the `session_token` and `iframe_url` to your frontend: ```typescript theme={null} // Server-side (never expose your secret key to the browser) const session = await fetch('https://sandbox.api.prava.space/v1/sessions', { method: 'POST', headers: { Authorization: `Bearer ${SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ user_id: 'user_123', user_email: 'user@example.com', total_amount: '49.99', currency: 'USD', purchase_context: { custom: [{ merchant_details: { name: 'Amazon', url: 'https://amazon.com', country_code_iso2: 'US' }, product_details: [{ description: 'Wireless Headphones', unit_price: '49.99', quantity: 1 }], }], }, }), }).then((r) => r.json()); // → { session_id, session_token, iframe_url, expires_at, order_id } ``` Full request/response details: [Create Session](/api-reference/create-session). ```typescript theme={null} const result = await prava.collectPAN({ sessionToken: session.session_token, iframeUrl: session.iframe_url, container: '#card-form', onSuccess: (data) => console.log(`Collected •••• ${data.last4}`), onError: (err) => console.error(err.code, err.message), }); ``` ```html theme={null}
``` See [Collect Card Details](/sdk/cards/collect-pan) for every option and callback.
After the card is collected and the transaction runs, your backend reads the outcome and reports the final status. See [Get Payment Result](/api-reference/get-payment-result) and [Report Status](/api-reference/report-status).
## API surface The SDK exposes exactly two methods: | Method | Description | | - | - | | [`collectPAN(options)`](/sdk/cards/collect-pan) | Mounts the secure iframe, collects the card, resolves with a `CollectPANResult`. | | `destroy()` | Tears down the iframe and listeners. Call it on unmount/cleanup. | ```typescript theme={null} // Clean up when your component unmounts prava.destroy(); ``` ## Authentication | Key | Where it's used | Where it lives | | - | - | - | | **Publishable key** (`pk_live_*` / `pk_test_*`) | Initialize the SDK in the browser | Frontend | | **Secret key** (`sk_*`) | Create sessions, read results | Backend only | Never put your secret key in client-side code or version control. ## Result & error types ```typescript theme={null} interface CollectPANResult { enrollmentId: string; last4: string; brand: string; // "visa", "mastercard", … expMonth: number; expYear: number; } interface PravaError { code: string; message: string; details?: Record; } ``` Error codes, every `collectPAN` option, and validation states are documented on [Collect Card Details](/sdk/cards/collect-pan) — the canonical `collectPAN` reference. ## Requirements * **Browsers:** Chrome 80+, Firefox 80+, Safari 14+, Edge 80+. * **Backend:** Your server implements the session API (`POST /v1/sessions`) with your secret key. * **Keys:** Obtain `publishableKey` and `secretKey` from the [Prava Dashboard](https://dashboard.prava.space). ## Next steps Every `collectPAN` option, callback, and validation state. Create sessions, read payment results, and report status server-side. The lifecycle behind a Prava payment. Let an AI agent pay from the command line. # Use Cases Source: https://docs.prava.space/use-cases What people build with Prava, shown as concrete flows. Prava does one thing: it lets an AI agent pay with a real card, without ever holding that card. Here is what that looks like in practice. ## An agent buys something for you You tell your agent "order me a phone case under \$20". The agent, connected via [MCP](/mcp/overview) (Model Context Protocol) or the [CLI](/prava-pay/overview): 1. Searches products across merchants and picks a case: \$18.50. 2. Locks the live price with a quote. 3. Sends you a payment link. You open it and approve with Face ID. 4. The agent completes the checkout. You get an order id; the agent never saw your card. If the total had changed mid-checkout, the charge would not have gone through. The credential is locked to that merchant and that amount. **Start here:** [Connect via MCP](/mcp/connect) or [Prava Pay quickstart](/prava-pay/quickstart). ## An agent pays a bill you already know No product discovery needed. You say "pay my $54.20 internet bill at Comcast". The agent creates a payment session for exactly $54.20, you approve with a passkey, and it pays with the one-time credentials. Wrong-amount or wrong-merchant use is declined. **Start here:** [Payment sessions (CLI)](/prava-pay/sessions) or [create\_payment\_session (MCP)](/mcp/tools#create-payment-session--checkoutrun). ## Your AI app takes payments from its users You're building a shopping assistant, a travel planner, or a booking bot. Your users need to pay *inside your product*. You integrate the [SDK + API](/sdk/overview): your backend creates a session, your frontend shows Prava's secure card form, and your app receives one-time credentials to complete the purchase. Your servers never touch a card number, which keeps you out of PCI scope (the security compliance that applies once you handle card data). Example: a stylist app's user picks a $79 jacket. The app creates a session for $79, the user enters their card and approves in the embedded form, the app checks out at the boutique with the one-time token, then reports the result. **Start here:** [Add Payments to Your AI App](/guides/add-payments-to-your-ai-app). ## Human-in-the-loop, by design Every flow above has the same shape: **the agent proposes, the human approves, Prava executes.** The approval is a passkey (Touch ID / Face ID) on the user's own device. An agent cannot skip it, and an approval for one purchase authorizes nothing else. ## Recurring payments A [mandate](/concepts/mandates) is the user's standing permission to pay: approve once, and the agent charges within caps without a fresh passkey each time. A user approves "up to \$12/month at one merchant" a single time, and the agent charges monthly within those limits. *Scheduled* subscriptions — where Prava auto-charges each cycle — are coming next; for now the agent initiates each charge. ## Where to next Agent, embedded, or hosted. Decided in 10 seconds. The lifecycle behind every flow above. For merchants: what works with zero changes.