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:

  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. 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"
  }'

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

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

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'

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

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'

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'

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'

The intent moves through these statuses as it's processed:

draftready_for_quotequotedready_for_executionexecutingcompleted

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

Previous
Rate limiting