# Quick start

> End-to-end guide to making your first cross-border payment with the Nexpay API.

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.

> **Note — 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](/docs/guides/payment-links.md) instead.

---

## Overview

Every payment in Nexpay is a **payment intent** — one typed object that handles every scenario. A typical manual flow follows these steps:

1. **Create a payee** — the recipient of funds.
2. **Get a quote** — check the exchange rate and fees.
3. **Create a student** — the payer and subject of the payment.
4. **Upload the identity document** — for compliance.
5. **Create the payment intent** — describe the payer, recipient, and amount.
6. **Dry-run the intent** — confirm it would succeed before moving money.
7. **Submit the intent** — execute the payment.
8. **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](/docs/authentication.md). If you haven't created a key yet, follow the [API keys guide](/docs/api-keys.md).

Every example below uses this header:

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

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

**JavaScript**

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

**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/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](/docs/guides/creating-payees.md)

---

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

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

**JavaScript**

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

**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/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:

```javascript
// 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](/docs/guides/getting-quotes.md)

---

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

```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": "John",
    "lastName": "Doe",
    "email": "john.doe@example.com",
    "countryCode": "AU"
  }'
```

**JavaScript**

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

**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' => '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](/docs/guides/parties.md) 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**

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

**JavaScript**

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

**PHP (Laravel)**

```php
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](/docs/guides/uploading-documents.md)

---

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

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

**JavaScript**

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

**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/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](/docs/guides/payment-intents-manual.md) 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**

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

**JavaScript**

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

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

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

**JavaScript**

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

**PHP (Laravel)**

```php
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'] ?? []);
```

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

---

## Step 9: Track the status

Poll the intent until it reaches a terminal status:

**cURL**

```bash
curl https://api.nexpay.com.au/v2/payment-intents/665f1a2b3c4d5e6f7a8b9c2f \
  -H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret'
```

**JavaScript**

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

**PHP (Laravel)**

```php
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](/docs/status-lifecycle.md) for the full state machine and a ready-made polling loop.

---

## Complete example

Here's the entire flow in one script:

```javascript
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](/docs/guides/payment-intents-manual.md) for the full payment-intent model — splits, refunds, payroll, and every scenario field.
* Learn the [status lifecycle](/docs/status-lifecycle.md) to handle every stage of the payment.
* Set up [error handling](/docs/errors.md) to gracefully manage failures.
* Use [lookup data](/docs/guides/lookup-data.md) to build dynamic payment forms with the correct countries, purposes, and payer types.
* Track [commissions](/docs/guides/commissions.md) earned on your payments.
