Guides
Quick start
This guide walks you through the complete flow of making a cross-border payment with the Nexpay API — from setting up your payee to submitting the payment. By the end, you'll have a working integration that creates, validates, and submits a payment intent.
This is the operator-driven flow
This guide drives a payment end-to-end on the payer's behalf — you supply the quote and the payer's identity document. If you'd rather send the payer a URL where they enter their own details and pay, use Payment links instead.
Overview
Every payment in Nexpay is a payment intent — one typed object that handles every scenario. A typical manual flow follows these steps:
- Create a payee — the recipient of funds.
- Get a quote — check the exchange rate and fees.
- Create a student — the payer and subject of the payment.
- Upload the identity document — for compliance.
- Create the payment intent — describe the payer, recipient, and amount.
- Dry-run the intent — confirm it would succeed before moving money.
- Submit the intent — execute the payment.
- Track the status — poll the intent until it completes.
Step 1: Set up your API key
All requests authenticate with an API key sent on the X-API-Key header. The value is your clientId:secret pair joined by a single colon (no URL-encoding, no base64). See Authentication. If you haven't created a key yet, follow the API keys guide.
Every example below uses this header:
const headers = {
'Content-Type': 'application/json',
'X-API-Key': 'nxp_ck_your-client-id:nxp_sk_your-secret',
};
Step 2: Create a payee
A payee is the entity receiving the funds — for example, a university. You only need to do this once per recipient.
curl -X POST https://api.nexpay.com.au/v2/payees \
-H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
-H 'Content-Type: application/json' \
-d '{
"name": "University of Sydney",
"countryCode": "AU",
"currencyCode": "AUD"
}'
const payeeResponse = await fetch('https://api.nexpay.com.au/v2/payees', {
method: 'POST',
headers,
body: JSON.stringify({
"name": "University of Sydney",
"countryCode": "AU",
"currencyCode": "AUD"
}),
});
const { data: payee } = await payeeResponse.json();
console.log('Payee ID:', payee.id); // e.g. 42 — this is the connectorPayeeId
use Illuminate\Support\Facades\Http;
$response = Http::withHeaders([
'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
])->post('https://api.nexpay.com.au/v2/payees', [
'name' => 'University of Sydney',
'countryCode' => 'AU',
'currencyCode' => 'AUD',
]);
$payee = $response->json('data');
echo $payee['id']; // e.g. 42 — this is the connectorPayeeId
See: Creating a payee
Step 3: Get a quote
Request a quote to see the exchange rate and available settlement methods. You'll need the payee ID from the previous step:
curl -X POST https://api.nexpay.com.au/v2/quotes \
-H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
-H 'Content-Type: application/json' \
-d '{
"payouts": [
{
"payeeId": 42,
"amount": 5000
}
],
"countryCode": "AU",
"paymentType": "provider"
}'
const quoteResponse = await fetch('https://api.nexpay.com.au/v2/quotes', {
method: 'POST',
headers,
body: JSON.stringify({
payouts: [{ payeeId: payee.id, amount: 5000 }], // major-unit decimal
countryCode: 'AU', // 2-letter ISO of the payer country
paymentType: 'provider',
}),
});
const { data: quote } = await quoteResponse.json();
console.log('Quote ID:', quote.quoteId); // UUID — not `quote.id`
console.log('Expires at:', quote.expiresOn); // not `expiresAt`
console.log('Variants:', quote.variants.length);
use Illuminate\Support\Facades\Http;
$response = Http::withHeaders([
'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
])->post('https://api.nexpay.com.au/v2/quotes', [
'payouts' => [
['payeeId' => 42, 'amount' => 5000], // major-unit decimal
],
'countryCode' => 'AU', // 2-letter ISO of the payer country
'paymentType' => 'provider',
]);
$quote = $response->json('data');
echo $quote['quoteId']; // UUID — not `quote.id`
echo $quote['expiresOn']; // not `expiresAt`
echo count($quote['variants']);
Each variant represents a different settlement method with its own rate and fees. Pick the one that works best for your payer:
// Pick a variant. `id` is server-assigned — pass it as selectedQuoteVariantId,
// NOT the array index.
const selectedVariant = quote.variants[0];
console.log('Variant id:', selectedVariant.id);
console.log('Rate:', selectedVariant.fxRate);
console.log('Payer pays:', selectedVariant.fromAmount, selectedVariant.fromCurrency);
console.log('Fee:', selectedVariant.fee);
See: FX quotes and rates
Step 4: Create the student
The student is the payer and the subject of a tuition payment. Create one (or reuse an existing one) and keep its _id — you'll reference it on the intent:
curl -X POST https://api.nexpay.com.au/v2/students \
-H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
-H 'Content-Type: application/json' \
-d '{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"countryCode": "AU"
}'
const studentResponse = await fetch('https://api.nexpay.com.au/v2/students', {
method: 'POST',
headers,
body: JSON.stringify({
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
countryCode: 'AU',
}),
});
const { data: student } = await studentResponse.json();
console.log('Student ID:', student._id);
use Illuminate\Support\Facades\Http;
$response = Http::withHeaders([
'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
])->post('https://api.nexpay.com.au/v2/students', [
'firstName' => 'John',
'lastName' => 'Doe',
'email' => 'john.doe@example.com',
'countryCode' => 'AU',
]);
$student = $response->json('data');
echo $student['_id'];
Already have the student? Look them up instead — GET /v2/students?filter[email]=john.doe@example.com. See Parties for the saved-student model and the inline-details alternative.
Step 5: Upload the payer identity document
Manual payments require a payer identity document for compliance. Upload it before creating the intent:
curl -X POST https://api.nexpay.com.au/v2/documents \
-H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
-F 'file=@passport.jpg'
const identityForm = new FormData();
identityForm.append('file', passportFile);
const identityResponse = await fetch('https://api.nexpay.com.au/v2/documents', {
method: 'POST',
headers: { 'X-API-Key': 'nxp_ck_your-client-id:nxp_sk_your-secret' },
body: identityForm,
});
const { data: identityDoc } = await identityResponse.json();
console.log('Identity doc ID:', identityDoc.documentId);
use Illuminate\Support\Facades\Http;
$response = Http::withHeaders([
'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
])->attach('file', file_get_contents('passport.jpg'), 'passport.jpg')
->post('https://api.nexpay.com.au/v2/documents');
$identityDoc = $response->json('data');
echo $identityDoc['documentId'];
See: Uploading documents
Step 6: Create the payment intent
Now bring it all together. The intent describes the payer, the recipient(s), and the amount, with the quote and identity document under instructions.manualPayment:
curl -X POST https://api.nexpay.com.au/v2/payment-intents \
-H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
-H 'Content-Type: application/json' \
-d '{
"useCase": "education_provider_tuition",
"deliveryMode": "manual",
"amountMode": "payer_fixed",
"payer": {
"role": "student",
"partyId": "665f1a2b3c4d5e6f7a8b9c0d"
},
"subject": {
"role": "student",
"partyId": "665f1a2b3c4d5e6f7a8b9c0d"
},
"recipients": [
{
"role": "education_provider",
"order": 1,
"connectorPayeeId": 42,
"splitRole": "principal_provider_amount",
"amount": {
"payerAmount": "5000.00",
"currency": "AUD"
}
}
],
"instructions": {
"manualPayment": {
"quoteId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"selectedQuoteVariantId": "665f1a2b3c4d5e6f7a8b9c1e",
"purpose": "undergraduate",
"countryCode": "AU",
"payerIdentityDocumentId": "3fa85f64-5717-4562-b3fc-2c963f66afb7",
"quotePayoutOrderConfirmed": true
}
},
"termsAccepted": true
}'
const intentResponse = await fetch('https://api.nexpay.com.au/v2/payment-intents', {
method: 'POST',
headers,
body: JSON.stringify({
useCase: 'education_provider_tuition',
deliveryMode: 'manual',
amountMode: 'payer_fixed',
payer: {
role: 'student',
partyId: student._id,
},
subject: {
role: 'student',
partyId: student._id, // the student is also the subject
},
recipients: [
{
role: 'education_provider',
order: 1,
connectorPayeeId: payee.id,
splitRole: 'principal_provider_amount',
amount: {
payerAmount: '5000.00', // decimal STRING — not a number
currency: 'AUD',
},
},
],
instructions: {
manualPayment: {
quoteId: quote.quoteId,
selectedQuoteVariantId: selectedVariant.id, // variant.id, not an array index
purpose: 'undergraduate',
countryCode: 'AU',
payerIdentityDocumentId: identityDoc.documentId,
quotePayoutOrderConfirmed: true,
},
},
termsAccepted: true,
}),
});
const { data: intent } = await intentResponse.json();
console.log('Intent ID:', intent.id);
console.log('Status:', intent.status); // "draft"
use Illuminate\Support\Facades\Http;
$response = Http::withHeaders([
'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
])->post('https://api.nexpay.com.au/v2/payment-intents', [
'useCase' => 'education_provider_tuition',
'deliveryMode' => 'manual',
'amountMode' => 'payer_fixed',
'payer' => [
'role' => 'student',
'partyId' => '665f1a2b3c4d5e6f7a8b9c0d',
],
'subject' => [
'role' => 'student',
'partyId' => '665f1a2b3c4d5e6f7a8b9c0d', // the student is also the subject
],
'recipients' => [
[
'role' => 'education_provider',
'order' => 1,
'connectorPayeeId' => 42,
'splitRole' => 'principal_provider_amount',
'amount' => [
'payerAmount' => '5000.00', // decimal STRING — not a number
'currency' => 'AUD',
],
],
],
'instructions' => [
'manualPayment' => [
'quoteId' => '3fa85f64-5717-4562-b3fc-2c963f66afa6',
'selectedQuoteVariantId' => '665f1a2b3c4d5e6f7a8b9c1e', // variant.id, not an array index
'purpose' => 'undergraduate',
'countryCode' => 'AU',
'payerIdentityDocumentId' => '3fa85f64-5717-4562-b3fc-2c963f66afb7',
'quotePayoutOrderConfirmed' => true,
],
],
'termsAccepted' => true,
]);
$intent = $response->json('data');
echo $intent['id'];
echo $intent['status']; // "draft"
termsAccepted must be true. To send a one-off payer without a saved student, pass payer.details inline instead of partyId — see Manual payment with splits for the full payload and the split (commission) variant.
Step 7: Dry-run the intent
A dry-run validates the intent without moving money — it confirms the scenario is allowed for your tenant and lists any missing fields:
curl -X POST https://api.nexpay.com.au/v2/payment-intents/665f1a2b3c4d5e6f7a8b9c2f/dry-run \
-H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret'
const dryRunResponse = await fetch(`https://api.nexpay.com.au/v2/payment-intents/${intent.id}/dry-run`, {
method: 'POST',
headers,
});
const { data: dryRun } = await dryRunResponse.json();
console.log('Rule status:', dryRun.rule.status); // "enabled" when ready
console.log('Missing:', dryRun.rule.missingRequirements); // [] when ready
use Illuminate\Support\Facades\Http;
$response = Http::withHeaders([
'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
])->post('https://api.nexpay.com.au/v2/payment-intents/665f1a2b3c4d5e6f7a8b9c2f/dry-run');
$dryRun = $response->json('data');
echo $dryRun['rule']['status']; // "enabled" when ready
print_r($dryRun['rule']['missingRequirements']); // [] when ready
When rule.status is enabled and missingRequirements is empty, you're ready to submit.
Step 8: Submit the payment intent
Submit executes the payment. Always include an Idempotency-Key header:
curl -X POST https://api.nexpay.com.au/v2/payment-intents/665f1a2b3c4d5e6f7a8b9c2f/submit \
-H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
-H 'Idempotency-Key: 3fa85f64-5717-4562-b3fc-2c963f66afc8'
const submitResponse = await fetch(`https://api.nexpay.com.au/v2/payment-intents/${intent.id}/submit`, {
method: 'POST',
headers: {
...headers,
// Derive from a stable upstream id (e.g. your order id) in production —
// generating a fresh UUID on each retry defeats the purpose. See /docs/idempotency.
'Idempotency-Key': crypto.randomUUID(),
},
});
const { data: submitted } = await submitResponse.json();
console.log('Status:', submitted.status);
console.log('Connector payment ids:', submitted.executions?.[0]?.connectorPaymentIds);
use Illuminate\Support\Facades\Http;
// Derive the Idempotency-Key from a stable upstream id (e.g. your order id) in
// production — generating a fresh UUID on each retry defeats the purpose. See /docs/idempotency.
$response = Http::withHeaders([
'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
'Idempotency-Key' => '3fa85f64-5717-4562-b3fc-2c963f66afc8',
])->post('https://api.nexpay.com.au/v2/payment-intents/665f1a2b3c4d5e6f7a8b9c2f/submit');
$submitted = $response->json('data');
echo $submitted['status'];
print_r($submitted['executions'][0]['connectorPaymentIds'] ?? []);
Idempotency on submit
POST /v2/payment-intents/{id}/submit is idempotent — Create is not. Reuse the same key across retries of the same submit so a retry never dispatches a second payment. See Idempotency.
Step 9: Track the status
Poll the intent until it reaches a terminal status:
curl https://api.nexpay.com.au/v2/payment-intents/665f1a2b3c4d5e6f7a8b9c2f \
-H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret'
const statusResponse = await fetch(`https://api.nexpay.com.au/v2/payment-intents/${intent.id}`, {
method: 'GET',
headers,
});
const { data: current } = await statusResponse.json();
console.log('Payment status:', current.status);
use Illuminate\Support\Facades\Http;
$response = Http::withHeaders([
'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
])->get('https://api.nexpay.com.au/v2/payment-intents/665f1a2b3c4d5e6f7a8b9c2f');
$current = $response->json('data');
echo $current['status'];
The intent moves through these statuses as it's processed:
draft → ready_for_quote → quoted → ready_for_execution → executing → completed
Stop polling on a terminal status: completed, partially_completed, failed, or cancelled. See Status lifecycle for the full state machine and a ready-made polling loop.
Complete example
Here's the entire flow in one script:
const API = 'https://api.nexpay.com.au/v2';
const headers = {
'Content-Type': 'application/json',
'X-API-Key': 'nxp_ck_your-client-id:nxp_sk_your-secret',
};
// 1. Create payee (or use an existing one)
const { data: payee } = await fetch(`${API}/payees`, {
method: 'POST', headers,
body: JSON.stringify({
name: 'University of Sydney', countryCode: 'AU', currencyCode: 'AUD',
}),
}).then(r => r.json());
// 2. Get quote
const { data: quote } = await fetch(`${API}/quotes`, {
method: 'POST', headers,
body: JSON.stringify({
payouts: [{ payeeId: payee.id, amount: 5000 }],
countryCode: 'AU', paymentType: 'provider',
}),
}).then(r => r.json());
const selectedVariant = quote.variants[0];
// 3. Create the student (payer + subject)
const { data: student } = await fetch(`${API}/students`, {
method: 'POST', headers,
body: JSON.stringify({
firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com', countryCode: 'AU',
}),
}).then(r => r.json());
// 4. Upload the payer identity document
const identityForm = new FormData();
identityForm.append('file', passportFile);
const { data: identityDoc } = await fetch(`${API}/documents`, {
method: 'POST',
headers: { 'X-API-Key': 'nxp_ck_your-client-id:nxp_sk_your-secret' },
body: identityForm,
}).then(r => r.json());
// 5. Create the payment intent
const { data: intent } = await fetch(`${API}/payment-intents`, {
method: 'POST', headers,
body: JSON.stringify({
useCase: 'education_provider_tuition',
deliveryMode: 'manual',
amountMode: 'payer_fixed',
payer: { role: 'student', partyId: student._id },
subject: { role: 'student', partyId: student._id },
recipients: [{
role: 'education_provider', order: 1, connectorPayeeId: payee.id,
splitRole: 'principal_provider_amount',
amount: { payerAmount: '5000.00', currency: 'AUD' },
}],
instructions: {
manualPayment: {
quoteId: quote.quoteId,
selectedQuoteVariantId: selectedVariant.id,
purpose: 'undergraduate',
countryCode: 'AU',
payerIdentityDocumentId: identityDoc.documentId,
quotePayoutOrderConfirmed: true,
},
},
termsAccepted: true,
}),
}).then(r => r.json());
// 6. Dry-run
const { data: dryRun } = await fetch(`${API}/payment-intents/${intent.id}/dry-run`, {
method: 'POST', headers,
}).then(r => r.json());
if (dryRun.rule.status !== 'enabled' || dryRun.rule.missingRequirements.length) {
throw new Error(`Not ready: ${JSON.stringify(dryRun.rule.missingRequirements)}`);
}
// 7. Submit (Idempotency-Key derived from a stable upstream id in production — see /docs/idempotency)
const { data: submitted } = await fetch(`${API}/payment-intents/${intent.id}/submit`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': crypto.randomUUID() },
}).then(r => r.json());
console.log('Submitted:', submitted.status);
Next steps
- Read Manual payment with splits for the full payment-intent model — splits, refunds, payroll, and every scenario field.
- Learn the status lifecycle to handle every stage of the payment.
- Set up error handling to gracefully manage failures.
- Use lookup data to build dynamic payment forms with the correct countries, purposes, and payer types.
- Track commissions earned on your payments.