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
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
| Issue | Cause | Solution |
|---|
| Timeout errors | Page loads slowly | Increase timeout, use waitForLoadState |
| Selector not found | Site structure changed | Use multiple fallback selectors |
| CAPTCHA triggered | Bot detection | Use residential proxies, rotate IPs |
| Payment declined | Invalid credentials | Verify intent amount matches |
| Iframe not loading | CSP restrictions | Wait 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