prava-payments/prava-skills

prava-sdk-integration

Integrate Prava's payment SDK into AI applications — create server-side payment sessions, embed or host the PCI-compliant checkout, retrieve one-time Visa credentials, and report real processor outcomes. For application integrations, not the Prava agent CLI.

First seen May 17, 2026

Installation

$ npx skills add prava-payments/prava-skills --skill prava-sdk-integration

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from prava-payments/prava-skills.

npx skills add prava-payments/prava-skills

Browse all from prava-payments/prava-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 8
Default branch main
Open issues 0
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.2.0
Declared agents clawdbot

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 21,411 B
  • docs SUMMARY.md 289 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 74 installs

SKILL.md

Prava SDK Integration

Integrate Prava into a web application without letting raw card data, the merchant secret key, or one-time payment credentials cross the wrong trust boundary.

This skill is for applications using @prava-sdk/core and Prava's merchant Session API. For an AI agent operating the Prava CLI, use the separate prava-pay skill.

Read the relevant resources

  • Read [references/session-api-reference.md](references/session-api-reference.md) when implementing or debugging create-session, payment-result, report-status, saved-card, or revoke calls.
  • Read [references/sdk-api-reference.md](references/sdk-api-reference.md) when mounting the iframe or handling SDK lifecycle/events.
  • Read [references/integration-flow.md](references/integration-flow.md) when deciding between custom checkout, quote checkout, embedded, and hosted flows.
  • Read [references/test-data.md](references/test-data.md) before running a sandbox test.
  • Adapt the files under templates/nextjs, templates/express, or templates/vanilla to the application's framework and design system. They are logic references, not a UI kit.

Non-negotiable invariants

  1. The merchant secret key is server-only in every environment. Never put an sk* value in HTML, client JavaScript, a browser bundle, or an environment variable prefixed with NEXTPUBLIC, VITE, or REACTAPP.
  2. Create sessions on an authenticated application server. Derive the user, amount, currency, destination merchant, and products from trusted server-side state. Persist the resulting session_id → application user/order binding, and enforce it on every browser-facing status request. Do not blindly relay browser-supplied values.
  3. Use the current create-session purchasecontext object. On POST /v1/sessions, a bare array is rejected. Custom checkout is { custom: [entry] }; quote checkout is { quote: true, quoteid, access_grant? }.
  4. Set the presentation explicitly. Use integrationtype: "embedding" for an SDK-mounted iframe and integrationtype: "fullcheckout" for a hosted/new-tab flow. The API default is fullcheckout.
  5. Pass iframeurl verbatim. Its session query parameter contains an opaque ses... session ID, not the JWT. Never rebuild the URL or replace that value with session_token.
  6. Keep payment credentials server-side. Polling returns a network token and dynamic CVV. Consume them in a trusted server/agent process; do not serialize or render them in the browser.
  7. Treat awaiting_result as credential-ready for custom checkout. completed normally happens only after the real processor outcome is reported. Waiting for completed before charging creates a deadlock.
  8. Make the processor charge idempotent. Before using a credential, atomically claim its txnrefid in durable storage and use a stable processor idempotency key derived from it. A worker retry must resume/query the same processor attempt, never create a second charge.
  9. Report the actual outcome. After attempting the charge, call report-status with APPROVED or DECLINED. Never fabricate approval merely to advance the state.
  10. Branch authorize-only mandates before polling. A session with mandatesetup.intent: "mandatesetup" creates an authorization for later charges; it emits no immediate payment credential. Do not wait for Session API awaiting_result or call Session API report-status for that setup.
  11. Create one session per checkout attempt. The iframe and payment-result poller must use the same session_id returned by that one call.
  12. Revoke abandoned sessions. A browser Cancel/restart action must call the authenticated server, verify session ownership, and invoke POST /v1/sessions/{id}/revoke. Closing an iframe/tab or clearing local state does not cancel the server session.

Required inputs

Collect these before integration:

Input Sandbox Production Exposure
Backend URL https://sandbox.api.prava.space https://api.prava.space Server-only is sufficient
Publishable key pktest* pklive* Browser-safe
Merchant secret key sktest* sklive* Server-only

Test keys must be used with the sandbox URL; live keys must be used with the production URL. If keys are unavailable, add obvious placeholders and tell the user to obtain credentials from Prava onboarding. Never invent usable-looking credentials.

Current Session API contract

Custom checkout

POST /v1/sessions uses Authorization: Bearer {MERCHANTSECRETKEY} and a JSON body like this:

