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
- 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: 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"
}'
async function createPaymentIntent(accountId, invoiceId, amountCentavos) {
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': `invoice-${invoiceId}-v1`,
},
body: JSON.stringify({
account_id: accountId,
amount: amountCentavos,
currency: 'PHP/2',
payment_instrument_options: {
method_type: 'qrph_p2m_reference',
method_provider: 'automatic',
},
external_id: invoiceId,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Payment Intent creation failed: ${error.message}`);
}
return response.json();
}
import requests
import base64
def create_payment_intent(account_id, invoice_id, amount_centavos):
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': f'invoice-{invoice_id}-v1',
},
json={
'account_id': account_id,
'amount': amount_centavos,
'currency': 'PHP/2',
'payment_instrument_options': {
'method_type': 'qrph_p2m_reference',
'method_provider': 'automatic',
},
'external_id': invoice_id,
}
)
response.raise_for_status()
return response.json()
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.
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_idis a UUID. Use the UUID returned when the account was created, not a prefixed identifier.- Set
external_idto your internal reference — it's returned in every webhook and status response. - Use
X-Idempotency-Keyto 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_provideroptions. 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.
Related
- Accept a QRPH Payment — simpler overview with QR display examples
- Setup Virtual Collection (static QR)
- Collections Lifecycle