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 ID (
acct_xxx) - 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": "acct_01HXYZ",
"amount": 500000,
"description": "Invoice INV-2025-001",
"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,
description: `Invoice ${invoiceId}`,
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,
'description': f'Invoice {invoice_id}',
'external_id': invoice_id,
}
)
response.raise_for_status()
return response.json()
Response
{
"id": "pi_01HABC",
"account_id": "acct_01HXYZ",
"amount": 500000,
"status": "pending",
"description": "Invoice INV-2025-001",
"external_id": "INV-2025-001",
"qr_code": "00020101021226...",
"qr_image_url": "https://api.partners.nextpay.world/v2/payment-intents/pi_01HABC/qr",
"created_at": "2025-11-15T10:00:00Z",
"expires_at": "2025-11-15T11:00:00Z"
}
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 → paid (customer pays)
pending → cancelled (you call POST /cancel)
pending → expired (time limit exceeded, default 1 hour)
Once in a terminal state (paid, cancelled, expired), the Payment Intent cannot be reused. Create a new one for the next attempt.
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.type) {
case 'payment_intent.paid': {
const { id, external_id, amount } = event.data;
// external_id === your invoice ID
await markInvoicePaid(external_id, { paymentIntentId: id, amount });
break;
}
case 'payment_intent.expired': {
const { external_id } = event.data;
await markInvoiceExpired(external_id);
break;
}
}
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 POST "https://api.partners.nextpay.world/v2/payment-intents/pi_01HABC/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 === 'paid') return intent;
if (['cancelled', '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. - 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.
Related
- Accept a QRPH Payment — simpler overview with QR display examples
- Setup Virtual Collection (static QR)
- Collections Lifecycle