{
  "user_id": "user_123",
  "user_email": "[email protected]",
  "total_amount": "49.99",
  "currency": "USD",
  "description": "Order 123",
  "integration_type": "embedding",
  "purchase_context": {
    "custom": [
      {
        "merchant_details": {
          "name": "Zara",
          "url": "https://www.zara.com",
          "country_code_iso2": "US",
          "category_code": "5651",
          "category": "Apparel"
        },
        "product_details": [
          {
            "product_id": "sku_123",
            "description": "Ribbed socks",
            "unit_price": "49.99",
            "quantity": 1
          }
        ],
        "effective_until_minutes": 15
      }
    ]
  }
}

Custom checkout currently supports exactly one purchase-context entry. merchant_details describes the destination merchant where the credential will be used, not the integrating AI application.

Quote checkout

Use quote mode only with a quote_id obtained through the Prava shop flow:

{
  "user_id": "user_123",
  "user_email": "[email protected]",
  "total_amount": "76.00",
  "currency": "USD",
  "integration_type": "full_checkout",
  "purchase_context": {
    "quote": true,
    "quote_id": "qte_..."
  }
}

An accessgrant may be supplied only when the quote owner explicitly issued one for a cross-caller flow. Do not invent or persist a grant. Quote and custom fields are mutually exclusive. For quote mode, the validated quote is authoritative: the requested amount/currency must match it exactly, and a mismatch can return 409 QUOTESESSIONMISMATCH with canonical quotedamount and currency_iso.

Authorize-only mandate setup

Adding mandatesetup.intent: "mandatesetup" changes the lifecycle. The iframe authorizes and activates a standing mandate, while the setup order and transaction become authorized; no token/CVV is emitted for an immediate charge. Do not run the custom payment-result loop below for this intent.

Resolve and store the active mandate on the server. A later charge uses POST /v1/mandates/{mandateid}/charge; after using that charge's credential server-side, report the real outcome to POST /v1/mandates/{mandateid}/charges/{txn_id}/report. These are mandate endpoints, not Session API report-status. See [references/session-api-reference.md](references/session-api-reference.md#mandate-setup) for the exact shapes.

Relevant validation

  • Merchant callers must send userid and useremail.
  • user_email must use a routable ICANN domain; reserved/local TLDs are rejected.
  • merchant_details.url must be a public HTTPS URL on an ICANN-delegated domain, with no IP host or embedded credentials. Paths, queries, and fragments are reduced to the origin.
  • countrycodeiso2 is two uppercase letters.
  • currency is an uppercase code in Prava's supported allowlist; it is not an arbitrary three-letter value.
  • total_amount currently accepts a non-negative decimal string with at most two fractional digits.
  • callback_url, when present, must be HTTPS.

Success response

{
  "session_id": "ses_...",
  "session_token": "eyJ...",
  "expires_at": "2026-08-21T12:30:00.000Z",
  "iframe_url": "https://sandbox.collect.prava.space?session=ses_...",
  "order_id": "ord_..."
}

Keep sessionid for server-side polling. Pass sessiontoken and the unmodified iframe_url separately to PravaSDK.collectPAN.

Integration workflow

1. Inspect the application before editing

  • Detect the framework and package manager.
  • Reuse its auth identity; do not hardcode production users.
  • Reuse its server-side cart/order source and validation.
  • Match its component library, loading/error patterns, and styling.
  • Decide whether the flow is embedded or hosted before creating a session.

2. Install and configure

npm install @prava-sdk/core

Next.js example:

PRAVA_BACKEND_URL=https://sandbox.api.prava.space
MERCHANT_SECRET_KEY=sk_test_YOUR_SECRET_KEY_HERE
NEXT_PUBLIC_PUBLISHABLE_KEY=pk_test_YOUR_PUBLISHABLE_KEY_HERE

Express example:

PRAVA_BACKEND_URL=https://sandbox.api.prava.space
MERCHANT_SECRET_KEY=sk_test_YOUR_SECRET_KEY_HERE
PRAVA_PUBLISHABLE_KEY=pk_test_YOUR_PUBLISHABLE_KEY_HERE

Only the publishable key needs to reach the browser.

3. Create the session on the server

Define the wire types rather than accepting an untyped legacy array:

type IntegrationType = 'embedding' | 'full_checkout';

interface PurchaseContextEntry {
  merchant_details: {
    name: string;
    url: string;
    country_code_iso2: string;
    category_code?: string;
    category?: string;
  };
  product_details: Array<{
    product_id?: string;
    description: string;
    unit_price: string;
    quantity?: number;
  }>;
  effective_until_minutes?: number;
}

type PurchaseContext =
  | { custom: PurchaseContextEntry[]; quote?: false }
  | { quote: true; quote_id: string; access_grant?: string };

The application-facing session endpoint should authenticate the user and build the API body from trusted state:

