Skip to main content

Create a Payment Intent

A Payment Intent represents a single payment request for a specific amount. Creating one generates a QRPH code your customer scans to pay. When paid, the funds credit the specified account and NextAPI fires a webhook.

This guide covers the full lifecycle: create → display → handle webhook → verify or cancel.

When to use Payment Intents

Use Payment Intents when you need exact-amount, single-use collection:

  • E-commerce order checkout
  • Invoice payment
  • Subscription renewal for a specific billing period
  • Any scenario where the payer must pay a specific amount

For a persistent QR code that accepts any amount at any time (counter QR, kiosk), use Funding Methods instead.

Prerequisites

  • A merchant and account UUID (e.g. 123e4567-e89b-12d3-a456-426614174000)
  • A registered webhook endpoint
  • Sandbox credentials from /sandbox

Create a Payment Intent

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: invoice-INV-2025-001-v1" \
-d '{
"account_id": "YOUR_ACCOUNT_UUID",
"amount": 500000,
"currency": "PHP/2",
"payment_instrument_options": {
"method_type": "qrph_p2m_reference",
"method_provider": "automatic"
},
"external_id": "INV-2025-001"
}'

Response

{
"id": "123e4567-e89b-12d3-a456-426614174000",
"account_id": "123e4567-e89b-12d3-a456-426614174000",
"external_id": "INV-2025-001",
"amount": 500000,
"currency": "PHP/2",
"status": "pending",
"expires_at": "2025-11-15T10:05:00Z",
"created_at": "2025-11-15T10:00:00Z",
"payment_instrument": {
"id": "123e4567-e89b-12d3-a456-426614174001",
"method_type": "qrph_p2m_reference",
"method_provider": "ph_netbank",
"method_details": {
"merchant_name": "Your Merchant Name",
"reference_label": "...",
"routing_account": "...",
"amount": { "value": 500000, "currency": "PHP/2" },
"resolution": 480
},
"status": "active",
"persistence_mode": "temporary",
"usage_mode": "single"
}
}

Retrieve by external ID

If you need to check whether a Payment Intent already exists for an invoice before creating a new one:

curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
"https://api.partners.nextpay.world/v2/payment-intents/external/INV-2025-001"

This is useful when combined with idempotency keys — the external ID lookup lets you recover the existing intent if your idempotency key has expired.

Payment Intent lifecycle

pending → succeeded   (customer pays)
pending → canceled (you call PATCH /cancel)
pending → expired (time limit exceeded, default 5 minutes / 300s)

Once in a terminal state (succeeded, canceled, expired), the Payment Intent cannot be reused. Create a new one for the next attempt.

No expiry webhook

When a Payment Intent expires, no webhook is emitted. Rely on expires_at from the creation response and perform a GET /v2/payment-intents/{id} if confirmation is needed.

Handle the payment webhook

app.post('/webhooks/nextapi', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-nextpay-signature'];

if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Unauthorized');
}

const event = JSON.parse(req.body);

switch (event.event) {
case 'v2.payment_intent.succeeded': {
const { id, external_id, amount } = event.payload;
// external_id === your invoice ID
await markInvoicePaid(external_id, { paymentIntentId: id, amount });
break;
}

case 'v2.payment_intent.canceled': {
const { external_id } = event.payload;
await markInvoiceCanceled(external_id);
break;
}

// Note: no webhook is fired when a Payment Intent expires.
// Check expires_at and call GET /v2/payment-intents/{id} to confirm expiry.
}

res.status(200).send('OK');
});

Cancel a Payment Intent

If the customer abandons checkout or the order is cancelled, cancel the Payment Intent to prevent it from being paid later:

curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-X PATCH "https://api.partners.nextpay.world/v2/payment-intents/YOUR_PAYMENT_INTENT_ID/cancel"

Cancelling is idempotent — calling it on an already-cancelled intent returns a success response.

Poll for status

In cases where webhooks are unavailable (local dev, network issues), poll directly:

async function waitForPayment(intentId, timeoutMs = 300000) {
const start = Date.now();

while (Date.now() - start < timeoutMs) {
const response = await fetch(
`https://api.partners.nextpay.world/v2/payment-intents/${intentId}`,
{ headers: { 'Authorization': 'Basic ...' } }
);
const intent = await response.json();

if (intent.status === 'succeeded') return intent;
if (['canceled', 'expired'].includes(intent.status)) {
throw new Error(`Payment Intent ${intent.status}`);
}

await new Promise(resolve => setTimeout(resolve, 5000)); // poll every 5s
}

throw new Error('Polling timeout');
}

Prefer webhooks over polling in production — polling adds latency and unnecessary API calls.

Key points

  • Amounts in centavos. PHP 5,000.00 = 500000.
  • account_id is a UUID. Use the UUID returned when the account was created, not a prefixed identifier.
  • Set external_id to your internal reference — it's returned in every webhook and status response.
  • Use X-Idempotency-Key to safely retry failed requests without creating duplicate intents.
  • One intent per payment. Create a new intent if the customer needs to retry after expiry.
  • method_provider options. Use "automatic" (recommended) to let NextAPI select the best provider. You can also specify "ph_netbank" or "ph_coins" to route through a particular network.