# Parties — students, payers, and inline details

> When to save a student or payer, when to send details inline, and how to reuse identity documents across payments.

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:

1. **Inline details** — pass `payer.details` (and optionally `subject.details`) in the payment-intent body. Nothing is persisted across payments.
2. **Saved student** — `POST /v2/students` once, then reference by `partyId` on every payment for that student. Best for tuition, refunds, or any flow where the same student pays repeatedly.
3. **Saved payer** — `POST /v2/payers` once, then reference by `partyId`. 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.

> **Note — 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](/docs/errors.md).

---

## 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**

```bash
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"
  }'
```

**JavaScript**

```javascript
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"
```

**PHP (Laravel)**

```php
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**

```bash
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'
```

**JavaScript**

```javascript
// 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
```

**PHP (Laravel)**

```php
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](/docs/queries.md) for filter and pagination syntax.

### Reference a student on a payment intent

```javascript
{
  // ... 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**

```bash
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"
    }
  }'
```

**JavaScript**

```javascript
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);
```

**PHP (Laravel)**

```php
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](/docs/guides/creating-payers.md) 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 |
| --- | --- | --- | --- |
| 1. Upload the raw document | `POST /v2/documents` | `documentId` (response: `{ data: { documentId } }`) | Sent as `sourceDocumentId` in step 2. |
| 2. Save against the student | `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. |
| 3. List or look up the saved doc | `GET /v2/students/{_id}/documents` | `documents[].legacyDocumentId` | Used as the path id in step 4. |
| 4. Mint a disposable copy for one payment | `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

**cURL**

```bash
# 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" } }
```

**JavaScript**

```javascript
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
```

**PHP (Laravel)**

```php
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:

**cURL**

```bash
# 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
```

**JavaScript**

```javascript
// 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
    },
  },
});
```

**PHP (Laravel)**

```php
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:

```javascript
{
  // ... 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 `_id` and a payer's `_id` are both Mongo ObjectIds (24 hex chars). Both are valid values for `partyId` on payment intents.
- The `partyId` field 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}/content` to download the original.
- Disposable copies minted via `/use` expire 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](/docs/guides/payment-intents-manual.md) — uses saved students + saved-document reuse in the worked example.
- [Payment links](/docs/guides/payment-links.md) — the alternative when you don't know the payer in advance.
- [Uploading documents](/docs/guides/uploading-documents.md) — the underlying upload, download, and combine endpoints.
- [Creating a payer](/docs/guides/creating-payers.md) — full payer-resource reference including business payers.
- [Querying data](/docs/queries.md) — filter and pagination for the student and payer list endpoints.