const response = await fetch(`${PRAVA_BACKEND_URL}/v1/sessions`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${MERCHANT_SECRET_KEY}`,
  },
  body: JSON.stringify({
    user_id: authenticatedUser.id,
    user_email: authenticatedUser.email,
    total_amount: order.total,
    currency: order.currency,
    external_order_ref: order.id,
    integration_type: 'embedding',
    purchase_context: { custom: [order.purchaseContext] },
  }),
  cache: 'no-store',
});

Validate the response before returning the non-secret session fields needed by the page. Do not send MERCHANTSECRETKEY or a payment-result payload to the browser.

4. Render the checkout

Embedded iframe

Create the session with integration_type: "embedding", then mount the returned URL without altering it:

import { PravaSDK } from '@prava-sdk/core';

const prava = new PravaSDK({ publishableKey });

// Start the iframe, but do not use this Promise/callback as payment success.
// Current iframe completion is determined by the server lifecycle below.
void prava.collectPAN({
  sessionToken: session.session_token,
  iframeUrl: session.iframe_url,
  container: '#prava-card-form',
  onReady: () => setReady(true),
  onChange: (state) => setValidation(state),
}).catch((error) => showError(error.message));

Current iframe builds emit enrollment/transaction lifecycle events but not the legacy PRAVASUCCESS event that resolves collectPAN() and triggers onSuccess. Do not await that Promise or gate UI/payment state on onSuccess; use onReady for mount state and the sanitized server poll for payment state. Destroy the SDK on unmount. In React Strict Mode, reset the mount guard in cleanup so the development remount can initialize again. Preserve the returned iframeurl; the SDK appends only the parent origin.

Hosted/new-tab checkout

Create the session with integrationtype: "fullcheckout". The SDK is unnecessary:

window.open(session.iframe_url, '_blank', 'noopener,noreferrer');

Do not add a token query parameter. The returned URL already carries the opaque session ID.

5. Poll and settle on the server

The merchant payment-result state machine for an immediate custom checkout (not quote mode and not authorize-only mandate setup) is:

pending | processing
        ↓
awaiting_result  (custom checkout credential is ready)
        ↓  charge through your processor, then report the real result
completed | failed

For custom checkout, poll until a complete credential-bearing line item appears at awaiting_result:

type PaymentStatus =
  | 'pending'
  | 'processing'
  | 'awaiting_result'
  | 'completed'
  | 'failed';

async function waitForCustomCredential(sessionId: string) {
  for (let attempt = 0; attempt < 30; attempt += 1) {
    const response = await fetch(
      `${PRAVA_BACKEND_URL}/v1/sessions/${encodeURIComponent(sessionId)}/payment-result?_t=${Date.now()}`,
      {
        headers: { Authorization: `Bearer ${MERCHANT_SECRET_KEY}` },
        cache: 'no-store',
      },
    );
    const result = await response.json();

    if (!response.ok) {
      throw new Error(result.error?.message ?? `Payment poll failed (${response.status})`);
    }
    if (result.status === 'failed') {
      throw new Error(
        result.error?.message ??
        result.transactions?.[0]?.error?.message ??
        'Payment failed',
      );
    }
    if (result.status === 'awaiting_result') {
      const lineItem = result.transactions
        ?.flatMap((transaction: { line_items?: unknown[] }) => transaction.line_items ?? [])
        .find((item: any) =>
          item.token && item.dynamic_cvv && item.expiry_month && item.expiry_year,
        );
      if (lineItem) return lineItem;
    }
    if (result.status === 'completed') {
      return null; // already settled; do not charge the credential again
    }

    await new Promise((resolve) => setTimeout(resolve, 3000));
  }
  throw new Error('Timed out waiting for a payment credential');
}

Use the returned token, dynamiccvv, expirymonth, and expiryyear only inside the trusted payment process. Before calling the processor, atomically create or acquire a durable payment-attempt record keyed by txnrefid, and pass a stable key derived from txnref_id through the processor's idempotency mechanism. Persist the processor request/reference and result. If a worker crashes after authorization but before report-status, a retry must query or repeat the same idempotent processor operation and report its stored outcome; it must not submit a new charge. An in-memory lock is insufficient.

Then report what the processor actually returned:

async function reportPaymentOutcome(args: {
  sessionId: string;
  txnRefId: string;
  approved: boolean;
  authorizationCode?: string;
  responseCode?: string;
  amountPaid?: string;
}) {
  const response = await fetch(
    `${PRAVA_BACKEND_URL}/v1/sessions/${encodeURIComponent(args.sessionId)}/report-status`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${MERCHANT_SECRET_KEY}`,
      },
      body: JSON.stringify({
        txn_ref_id: args.txnRefId,
        txn_status: args.approved ? 'APPROVED' : 'DECLINED',
        ...(args.authorizationCode && { authorization_code: args.authorizationCode }),
        ...(args.responseCode && { response_code: args.responseCode }),
        ...(args.amountPaid && { amount_paid: args.amountPaid }),
      }),
      cache: 'no-store',
    },
  );
  const result = await response.json();
  if (!response.ok) {
    throw new Error(result.error?.message ?? `Report failed (${response.status})`);
  }
  return result;
}

