Skip to main content

Overview

When you invoke an intent, you receive one-time payment tokens (virtual card number, expiry, and CVV) that can be used at any merchant checkout. Since these tokens work like regular card details, they can be entered into standard checkout forms via browser automation.

Why Browser Automation?

  • Universal Compatibility: Tokens work with any merchant checkout — Stripe, Braintree, Adyen, or custom PSPs
  • No Merchant Integration Required: Execute payments without needing merchant API access
  • AI Agent Ready: Perfect for autonomous agents completing purchases on behalf of users

Build Your Own Flow

The payment tokens returned by invokeIntent() are standard card details that can be used however you choose:
  • Browser automation (covered here) — Automate form filling on merchant sites
  • Direct API integration — If you have merchant API access, use tokens directly
  • Mobile automation — Use Appium or similar tools for mobile checkouts
  • Custom solutions — Any system that accepts card payments
Server-side execution: Browser automation should run on your backend servers, not in the user’s browser, to maintain security and reliability.

Setup

npm install playwright

Complete Example

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

const prava = new PravaSDK({ publishableKey: 'pk_live_xxx' });

async function executeCheckout(intentId: string, checkoutInfo: CheckoutInfo) {
  // 1. Get payment tokens
  const tokens = await prava.invokeIntent({
    intentId,
    merchant: checkoutInfo.merchant,
    amount: checkoutInfo.amount,
  });

  // 2. Launch browser
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  try {
    // 3. Navigate to checkout
    await page.goto(checkoutInfo.checkoutUrl);
    await page.waitForLoadState('networkidle');

    // 4. Fill shipping
    await page.fill('[name="email"]', checkoutInfo.email);
    await page.fill('[name="firstName"]', checkoutInfo.firstName);
    await page.fill('[name="lastName"]', checkoutInfo.lastName);
    await page.fill('[name="address"]', checkoutInfo.address);

    // 5. Fill payment
    await page.fill('[name="cardNumber"]', tokens.pan);
    await page.fill('[name="expMonth"]', String(tokens.expMonth));
    await page.fill('[name="expYear"]', String(tokens.expYear));
    await page.fill('[name="cvv"]', tokens.cvv);

    // 6. Submit
    await page.click('button[type="submit"]');
    await page.waitForURL('**/confirmation', { timeout: 30000 });

    return { success: true };
  } catch (error) {
    await page.screenshot({ path: `error-${Date.now()}.png` });
    throw error;
  } finally {
    await browser.close();
  }
}

Checkout Flow Patterns

Shopify Checkout

async function shopifyCheckout(page, tokens, info) {
  await page.goto(`${info.storeUrl}/checkout`);
  await page.fill('input[name="email"]', info.email);
  await page.fill('input[name="firstName"]', info.firstName);
  await page.fill('input[name="lastName"]', info.lastName);
  await page.fill('input[name="address1"]', info.address);
  await page.click('button[type="submit"]');

  // Shopify uses Stripe iframes
  const cardFrame = page.frameLocator('iframe[name*="card-number"]');
  await cardFrame.locator('input[name="cardnumber"]').fill(tokens.pan);

  const expFrame = page.frameLocator('iframe[name*="card-expiry"]');
  await expFrame.locator('input[name="exp-date"]').fill(
    `${String(tokens.expMonth).padStart(2, '0')}${String(tokens.expYear).slice(-2)}`
  );

  const cvcFrame = page.frameLocator('iframe[name*="card-cvc"]');
  await cvcFrame.locator('input[name="cvc"]').fill(tokens.cvv);

  await page.click('button[type="submit"]');
}

Handling Payment Iframes

Many merchants use embedded payment forms (Stripe, Braintree, etc.):
async function fillPaymentIframe(page, tokens) {
  // Stripe: multiple separate iframes
  const numberFrame = page.frameLocator('iframe[title*="card number"]');
  await numberFrame.locator('input').fill(tokens.pan);

  const expiryFrame = page.frameLocator('iframe[title*="expiration"]');
  await expiryFrame.locator('input').fill(
    `${String(tokens.expMonth).padStart(2, '0')}${String(tokens.expYear).slice(-2)}`
  );

  const cvcFrame = page.frameLocator('iframe[title*="CVC"]');
  await cvcFrame.locator('input').fill(tokens.cvv);
}

Error Handling

async function robustCheckout(intentId, info, maxRetries = 2) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await executeCheckout(intentId, info);
    } catch (error) {
      if (error.message.includes('CAPTCHA')) {
        throw new Error('CAPTCHA detected — manual intervention required');
      }
      if (attempt >= maxRetries - 1) throw error;
    }
  }
}

Security Best Practices

Never log payment tokens: The pan and cvv are sensitive. Never write them to logs, databases, or error messages.
// ✅ Good: Redact sensitive data
console.log({ action: 'checkout_started', intentId, merchant: info.merchant });

// ❌ Bad: Logging tokens
console.log({ tokens }); // NEVER DO THIS

Secure Token Handling

async function secureCheckout(intentId, info) {
  let tokens = null;
  try {
    tokens = await prava.invokeIntent({
      intentId,
      merchant: info.merchant,
      amount: info.amount,
    });
    return await executeCheckout(tokens, info);
  } finally {
    if (tokens) {
      tokens.pan = '';
      tokens.cvv = '';
      tokens = null;
    }
  }
}

Troubleshooting

IssueCauseSolution
Timeout errorsPage loads slowlyIncrease timeout, use waitForLoadState
Selector not foundSite structure changedUse multiple fallback selectors
CAPTCHA triggeredBot detectionUse residential proxies, rotate IPs
Payment declinedToken constraints violatedVerify intent amount matches
Iframe not loadingCSP restrictionsWait longer, check console errors

Next Steps