Guides
Parties — students, payers, and inline details
Every payment intent involves at least one party — the person or entity paying — and many flows also have a separate subject (the person the payment is for, usually a student). Nexpay lets you describe these parties three different ways:
- Inline details — pass
payer.details(and optionallysubject.details) in the payment-intent body. Nothing is persisted across payments. - Saved student —
POST /v2/studentsonce, then reference bypartyIdon every payment for that student. Best for tuition, refunds, or any flow where the same student pays repeatedly. - Saved payer —
POST /v2/payersonce, then reference bypartyId. Best for business / corporate payers that recur across multiple students or invoices.
This guide explains when to use which, and walks the saved-document reuse flow that lets you upload an identity document once per student and reattach it to every future payment.
JSON examples show the unwrapped `data` payload
Every success response on the wire is { "data": { ... } }. The JSON examples in this guide show the inner data value — what you get after const { data } = await response.json(). List responses also include top-level hasMore, count, and sometimes total. Error responses are not wrapped — see Errors.
Pick the right shape
| Scenario | Use this | Why |
|---|---|---|
| One-off payer who won't pay again (random checkout, ad-hoc refund recipient) | Inline payer.details | No persistence overhead. The payer details are written to the intent for audit and never read again. |
| Recurring student (tuition, instalments, multiple semesters) | Saved student — POST /v2/students then reuse partyId | Lets you save identity docs once, list payment history per student, and avoid re-typing personal data. |
| Recurring business payer (a corporate employer, an agent paying for many students) | Saved payer — POST /v2/payers then reuse partyId | Mirrors students but models a non-student paying entity. Can also be a saved-document holder. |
| Payment-link checkout (you don't know the payer until they click) | payer.role: "unknown_link_payer", no partyId, no details | The payer fills in their own details at submission time; Core writes them on the child intent. |
A student and a payer are not mutually exclusive — for a tuition payment, the student is the subject of the payment, and the payer could be the student themselves, a parent, an agent, or a saved business payer. Both subject.partyId and payer.partyId accept saved-student or saved-payer Mongo ids.
Saved students
A student record holds personal details and identity documents for one person. It can be referenced as either the subject or the payer (or both) on any payment intent.
Create a student
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": "Ada",
"lastName": "Lovelace",
"email": "ada@example.com",
"phone": "+61400000000",
"dob": "1998-04-12",
"addressLine1": "1 Macquarie St",
"city": "Sydney",
"state": "NSW",
"postcode": "2000",
"countryCode": "AU",
"taxIdentificationNumber": "123456789"
}'
const headers = {
'Content-Type': 'application/json',
'X-API-Key': 'nxp_ck_your-client-id:nxp_sk_your-secret',
};
const response = await fetch('https://api.nexpay.com.au/v2/students', {
method: 'POST',
headers,
body: JSON.stringify({
firstName: 'Ada',
lastName: 'Lovelace',
email: 'ada@example.com',
phone: '+61400000000',
dob: '1998-04-12',
addressLine1: '1 Macquarie St',
city: 'Sydney',
state: 'NSW',
postcode: '2000',
countryCode: 'AU',
taxIdentificationNumber: '123456789',
}),
});
const { data: student } = await response.json();
console.log('Student _id:', student._id); // e.g. "6a20162451c558203a879c5f"
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' => 'Ada',
'lastName' => 'Lovelace',
'email' => 'ada@example.com',
'phone' => '+61400000000',
'dob' => '1998-04-12',
'addressLine1' => '1 Macquarie St',
'city' => 'Sydney',
'state' => 'NSW',
'postcode' => '2000',
'countryCode' => 'AU',
'taxIdentificationNumber' => '123456789',
]);
$student = $response->json('data');
echo 'Student _id: ' . $student['_id']; // e.g. "6a20162451c558203a879c5f"
Only firstName, lastName, and email are required. Everything else can be added later via PUT /v2/students/{_id}.
Students are identified by Mongo _id (24 hex chars). Pass this verbatim into payment-intent subject.partyId or payer.partyId.
List or look up a student
curl 'https://api.nexpay.com.au/v2/students?filter[email]=ada@example.com' \
-H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret'
// By email (most common — dedupe before creating)
const response = await fetch(
`https://api.nexpay.com.au/v2/students?filter[email]=ada@example.com`,
{ headers },
);
const { data } = await response.json();
const existing = data.students[0]; // may be undefined
use Illuminate\Support\Facades\Http;
// By email (most common — dedupe before creating)
$response = Http::withHeaders([
'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
])->get('https://api.nexpay.com.au/v2/students', [
'filter[email]' => 'ada@example.com',
]);
$data = $response->json('data');
$existing = $data['students'][0] ?? null; // may be null
The list envelope includes hasMore, count, and total at the top level. See Querying data for filter and pagination syntax.
Reference a student on a payment intent
{
// ... useCase, deliveryMode, amountMode, etc.
payer: {
role: 'student',
partyId: '6a20162451c558203a879c5f', // student._id
},
subject: {
role: 'student',
partyId: '6a20162451c558203a879c5f', // same student
},
recipients: [/* ... */],
}
If you don't set payer.details and provide only partyId, Core hydrates the personal details from the saved student at execution time. If you set both, the inline details win and the saved record is left unchanged.
Saved payers
Use POST /v2/payers when the payer is not a student — for example a parent paying on behalf of their child, an agent paying for many students, or a company funding tuition.
curl -X POST 'https://api.nexpay.com.au/v2/payers' \
-H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
-H 'Content-Type: application/json' \
-d '{
"payerType": "individual",
"email": "jane.doe@example.com",
"profile": {
"firstName": "Jane",
"lastName": "Doe",
"phone": "+61412345678",
"dob": "1972-09-30"
},
"address": {
"country": "AU",
"line1": "123 Main Street",
"city": "Sydney",
"state": "NSW",
"postalCode": "2000"
}
}'
const response = await fetch('https://api.nexpay.com.au/v2/payers', {
method: 'POST',
headers,
body: JSON.stringify({
payerType: 'individual',
email: 'jane.doe@example.com',
profile: {
firstName: 'Jane',
lastName: 'Doe',
phone: '+61412345678',
dob: '1972-09-30',
},
address: {
country: 'AU',
line1: '123 Main Street',
city: 'Sydney',
state: 'NSW',
postalCode: '2000',
},
}),
});
const { data: payer } = await response.json();
console.log('Payer _id:', payer._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/payers', [
'payerType' => 'individual',
'email' => 'jane.doe@example.com',
'profile' => [
'firstName' => 'Jane',
'lastName' => 'Doe',
'phone' => '+61412345678',
'dob' => '1972-09-30',
],
'address' => [
'country' => 'AU',
'line1' => '123 Main Street',
'city' => 'Sydney',
'state' => 'NSW',
'postalCode' => '2000',
],
]);
$payer = $response->json('data');
echo 'Payer _id: ' . $payer['_id'];
Reference on a payment intent the same way — payer.partyId: payer._id. Saved payers also hold identity documents via the same saved-document endpoints (see below).
See Creating a payer for the full field reference, including business payers (payerType: "business").
Saved-document reuse — the load-bearing optimisation
Without this, every payment for the same student requires re-uploading the same identity document. With it, you upload once and mint a fresh disposable copy for each payment.
The flow has four steps and involves three different ID fields that all look the same (UUIDs) but aren't interchangeable. Get the names right:
| Step | Endpoint | What ID it returns | Where it goes next |
|---|---|---|---|
| POST /v2/documents | documentId (response: { data: { documentId } }) | Sent as sourceDocumentId in step 2. |
| POST /v2/students/{_id}/documents | legacyDocumentId (a new UUID; not the same as the source) | Used as the path id in steps 3 and 4. |
| GET /v2/students/{_id}/documents | documents[].legacyDocumentId | Used as the path id in step 4. |
| POST /v2/students/{_id}/documents/{legacyDocumentId}/use | documentId (yet another new UUID — single-use) | Pass this on the payment intent as instructions.manualPayment.payerIdentityDocumentId (or wherever an identity doc id is expected). |
The disposable copy is consumed when the payment intent submits. Mint a fresh one for every new payment — they can't be reused across payments.
Worked example
# 1. Upload the passport once (first payment only)
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.pdf'
# -> { "data": { "documentId": "30300c43-4012-436a-b422-2726f89cf7cd" } }
# 2. Save against the student (also first payment only)
curl -X POST 'https://api.nexpay.com.au/v2/students/6a20162451c558203a879c5f/documents' \
-H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
-H 'Content-Type: application/json' \
-d '{
"sourceDocumentId": "30300c43-4012-436a-b422-2726f89cf7cd",
"type": "identity",
"fileName": "passport.pdf",
"contentType": "application/pdf"
}'
# -> { "data": { "legacyDocumentId": "b4f958b9-0114-435c-a4aa-4d3e5df176b4" } }
const headers = {
'Content-Type': 'application/json',
'X-API-Key': 'nxp_ck_your-client-id:nxp_sk_your-secret',
};
// 1. Upload the passport once (first payment only)
const form = new FormData();
form.append('file', passportFile);
const uploadResp = 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: form,
});
const { data: upload } = await uploadResp.json();
const sourceDocumentId = upload.documentId;
// e.g. "30300c43-4012-436a-b422-2726f89cf7cd"
// 2. Save against the student (also first payment only)
const saveResp = await fetch(
`https://api.nexpay.com.au/v2/students/${student._id}/documents`,
{
method: 'POST',
headers,
body: JSON.stringify({
sourceDocumentId,
type: 'identity',
fileName: 'passport.pdf',
contentType: 'application/pdf',
}),
},
);
const { data: saved } = await saveResp.json();
const savedDocId = saved.legacyDocumentId;
// e.g. "b4f958b9-0114-435c-a4aa-4d3e5df176b4" — note: different from sourceDocumentId
use Illuminate\Support\Facades\Http;
$authHeaders = ['X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret'];
// 1. Upload the passport once (first payment only)
$uploadResp = Http::withHeaders($authHeaders)
->attach('file', file_get_contents('passport.pdf'), 'passport.pdf')
->post('https://api.nexpay.com.au/v2/documents');
$upload = $uploadResp->json('data');
$sourceDocumentId = $upload['documentId'];
// e.g. "30300c43-4012-436a-b422-2726f89cf7cd"
// 2. Save against the student (also first payment only)
$saveResp = Http::withHeaders($authHeaders)
->post('https://api.nexpay.com.au/v2/students/6a20162451c558203a879c5f/documents', [
'sourceDocumentId' => $sourceDocumentId,
'type' => 'identity',
'fileName' => 'passport.pdf',
'contentType' => 'application/pdf',
]);
$saved = $saveResp->json('data');
$savedDocId = $saved['legacyDocumentId'];
// e.g. "b4f958b9-0114-435c-a4aa-4d3e5df176b4" — different from sourceDocumentId
Then, for every subsequent payment, skip the upload and just mint a fresh copy:
# 3 (next payment). List saved docs to find the identity one (optional)
curl 'https://api.nexpay.com.au/v2/students/6a20162451c558203a879c5f/documents' \
-H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret'
# -> find the entry where "type": "identity", read its legacyDocumentId
# 4. Mint a disposable copy for THIS payment only
curl -X POST 'https://api.nexpay.com.au/v2/students/6a20162451c558203a879c5f/documents/b4f958b9-0114-435c-a4aa-4d3e5df176b4/use' \
-H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret'
# -> { "data": { "documentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } }
# 5. Submit that disposable documentId as
# instructions.manualPayment.payerIdentityDocumentId on the payment intent
// 3 (next payment). List saved docs to find the identity one (optional)
const listResp = await fetch(
`https://api.nexpay.com.au/v2/students/${student._id}/documents`,
{ headers },
);
const { data: list } = await listResp.json();
const identity = list.documents.find((d) => d.type === 'identity');
// 4. Mint a disposable copy for THIS payment only
const useResp = await fetch(
`https://api.nexpay.com.au/v2/students/${student._id}/documents/${identity.legacyDocumentId}/use`,
{ method: 'POST', headers },
);
const { data: copy } = await useResp.json();
// 5. Submit the disposable id on the payment intent
const intent = await createPaymentIntent({
// ...
instructions: {
manualPayment: {
// ...
payerIdentityDocumentId: copy.documentId, // disposable, single-use
},
},
});
use Illuminate\Support\Facades\Http;
$authHeaders = ['X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret'];
// 3 (next payment). List saved docs to find the identity one (optional)
$listResp = Http::withHeaders($authHeaders)
->get('https://api.nexpay.com.au/v2/students/6a20162451c558203a879c5f/documents');
$list = $listResp->json('data');
$identity = collect($list['documents'])->firstWhere('type', 'identity');
// 4. Mint a disposable copy for THIS payment only
$useResp = Http::withHeaders($authHeaders)
->post("https://api.nexpay.com.au/v2/students/6a20162451c558203a879c5f/documents/{$identity['legacyDocumentId']}/use");
$copy = $useResp->json('data');
// 5. Submit the disposable id on the payment intent
$intent = createPaymentIntent([
// ...
'instructions' => [
'manualPayment' => [
// ...
'payerIdentityDocumentId' => $copy['documentId'], // disposable, single-use
],
],
]);
What can be saved
The type field on the save endpoint accepts identity — identity documents (passport, driver's licence, etc.) — and is the most common case. Purpose-proof documents (enrolment letters, invoices) are not saved for reuse because they vary per payment.
The same endpoints exist on saved payers — POST /v2/payers/{_id}/documents, /use, etc. — with identical semantics.
Inline details — when you don't need persistence
For one-off flows where the payer never recurs, skip the POST /v2/students step entirely and pass payer.details inline:
{
// ... useCase, etc.
payer: {
role: 'family',
details: {
payerType: 'parent',
firstName: 'Jane',
lastName: 'Doe',
email: 'jane.doe@example.com',
phone: '+61400000000',
dob: '1972-09-30',
addressLine1: '123 Main Street',
city: 'Sydney',
state: 'NSW',
postcode: '2000',
countryCode: 'AU',
},
},
subject: {
role: 'student',
details: {
firstName: 'Ada',
lastName: 'Lovelace',
email: 'ada@example.com',
},
},
recipients: [/* ... */],
}
The fields on payer.details and subject.details are the same fields the POST /v2/students / POST /v2/payers endpoints accept. The intent stores them verbatim and never looks them up again.
For the identity document, you still need to upload it via POST /v2/documents and pass the resulting documentId to instructions.manualPayment.payerIdentityDocumentId. There is no reuse path without a saved party.
Things to know
- A student's
_idand a payer's_idare both Mongo ObjectIds (24 hex chars). Both are valid values forpartyIdon payment intents. - The
partyIdfield doesn't tell Core which collection to look in — Core resolves the id against students and payers in turn. - Saved-document lists do not include the document content — they're metadata only. Use
GET /v2/students/{_id}/documents/{legacyDocumentId}/contentto download the original. - Disposable copies minted via
/useexpire if not submitted promptly. Mint right before you submit the intent. - Payment links (
deliveryMode: reusable_payment_link) skip parties entirely on create — the payer's details and identity document are collected by the hosted page and recorded on the child payment intent.
Next steps
- Manual payment with splits — uses saved students + saved-document reuse in the worked example.
- Payment links — the alternative when you don't know the payer in advance.
- Uploading documents — the underlying upload, download, and combine endpoints.
- Creating a payer — full payer-resource reference including business payers.
- Querying data — filter and pagination for the student and payer list endpoints.