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
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
| 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 | Token constraints violated | Verify intent amount matches |
| Iframe not loading | CSP restrictions | Wait longer, check console errors |
Next Steps