Skip to main content

Accept an AUB Direct Card Payment

Early Access — Requires Activation

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.

PCI scope

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-v1 activation
  • 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_url values

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 -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"
}
}
}'

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 here
  • payment_instrument.id — save this; you need it for sandbox simulation
  • external_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');
});
Event field name

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 -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"
}'

After simulation, poll GET /v2/payment-intents/{id}status should be "succeeded".

Payment Intent states

StatusMeaning
pendingCreated — awaiting 3DS completion and settlement
succeededSettled — funds credited to the account
failedCard declined or 3DS authentication failed
expiredCustomer did not complete 3DS within the expiry window

Key points

  • Amounts are in centavos. PHP 1,500.00 = 150000. currency must be "PHP/2".
  • Feature header is required. Requests without X-Feature-Flag: aub-direct-card-v1 return 422. 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_information fields 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 via event.payload, not event.data.
  • Do not log the request body on the payment intent creation route. It contains PAN and CVV.