Accept an AUB Direct Card Payment
AUB Direct Card is available to partners on request. It is not enabled by default: your NextPay workspace needs the aub-direct-card-v1 feature activated before the API will accept card_online payment intents. Contact your NextPay account manager to request access. Once activated, this guide works against the standard /v2/payment-intents endpoint with no other changes to your integration.
This guide shows you how to accept a credit or debit card payment via AUB Direct Card — create a payment intent with card details, redirect the customer to 3DS authentication, and confirm settlement.
Use this for checkout flows where your customer pays by card and your backend collects the card details before submitting them to NextAPI.
This endpoint receives raw PAN and CVV on your server before NextAPI encrypts them for AUB. Your backend is within PCI scope for card data in transit. Never log, persist, or forward the card fields. See Online Card Payments for the full rules.
Prerequisites
- AUB Direct Card access enabled on your workspace — contact your NextPay account manager to request the
aub-direct-card-v1activation - A merchant and account already created (Wallet Structure)
- A webhook endpoint set up and registered (Setup Webhooks)
- An HTTPS server — AUB rejects non-HTTPS
redirect_urlvalues
Step 1: Collect browser information
3DS requires browser context fields that are only available in the customer's browser. Collect them client-side before initiating checkout and pass them to your backend:
// Run this in the browser before submitting the checkout form
function collectBrowserInfo() {
return {
accept_header: '*/*',
language: navigator.language || 'en-US',
ip: null, // populate server-side from the incoming request IP
java_enabled: navigator.javaEnabled?.() ?? false,
javascript_enabled: true,
screen_color_depth: window.screen.colorDepth,
screen_height: window.screen.height,
screen_width: window.screen.width,
timezone: new Date().getTimezoneOffset(),
user_agent: navigator.userAgent,
challenge_window: 5, // full-page challenge window
};
}
Send these values along with the card details to your backend endpoint.
Step 2: Create the payment intent
Your backend submits the card data and browser context to NextAPI. The X-Feature-Flag header is required.
- cURL
- Node.js
- Python
curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-X POST "https://api.partners.nextpay.world/v2/payment-intents" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: order-12345-card-v1" \
-H "X-Feature-Flag: aub-direct-card-v1" \
-d '{
"account_id": "YOUR_ACCOUNT_UUID",
"external_id": "order-12345",
"amount": 150000,
"currency": "PHP/2",
"expires_in_seconds": 300,
"intent_created_at": "2026-05-19T10:00:00Z",
"payment_instrument_options": {
"method_type": "card_online",
"method_provider": "ph_aub",
"authorization_mode": "sale_3ds",
"redirect_url": "https://your-site.com/checkout/return",
"card": {
"pan": "CUSTOMER_CARD_NUMBER",
"expiration_year": "2030",
"expiration_month": "12",
"security_code": "CUSTOMER_CVV"
},
"customer_information": {
"first_name": "Ada",
"last_name": "Lovelace",
"phone": "09171234567",
"browser_information": {
"accept_header": "*/*",
"language": "en-US",
"ip": "203.0.113.1",
"java_enabled": false,
"javascript_enabled": true,
"screen_color_depth": 32,
"screen_height": 800,
"screen_width": 1200,
"timezone": -480,
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
"challenge_window": 5
}
},
"billing_information": {
"street1": "Ayala Ave",
"city": "Makati",
"state": "PH-00",
"postcode": "1229",
"country": "PH"
}
}
}'
const response = await fetch('https://api.partners.nextpay.world/v2/payment-intents', {
method: 'POST',
headers: {
'Authorization': 'Basic ' + Buffer.from('YOUR_CLIENT_ID:YOUR_CLIENT_SECRET').toString('base64'),
'Content-Type': 'application/json',
'X-Idempotency-Key': 'order-12345-card-v1',
'X-Feature-Flag': 'aub-direct-card-v1',
},
body: JSON.stringify({
account_id: 'YOUR_ACCOUNT_UUID',
external_id: 'order-12345',
amount: 150000, // PHP 1,500.00 in centavos
currency: 'PHP/2',
expires_in_seconds: 300,
intent_created_at: new Date().toISOString(),
payment_instrument_options: {
method_type: 'card_online',
method_provider: 'ph_aub',
authorization_mode: 'sale_3ds',
redirect_url: 'https://your-site.com/checkout/return',
card: {
pan: customerCardNumber, // from your checkout form
expiration_year: expiryYear,
expiration_month: expiryMonth,
security_code: customerCvv,
},
customer_information: {
first_name: customer.firstName,
last_name: customer.lastName,
phone: customer.phone,
browser_information: browserInfo, // from Step 1
},
billing_information: {
street1: billing.street,
city: billing.city,
state: billing.state, // ISO 3166-2 subdivision code, e.g. "PH-00"
postcode: billing.postcode,
country: 'PH',
},
},
}),
});
const paymentIntent = await response.json();
import requests
import base64
from datetime import datetime, timezone
credentials = base64.b64encode(b'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET').decode()
response = requests.post(
'https://api.partners.nextpay.world/v2/payment-intents',
headers={
'Authorization': f'Basic {credentials}',
'Content-Type': 'application/json',
'X-Idempotency-Key': 'order-12345-card-v1',
'X-Feature-Flag': 'aub-direct-card-v1',
},
json={
'account_id': 'YOUR_ACCOUNT_UUID',
'external_id': 'order-12345',
'amount': 150000, # PHP 1,500.00 in centavos
'currency': 'PHP/2',
'expires_in_seconds': 300,
'intent_created_at': datetime.now(timezone.utc).isoformat(),
'payment_instrument_options': {
'method_type': 'card_online',
'method_provider': 'ph_aub',
'authorization_mode': 'sale_3ds',
'redirect_url': 'https://your-site.com/checkout/return',
'card': {
'pan': customer_card_number,
'expiration_year': expiry_year,
'expiration_month': expiry_month,
'security_code': customer_cvv,
},
'customer_information': {
'first_name': customer['first_name'],
'last_name': customer['last_name'],
'phone': customer['phone'],
'browser_information': browser_info, # from Step 1
},
'billing_information': {
'street1': billing['street'],
'city': billing['city'],
'state': billing['state'],
'postcode': billing['postcode'],
'country': 'PH',
},
},
}
)
payment_intent = response.json()
Response
{
"id": "423e4567-e89b-12d3-a456-426614174003",
"status": "pending",
"account_id": "YOUR_ACCOUNT_UUID",
"external_id": "order-12345",
"amount": 150000,
"currency": "PHP/2",
"payment_instrument": {
"id": "523e4567-e89b-12d3-a456-426614174004",
"method_type": "card_online",
"method_provider": "ph_aub",
"status": "active",
"actions": [
{
"action_kind": "card.challenge",
"status": "requires_action",
"client_instructions": {
"type": "3ds_redirect",
"redirect_url": "https://..."
}
}
]
}
}
Key fields:
payment_instrument.actions[].client_instructions.redirect_url— the AUB 3DS page URL; redirect the customer's browser herepayment_instrument.id— save this; you need it for sandbox simulationexternal_id— your order reference, returned in the settlement webhook
Step 3: Redirect the customer to 3DS
Extract the challenge URL from the response and redirect the customer's browser. This must be a full browser navigation — do not use fetch or embed it in an iframe.
const instrument = paymentIntent.payment_instrument;
const challengeAction = instrument.actions.find(
(a) => a.action_kind === 'card.challenge' && a.status === 'requires_action'
);
if (!challengeAction) {
// No 3DS required — payment may have settled directly (non-3DS mode)
return handleDirectSettlement(paymentIntent);
}
const challengeUrl = challengeAction.client_instructions.redirect_url;
// Store the order ID before redirecting so the return URL can retrieve it
sessionStorage.setItem('pendingOrderId', orderId);
// Full browser redirect — not fetch, not iframe
window.location.href = challengeUrl;
Step 4: Handle the return URL
After the customer completes (or abandons) 3DS, AUB redirects the browser to your redirect_url. At this point the payment may still be processing — poll the payment intent to confirm the final status.
// GET /checkout/return?order_id=order-12345 (your backend)
app.get('/checkout/return', async (req, res) => {
const orderId = req.query.order_id;
const order = await orders.findByExternalId(orderId);
if (!order) {
return res.redirect('/checkout/error?reason=not_found');
}
// Poll for up to 10 seconds — settlement usually completes within a few seconds
let intent;
for (let attempt = 0; attempt < 5; attempt++) {
const response = await fetch(
`https://api.partners.nextpay.world/v2/payment-intents/${order.paymentIntentId}`,
{
headers: {
'Authorization': 'Basic ' + Buffer.from(
`${process.env.NEXTAPI_CLIENT_ID}:${process.env.NEXTAPI_CLIENT_SECRET}`
).toString('base64'),
},
}
);
intent = await response.json();
if (intent.status === 'succeeded') break;
if (intent.status === 'failed') break;
await new Promise((r) => setTimeout(r, 2000));
}
if (intent.status === 'succeeded') {
return res.redirect(`/orders/${orderId}/confirmation`);
}
if (intent.status === 'failed') {
return res.redirect(`/checkout?order_id=${orderId}&error=card_declined`);
}
// Still pending — show a "processing" page that auto-refreshes
return res.redirect(`/checkout/processing?order_id=${orderId}`);
});
Step 5: Handle the settlement webhook
When NextAPI settles the payment, it sends a v2.payment_intent.succeeded event to your registered webhook endpoint. This is the reliable confirmation path — use the return URL handler (Step 4) as a UX fallback, not as your primary fulfillment trigger.
app.post('/webhooks/nextapi', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-nextpay-signature'];
const expected = crypto
.createHmac('sha256', process.env.NEXTAPI_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
try {
if (!crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'))) {
return res.status(401).json({ error: 'Invalid signature' });
}
} catch {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString());
if (event.event === 'v2.payment_intent.succeeded') {
const { id: paymentIntentId, external_id: orderId, amount } = event.payload;
const order = await orders.findByExternalId(orderId);
if (!order) {
return res.status(200).send('OK'); // Unknown order — return 200 to stop retries
}
if (order.status === 'paid') {
return res.status(200).send('OK'); // Already processed — idempotent
}
await orders.markPaid(orderId, { paymentIntentId, amount });
triggerFulfillment(orderId).catch(console.error);
}
res.status(200).send('OK');
});
Card settlement uses event.event (not event.type) and event.payload (not event.data). The event name is v2.payment_intent.succeeded, not payment_intent.paid.
Sandbox simulation
Card payments use a different simulation endpoint from QRPH. Simulate settlement using the payment_instrument_id from Step 2:
- cURL
- Node.js
curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-X POST "https://api.partners.nextpay.world/v2/payment-simulations/payment-instrument" \
-H "Content-Type: application/json" \
-d '{
"payment_instrument_id": "523e4567-e89b-12d3-a456-426614174004"
}'
const response = await fetch(
'https://api.partners.nextpay.world/v2/payment-simulations/payment-instrument',
{
method: 'POST',
headers: {
'Authorization': 'Basic ' + Buffer.from('YOUR_CLIENT_ID:YOUR_CLIENT_SECRET').toString('base64'),
'Content-Type': 'application/json',
},
body: JSON.stringify({ payment_instrument_id: '523e4567-e89b-12d3-a456-426614174004' }),
}
);
const result = await response.json();
// result.result === 'payment_processed'
After simulation, poll GET /v2/payment-intents/{id} — status should be "succeeded".
Payment Intent states
| Status | Meaning |
|---|---|
pending | Created — awaiting 3DS completion and settlement |
succeeded | Settled — funds credited to the account |
failed | Card declined or 3DS authentication failed |
expired | Customer did not complete 3DS within the expiry window |
Key points
- Amounts are in centavos. PHP 1,500.00 =
150000.currencymust be"PHP/2". - Feature header is required. Requests without
X-Feature-Flag: aub-direct-card-v1return422. This header is only accepted on workspaces where the feature has been activated. - No iframe. The 3DS challenge must be a full browser navigation. Iframes break 3DS.
- All
browser_informationfields are required. Collect them client-side before your backend submits the request. - Both the return URL and the webhook can confirm settlement. Handle both idempotently — check
order.status === 'paid'before processing. - Webhook event is
v2.payment_intent.succeeded. Access data viaevent.payload, notevent.data. - Do not log the request body on the payment intent creation route. It contains PAN and CVV.
Related
- Online Card Payments — the mental model behind this flow
- Build a Card Checkout — end-to-end tutorial
- Verify Webhook Signatures — full HMAC verification reference
- Collections Lifecycle — how
card_onlinefits Money In