Return only a sanitized status to the browser. Never return the token, dynamic CVV, full payment-result body, or processor authorization data to a client component.

For quote checkout, Prava owns the merchant checkout and suppresses credentials. Continue polling through pending/processing; treat top-level completed or failed as terminal. transactions may be empty, and failure may live in top-level error. Do not wait for or expose a credential-bearing awaiting_result response in quote mode.

For mandatesetup.intent: "mandatesetup", do not use this Session API polling path at all. Complete the authorization flow, persist the resulting mandate server-side, and use the mandate charge/report lifecycle for later purchases.

6. Test in sandbox

  • Use https://sandbox.api.prava.space with pktest and sktest keys.
  • Use only the network test card assigned by Prava onboarding.
  • A first run on a new browser/device can include device-binding OTP before passkey registration; use the sandbox OTP documented for the account (the standard sandbox code is 456789).
  • Passkeys require HTTPS or localhost and a WebAuthn-capable browser/device.
  • Sessions normally expire after about 15 minutes. Create a new session after expiry.

Framework templates

  • Next.js server/session logic: templates/nextjs/server-action.ts
  • Next.js SDK mount: templates/nextjs/card-form-component.tsx
  • Next.js page/state example: templates/nextjs/page-integration.tsx
  • Express server routes: templates/express/session-route.ts
  • Vanilla browser half: templates/vanilla/integration.html (pair it with a server route and an ESM-aware bundler/import map; it contains no secret key)

When adapting a template, preserve the contract and security boundaries while replacing its demo user, order, merchant, processor, layout, and error presentation with the application's real implementations.

Common failures

Symptom Check
400 VAL2001 on purchasecontext The current wire shape is { custom: [...] } or { quote: true, quote_id }; a bare array is invalid.
Embedded flow looks like a hosted checkout Create the session with integration_type: "embedding".
401 while polling Use session_id in the path and the merchant secret key as Bearer auth, server-side.
Poll remains pending in Next.js Add a timestamp query, cache: 'no-store', and next: { revalidate: 0 }.
Poll reaches awaiting_result but never completed The credential is ready; charge it and call report-status.
A worker retry can charge twice Atomically claim txnrefid in durable storage and reuse it as the processor idempotency anchor before touching the credential.
Iframe preview returns 404 Pass iframeurl verbatim. Do not replace its ses... query value with the JWT.
React development remount breaks the iframe Reset the Strict Mode mount guard and destroy the SDK in cleanup.
Loading spinner never clears Keep onReady, plus an iframe MutationObserver and bounded fallback timeout.
collectPAN() / onSuccess never finishes Current iframe builds do not emit the legacy PRAVA_SUCCESS event. Start collectPAN() without awaiting it, catch errors, and use authenticated server polling as the payment authority.
Quote completion has no transaction rows This is valid; use top-level status/error and shop_pay.
Mandate setup polling never reaches awaiting_result Authorize-only setup emits no credential; use the later /v1/mandates/{id}/charge and charge-report lifecycle.
Merchant URL or user email is rejected late Use a public HTTPS ICANN merchant origin and a routable email domain when creating the session.
Cancel hides the UI but checkout still works Authorize ownership and call the server-side session revoke endpoint before offering a fresh attempt.

Security checklist

  • Merchant secret key exists only in a server secret store/environment.
  • The application server authenticates the caller and derives user/order data from trusted state.
  • Every browser-facing status request is authorized against a durable session/user ownership record.
  • Only the publishable key, session token, and verbatim iframe URL reach the iframe host page.
  • Payment-result polling, credential use, and report-status happen server-side.
  • Token, dynamic CVV, session JWT, and quote access grant are absent from browser rendering and logs.
  • Custom checkout stops on credential-ready awaiting_result, not only completed.
  • Each txnrefid has a durable processor attempt/claim and a stable processor idempotency key.
  • Every attempted custom charge reports its real APPROVED or DECLINED result.
  • Quote completion handles empty transactions and top-level errors.
  • Authorize-only mandate setup is not sent through the immediate custom credential/report loop.
  • Active sessions are revoked server-side on explicit Cancel before local state is reset.
  • Production uses HTTPS and environment-matched live keys/URLs.

Built by Prava Payments — the payment stack for AI agents.