Skip to main content

Overview

Prava provides end-mile checkout execution via browser automation. When you invoke an intent, you receive one-time payment credentials (tokenized card number, expiry, and dynamic CVV) that can be consumed on any merchant’s payment service provider (PSP). Since these credentials work like regular card details, they can be entered into standard checkout forms.

Why Browser Automation?

  • Universal Compatibility: Credentials 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

While this guide covers Playwright and Puppeteer implementations, you’re free to build your own checkout execution flow. The payment credentials 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 credentials 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_sandbox_your_key',
  environment: 'sandbox'
});

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

  // 2. Launch browser
  const browser = await chromium.launch({
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });

  const context = await browser.newContext({
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    viewport: { width: 1920, height: 1080 },
    locale: 'en-US'
  });

  const page = await context.newPage();

  try {
    // 3. Navigate to merchant site
    await page.goto(checkoutInfo.productUrl);
    await page.waitForLoadState('networkidle');

    // 4. Add to cart (if needed)
    await page.click('button[data-testid="add-to-cart"]');
    await page.waitForURL('**/cart');

    // 5. Proceed to checkout
    await page.click('a[href*="checkout"]');
    await page.waitForURL('**/checkout');

    // 6. Fill shipping information
    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);
    await page.fill('[name="city"]', checkoutInfo.city);
    await page.fill('[name="zipCode"]', checkoutInfo.zipCode);
    await page.selectOption('[name="state"]', checkoutInfo.state);

    // 7. Continue to payment
    await page.click('button:has-text("Continue to Payment")');
    await page.waitForLoadState('networkidle');

    // 8. Fill payment credentials
    const cardNumberFrame = page.frameLocator('iframe[name*="card"]').first();
    await cardNumberFrame.locator('[name="cardNumber"]').fill(credentials.paymentToken);

    await page.fill('[name="cardExpiry"]', 
      `${credentials.tokenExpiration.month}/${credentials.tokenExpiration.year}`
    );
    
    await page.fill('[name="cardCvc"]', credentials.dynamicDataValue);
    await page.fill('[name="cardName"]', `${checkoutInfo.firstName} ${checkoutInfo.lastName}`);

    // 9. Submit payment
    await page.click('button[type="submit"]:has-text("Place Order")');

    // 10. Wait for confirmation
    await page.waitForURL('**/confirmation', { timeout: 30000 });
    
    const orderNumber = await page.locator('[data-testid="order-number"]').textContent();

    return {
      success: true,
      orderNumber,
      transactionId: credentials.transactionId
    };

  } catch (error) {
    // Capture screenshot on error
    await page.screenshot({ path: `error-${Date.now()}.png` });
    throw error;
  } finally {
    await browser.close();
  }
}

Checkout Flow Patterns

Standard Guest Checkout

async function guestCheckout(page: Page, credentials: PaymentCredentials, info: CheckoutInfo) {
  // 1. Product page → Add to cart
  await page.goto(info.productUrl);
  await page.click('[data-add-to-cart]');

  // 2. Cart → Checkout
  await page.click('[href*="checkout"]');

  // 3. Guest checkout (skip login)
  await page.click('button:has-text("Checkout as Guest")');

  // 4. Shipping
  await fillShippingForm(page, info);
  await page.click('button:has-text("Continue")');

  // 5. Payment
  await fillPaymentForm(page, credentials, info);
  
  // 6. Review & submit
  await page.click('button:has-text("Place Order")');
  await page.waitForURL('**/confirmation');
}

Shopify Checkout

async function shopifyCheckout(page: Page, credentials: PaymentCredentials, info: CheckoutInfo) {
  // Shopify-specific selectors
  await page.goto(info.productUrl);
  
  // Add to cart
  await page.click('button[name="add"]');
  await page.waitForSelector('.cart-drawer', { timeout: 5000 }).catch(() => {});
  
  // Checkout
  await page.goto(`${info.storeUrl}/checkout`);

  // Contact
  await page.fill('input[name="email"]', info.email);

  // Shipping
  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.fill('input[name="city"]', info.city);
  await page.fill('input[name="postalCode"]', info.zipCode);
  await page.selectOption('select[name="province"]', info.state);

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

  // Payment
  // Shopify often uses Stripe elements in iframe
  const cardFrame = page.frameLocator('iframe[name*="card-number"]');
  await cardFrame.locator('input[name="cardnumber"]').fill(credentials.paymentToken);

  const expFrame = page.frameLocator('iframe[name*="card-expiry"]');
  await expFrame.locator('input[name="exp-date"]').fill(
    `${credentials.tokenExpiration.month}${credentials.tokenExpiration.year.slice(2)}`
  );

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

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

WooCommerce Checkout

async function wooCommerceCheckout(page: Page, credentials: PaymentCredentials, info: CheckoutInfo) {
  await page.goto(info.productUrl);

  // Add to cart
  await page.click('button.single_add_to_cart_button');
  await page.waitForLoadState('networkidle');

  // Proceed to checkout
  await page.goto(`${info.storeUrl}/checkout`);

  // Billing details
  await page.fill('#billing_first_name', info.firstName);
  await page.fill('#billing_last_name', info.lastName);
  await page.fill('#billing_email', info.email);
  await page.fill('#billing_address_1', info.address);
  await page.fill('#billing_city', info.city);
  await page.fill('#billing_postcode', info.zipCode);
  await page.selectOption('#billing_state', info.state);

  // Payment method
  await page.click('#payment_method_stripe');

  // Card details (WooCommerce Stripe)
  const stripeFrame = page.frameLocator('iframe[name*="__privateStripeFrame"]');
  await stripeFrame.locator('[name="cardnumber"]').fill(credentials.paymentToken);
  await stripeFrame.locator('[name="exp-date"]').fill(
    `${credentials.tokenExpiration.month}${credentials.tokenExpiration.year.slice(2)}`
  );
  await stripeFrame.locator('[name="cvc"]').fill(credentials.dynamicDataValue);

  // Place order
  await page.click('#place_order');
}

Handling Payment Iframes

Many merchants use embedded payment forms (Stripe, Braintree, etc.):
async function fillPaymentIframe(page: Page, credentials: PaymentCredentials) {
  // Method 1: Wait for iframe and switch to it
  const cardFrame = await page.waitForSelector('iframe[name*="card"]');
  const frame = await cardFrame.contentFrame();
  
  if (frame) {
    await frame.fill('[name="cardNumber"]', credentials.paymentToken);
    await frame.fill('[name="cardExpiry"]', 
      `${credentials.tokenExpiration.month}/${credentials.tokenExpiration.year}`
    );
    await frame.fill('[name="cardCvc"]', credentials.dynamicDataValue);
  }

  // Method 2: Use frameLocator (Playwright)
  const cardLocator = page.frameLocator('iframe[name*="card-number"]');
  await cardLocator.locator('input[name="cardnumber"]').fill(credentials.paymentToken);

  // Method 3: Handle multiple separate iframes (Stripe)
  const numberFrame = page.frameLocator('iframe[title*="card number"]');
  await numberFrame.locator('input').fill(credentials.paymentToken);

  const expiryFrame = page.frameLocator('iframe[title*="expiration"]');
  await expiryFrame.locator('input').fill(
    `${credentials.tokenExpiration.month}${credentials.tokenExpiration.year.slice(2)}`
  );

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

Error Handling

async function robustCheckout(intentId: string, info: CheckoutInfo, maxRetries = 2) {
  let attempt = 0;

  while (attempt < maxRetries) {
    try {
      const result = await executeCheckout(intentId, info);
      return result;
    } catch (error) {
      attempt++;

      if (error.message.includes('timeout')) {
        console.log(`Attempt ${attempt} timed out, retrying...`);
        if (attempt < maxRetries) continue;
      }

      if (error.message.includes('CAPTCHA')) {
        throw new Error('CAPTCHA detected - manual intervention required');
      }

      if (error.message.includes('card declined')) {
        throw new Error('Payment declined by issuer');
      }

      if (attempt >= maxRetries) {
        throw new Error(`Checkout failed after ${maxRetries} attempts: ${error.message}`);
      }
    }
  }
}

Selector Strategies

Flexible Selectors

async function smartFillCardNumber(page: Page, cardNumber: string) {
  // Try multiple selectors in order of preference
  const selectors = [
    '[data-testid="card-number"]',
    '[name="cardNumber"]',
    '[name="card-number"]',
    '[placeholder*="Card number"]',
    'input[autocomplete="cc-number"]',
    '#card-number',
    '.card-number'
  ];

  for (const selector of selectors) {
    try {
      const element = await page.locator(selector).first();
      if (await element.isVisible({ timeout: 1000 })) {
        await element.fill(cardNumber);
        return true;
      }
    } catch {
      continue;
    }
  }

  throw new Error('Could not find card number field');
}

Wait Strategies

async function waitForCheckoutReady(page: Page) {
  // Wait for network to be idle
  await page.waitForLoadState('networkidle');

  // Wait for specific elements
  await page.waitForSelector('[name="cardNumber"]', { state: 'visible', timeout: 10000 });

  // Wait for JavaScript to initialize
  await page.waitForFunction(() => {
    return window['checkoutReady'] === true;
  }, { timeout: 5000 }).catch(() => {});

  // Additional delay for dynamic forms
  await page.waitForTimeout(1000);
}

Security Best Practices

Never log credentials: Payment tokens and DAVVs are sensitive. Never write them to logs or error messages.
// ✅ Good: Redact sensitive data
console.log({
  action: 'checkout_started',
  intentId: intentId,
  merchantUrl: info.merchantUrl,
  // No credential data logged
});

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

Secure Credential Handling

// Clear credentials from memory after use
async function secureCheckout(intentId: string, info: CheckoutInfo) {
  let credentials = null;

  try {
    credentials = await prava.invokeIntent(intentId);
    const result = await executeCheckout(credentials, info);
    return result;
  } finally {
    // Clear credentials
    if (credentials) {
      credentials.paymentToken = '';
      credentials.dynamicDataValue = '';
      credentials = null;
    }
  }
}

Production Considerations

Headless vs Headful

// Development: Use headful for debugging
const browser = await chromium.launch({
  headless: false,
  slowMo: 100  // Slow down actions
});

// Production: Always headless
const browser = await chromium.launch({
  headless: true,
  args: [
    '--no-sandbox',
    '--disable-setuid-sandbox',
    '--disable-dev-shm-usage'
  ]
});

Proxy & IP Rotation

const browser = await chromium.launch({
  proxy: {
    server: 'http://proxy.example.com:8080',
    username: 'user',
    password: 'pass'
  }
});

User Agent Rotation

const userAgents = [
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
  'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
];

const randomUA = userAgents[Math.floor(Math.random() * userAgents.length)];
await page.setUserAgent(randomUA);

Troubleshooting

Common Issues

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

Debug Mode

async function debugCheckout(intentId: string, info: CheckoutInfo) {
  const browser = await chromium.launch({ headless: false, slowMo: 500 });
  const context = await browser.newContext({
    recordVideo: { dir: './videos/' }
  });

  const page = await context.newPage();

  // Log all console messages
  page.on('console', msg => console.log('PAGE LOG:', msg.text()));

  // Log all network requests
  page.on('request', req => console.log('REQUEST:', req.url()));

  // Screenshot before each action
  await page.goto(info.productUrl);
  await page.screenshot({ path: '1-product-page.png' });

  await page.click('[data-add-to-cart]');
  await page.screenshot({ path: '2-after-add-to-cart.png' });

  // ... continue with screenshots
}

Next Steps