# Payment links

> Create reusable, shareable payment links so a payer can complete a payment without operator input — including split links where the tenant takes a portion.

A **reusable payment link** is a payment intent that produces a shareable URL. You configure who receives the money and how much; the payer opens the link, enters their details, picks a payment method, and pays. The same URL can be opened by many payers — Core mints a child submission for each one and reconciles it asynchronously.

Use payment links when you don't want the operator to drive the payment — for example, a tuition page on your website, a self-service invoice for a partner, or a split fundraiser where each payer contributes whatever amount they choose.

> **Note — Terminology in this guide**
>
> - **Tenant** — your own organization (the Nexpay customer whose API key is making the call).
> - **Payee** — a saved recipient of money (a university, your tenant, a supplier).
> - **Payer** — the person or entity paying through the link.
> - **Connector** — the underlying payment rail that executes the payment.
> - **Child submission** — the payment intent created automatically each time a payer completes the link.

---

## Before you start

You will need:

- A valid [API key](/docs/api-keys.md).
- A recipient — usually an education provider via `connectorPayeeId`, or your own tenant for a service invoice. See [Creating a payee](/docs/guides/creating-payees.md).
- For split links, your tenant's `connectorPayeeId`. Find it in the Nexpay Dashboard under **Settings → Organization**. Sandbox and production have different ids; the value `14001` in examples below is illustrative.

You do **not** need a quote, a student, or a payer document when creating the link — those are collected from the payer at submission time.

---

## When to use which link type

Three combinations cover almost every real-world payment link. Pick by what the payer should see.

| Scenario | `useCase` | `amountMode` | What the payer sees |
| --- | --- | --- | --- |
| Student pays a single provider | `education_provider_tuition` | `fixed_payee_amounts` | A fixed amount they can't change. |
| Student pays one link, provider + tenant split | `education_provider_tuition_with_tenant_split` | `free_entry_with_portions` | An open total; the split is invisible to the payer. |
| Partner pays the tenant (service invoice / commission) | `generic_service_invoice` or `owed_commission_invoice` | `fixed_payee_amounts` | A fixed invoice amount. |

All payment links use `deliveryMode: "reusable_payment_link"` and `payer.role: "unknown_link_payer"` — the payer is not known at create time. Each completed payment then produces a **child** payment intent with `deliveryMode: "payment_link_submission"` where the actual payer's details are stored (see [Discovering child submissions](#discovering-child-submissions)).

---

## Creating a fixed-amount link

Use this when the amount is set by you, not the payer. The payer sees a single number and a Pay button.

**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": "reusable_payment_link",
    "amountMode": "fixed_payee_amounts",
    "payer": {
      "role": "unknown_link_payer"
    },
    "recipients": [
      {
        "role": "education_provider",
        "order": 1,
        "connectorPayeeId": 123,
        "splitRole": "principal_provider_amount",
        "amount": {
          "payeeAmount": "10000.00",
          "currency": "AUD"
        }
      }
    ],
    "instructions": {
      "paymentLink": {
        "reference": "INV-2026-001",
        "includeCreatedByText": true
      }
    },
    "termsAccepted": true
  }'
```

**JavaScript**

```javascript
const headers = {
  'Content-Type': 'application/json',
  'X-API-Key': 'nxp_ck_your-client-id:nxp_sk_your-secret',
};

const { data: intent } = await fetch('https://api.nexpay.com.au/v2/payment-intents', {
  method: 'POST',
  headers,
  body: JSON.stringify({
    useCase: 'education_provider_tuition',
    deliveryMode: 'reusable_payment_link',
    amountMode: 'fixed_payee_amounts',

    payer: {
      role: 'unknown_link_payer',
    },

    recipients: [
      {
        role: 'education_provider',
        order: 1,
        connectorPayeeId: 123,
        splitRole: 'principal_provider_amount',
        amount: {
          payeeAmount: '10000.00',
          currency: 'AUD',
        },
      },
    ],

    instructions: {
      paymentLink: {
        reference: 'INV-2026-001',
        includeCreatedByText: true,
      },
    },

    termsAccepted: true,
  }),
}).then(r => r.json());

console.log('Intent ID:', intent.id);
console.log('Public link ID:', intent.publicLinkId);
```

**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' => 'reusable_payment_link',
    'amountMode' => 'fixed_payee_amounts',
    'payer' => [
        'role' => 'unknown_link_payer',
    ],
    'recipients' => [
        [
            'role' => 'education_provider',
            'order' => 1,
            'connectorPayeeId' => 123,
            'splitRole' => 'principal_provider_amount',
            'amount' => [
                'payeeAmount' => '10000.00',
                'currency' => 'AUD',
            ],
        ],
    ],
    'instructions' => [
        'paymentLink' => [
            'reference' => 'INV-2026-001',
            'includeCreatedByText' => true,
        ],
    ],
    'termsAccepted' => true,
]);

$intent = $response->json('data');

echo 'Intent ID: ' . $intent['id'] . PHP_EOL;
echo 'Public link ID: ' . $intent['publicLinkId'] . PHP_EOL;
```

After creating the intent, dry-run and submit it to publish the link:

**cURL**

```bash
# Dry-run
curl -X POST 'https://api.nexpay.com.au/v2/payment-intents/3fa85f64-5717-4562-b3fc-2c963f66afa6/dry-run' \
  -H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret'

# Submit to publish the link
curl -X POST 'https://api.nexpay.com.au/v2/payment-intents/3fa85f64-5717-4562-b3fc-2c963f66afa6/submit' \
  -H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
  -H 'Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7'
```

**JavaScript**

```javascript
await fetch(
  `https://api.nexpay.com.au/v2/payment-intents/${intent.id}/dry-run`,
  { method: 'POST', headers },
);

const { data: submitted } = await fetch(
  `https://api.nexpay.com.au/v2/payment-intents/${intent.id}/submit`,
  {
    method: 'POST',
    headers: {
      ...headers,
      'Idempotency-Key': crypto.randomUUID(),
    },
  },
).then(r => r.json());

const linkUrl = submitted.executions?.[0]?.paymentLinkUrl;
console.log('Send this URL to the payer:', linkUrl);
```

**PHP (Laravel)**

```php
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

// Dry-run
Http::withHeaders([
    'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
])->post('https://api.nexpay.com.au/v2/payment-intents/3fa85f64-5717-4562-b3fc-2c963f66afa6/dry-run');

// Submit to publish the link
$response = Http::withHeaders([
    'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
    'Idempotency-Key' => (string) Str::uuid(),
])->post('https://api.nexpay.com.au/v2/payment-intents/3fa85f64-5717-4562-b3fc-2c963f66afa6/submit');

$submitted = $response->json('data');

$linkUrl = $submitted['executions'][0]['paymentLinkUrl'] ?? null;
echo 'Send this URL to the payer: ' . $linkUrl . PHP_EOL;
```

After submit, the parent intent transitions to `pending_link_payment` — it stays in that state for the lifetime of the link, accumulating child submissions as payers complete payments.

Two URLs come out of this flow:

- `submitted.executions[0].paymentLinkUrl` — the hosted Nexpay URL, in the form `https://app.nexpay.com.au/pay/plink_{publicLinkId}` (sandbox uses `sandbox-app.nexpay.com.au`). Send this to payers as-is if you don't host your own page.
- `intent.publicLinkId` — the opaque identifier for the link. It is available immediately on create (you don't have to wait for submit) and it's the same value as `{token}` in the public payer endpoints (`/v2/public/payment-links/{token}`). Use it as your internal correlation id, or build a vanity URL on your own domain (for example `https://pay.yoursite.com/checkout/{publicLinkId}`) that redirects to `paymentLinkUrl`.

`publicLinkId` does not encode payment data — it's just a lookup key.

---

## Creating a split link with portions

When the payer enters their own total but you want a fixed percentage split between two recipients, use `amountMode: "free_entry_with_portions"`. Each recipient gets a `portionBps` (basis points) instead of an absolute amount.

`portionBps` is an integer share out of `10000`:

- `7000` → 70%
- `3000` → 30%
- All recipient portions **must sum to 10000**.

**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_with_tenant_split",
    "deliveryMode": "reusable_payment_link",
    "amountMode": "free_entry_with_portions",
    "payer": {
      "role": "unknown_link_payer"
    },
    "recipients": [
      {
        "role": "education_provider",
        "order": 1,
        "connectorPayeeId": 123,
        "splitRole": "principal_provider_amount",
        "amount": {
          "portionBps": 7000,
          "currency": "AUD"
        }
      },
      {
        "role": "tenant",
        "order": 2,
        "connectorPayeeId": 14001,
        "splitRole": "tenant_commission",
        "amount": {
          "portionBps": 3000,
          "currency": "AUD"
        }
      }
    ],
    "instructions": {
      "paymentLink": {
        "displayCurrency": "AUD",
        "includeCreatedByText": true
      }
    },
    "termsAccepted": true
  }'
```

**JavaScript**

```javascript
const { data: intent } = await fetch('https://api.nexpay.com.au/v2/payment-intents', {
  method: 'POST',
  headers,
  body: JSON.stringify({
    useCase: 'education_provider_tuition_with_tenant_split',
    deliveryMode: 'reusable_payment_link',
    amountMode: 'free_entry_with_portions',

    payer: {
      role: 'unknown_link_payer',
    },

    recipients: [
      {
        role: 'education_provider',
        order: 1,
        connectorPayeeId: 123,
        splitRole: 'principal_provider_amount',
        amount: {
          portionBps: 7000, // 70%
          currency: 'AUD',  // required for the link's display currency
        },
      },
      {
        role: 'tenant',
        order: 2,
        connectorPayeeId: 14001, // your tenant payee id
        splitRole: 'tenant_commission',
        amount: {
          portionBps: 3000, // 30%
          currency: 'AUD',
        },
      },
    ],

    instructions: {
      paymentLink: {
        displayCurrency: 'AUD',     // required for split links
        includeCreatedByText: true,
      },
    },

    termsAccepted: true,
  }),
}).then(r => r.json());
```

**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_with_tenant_split',
    'deliveryMode' => 'reusable_payment_link',
    'amountMode' => 'free_entry_with_portions',
    'payer' => [
        'role' => 'unknown_link_payer',
    ],
    'recipients' => [
        [
            'role' => 'education_provider',
            'order' => 1,
            'connectorPayeeId' => 123,
            'splitRole' => 'principal_provider_amount',
            'amount' => [
                'portionBps' => 7000, // 70%
                'currency' => 'AUD',  // required for the link's display currency
            ],
        ],
        [
            'role' => 'tenant',
            'order' => 2,
            'connectorPayeeId' => 14001, // your tenant payee id
            'splitRole' => 'tenant_commission',
            'amount' => [
                'portionBps' => 3000, // 30%
                'currency' => 'AUD',
            ],
        ],
    ],
    'instructions' => [
        'paymentLink' => [
            'displayCurrency' => 'AUD', // required for split links
            'includeCreatedByText' => true,
        ],
    ],
    'termsAccepted' => true,
]);

$intent = $response->json('data');
```

> **Warning — Send basis points, not percentages**
>
> `portionBps` is always in basis points — `3000` for 30%, not `30` or `0.3`. Sending `30` will be interpreted as 0.3% and rejected during dry-run because the portions don't sum to `10000`. Every recipient row must include `portionBps`; Core does not auto-fill missing shares.

---

## Display currency for split links

For reusable links with multiple recipient currencies, Core needs to know which currency to show the payer. You control this with `instructions.paymentLink.displayCurrencySource`:

| Value | Behavior |
| --- | --- |
| `explicit` | Uses `instructions.paymentLink.displayCurrency` verbatim. |
| `first_recipient_currency` | Uses `recipients[0].amount.currency` literally. Choose this only if you're migrating an older integration that depended on this exact behavior. |
| `primary_recipient_currency` | Uses the currency of whichever recipient Core resolves as the primary one. Today this is the same as `recipients[0]`, but it may resolve via `splitRole` in the future. **Prefer this for new integrations.** |

```javascript
instructions: {
  paymentLink: {
    displayCurrencySource: 'explicit',
    // Recipient-side currency. The payer always sees their local currency on the
    // hosted page (computed via live quote at submission).
    displayCurrency: 'AUD',
    reference: 'TUITION-2026',
    // Renders a "Created by [your tenant name]" line on the hosted page. Set
    // false for a white-label experience.
    includeCreatedByText: true,
  },
}
```

---

## Recipient caps

Some payment-link connectors limit the number of recipients per link (for example, PayNow split links allow up to 3). Discover the limit *before* you build the recipient list with the stateless requirements endpoint:

**cURL**

```bash
curl -X POST 'https://api.nexpay.com.au/v2/payment-intents/requirements' \
  -H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
  -H 'Content-Type: application/json' \
  -d '{
    "useCase": "education_provider_tuition_with_tenant_split",
    "deliveryMode": "reusable_payment_link",
    "amountMode": "free_entry_with_portions",
    "payer": {
      "role": "unknown_link_payer"
    }
  }'
```

**JavaScript**

```javascript
const { data: preview } = await fetch(
  'https://api.nexpay.com.au/v2/payment-intents/requirements',
  {
    method: 'POST',
    headers,
    body: JSON.stringify({
      useCase: 'education_provider_tuition_with_tenant_split',
      deliveryMode: 'reusable_payment_link',
      amountMode: 'free_entry_with_portions',
      payer: { role: 'unknown_link_payer' },
    }),
  },
).then(r => r.json());

console.log('Max recipients:', preview.limits?.maxRecipients);
console.log('Connector:', preview.connector); // { provider, action, version }
```

**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/requirements', [
    'useCase' => 'education_provider_tuition_with_tenant_split',
    'deliveryMode' => 'reusable_payment_link',
    'amountMode' => 'free_entry_with_portions',
    'payer' => ['role' => 'unknown_link_payer'],
]);

$preview = $response->json('data');

echo 'Max recipients: ' . ($preview['limits']['maxRecipients'] ?? null) . PHP_EOL;
// $preview['connector'] === [ 'provider', 'action', 'version' ]
```

`limits` is the authoritative source — don't hardcode caps client-side.

The same response also surfaces `controls.acceptedLimitations` — connector-specific constraints worth noting before you publish. For the current reusable-link connector (PayNow v1):

- Links are **reusable** (many payers per URL), not single-use.
- Links **cannot be cancelled** after they're issued. If you need to revoke a link, stop sharing the URL on your side — there is no API call that disables it.
- Completion is reconciled asynchronously via polling.
- A single shared `reference` applies to every payee on the link.

If single-use links or post-issue cancellation matter to your flow, plan around it (for example, rotate links per cohort by minting a new intent).

---

## What happens when a payer opens the link

**You probably don't need this section.** The default URL Core returns in `paymentLinkUrl` already drives the whole payer journey through Nexpay's hosted page. Read on only if you're replacing that page with your own.

Public payer endpoints live under `/v2/public/payment-links/{token}` and do **not** require authentication. `{token}` is the `publicLinkId` you got back from create/submit. The payer's browser walks them in this order:

1. **Resolve** the link → `GET /v2/public/payment-links/{token}` returns display details (recipient name, fixed or open amount, currency).
2. **Preview a quote** → `POST /v2/public/payment-links/{token}/quote` with the amount the payer entered and their `payerCountryCode`. Returns selectable variants (bank transfer, card, installments).
3. **Upload documents** if the scenario requires them → `POST /v2/public/payment-links/{token}/documents` with a file and `documentType`.
4. **Create the payer session** → `POST /v2/public/payment-links/{token}/payer-session` with `payerDetails`, the selected quote, and any document ids. Returns a checkout URL or next-action instructions.
5. **Poll status** → `GET /v2/public/payment-links/{token}/status` (optionally with `paymentIntentId` for child submissions).

You only need to call these if you're building your own hosted public page. The default link URL Core returns already drives this flow through Nexpay's payer experience.

---

## Discovering child submissions

Every payer who completes a reusable link creates a **child payment intent** with `deliveryMode: "payment_link_submission"` and `createdFrom.paymentIntentId` pointing to the original link.

> **Note — Reconciliation today is poll-based**
>
> The current reusable-link connector (PayNow v1) reconciles completion **asynchronously by polling**. List children periodically and detect new completions by `status === 'completed'`. See [Status lifecycle](/docs/status-lifecycle.md) for the production-grade polling pattern.

To enumerate children for reporting, list them by filtering:

**cURL**

```bash
curl -G 'https://api.nexpay.com.au/v2/payment-intents' \
  -H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
  --data-urlencode 'filter[deliveryMode]=payment_link_submission' \
  --data-urlencode 'filter[createdFrom.paymentIntentId]=3fa85f64-5717-4562-b3fc-2c963f66afa6'
```

**JavaScript**

```javascript
const url = new URL('https://api.nexpay.com.au/v2/payment-intents');
url.searchParams.set('filter[deliveryMode]', 'payment_link_submission');
url.searchParams.set('filter[createdFrom.paymentIntentId]', intent.id);

const { data: children } = await fetch(url, { headers }).then(r => r.json());

for (const child of children.paymentIntents) {
  console.log(
    child._id,
    child.status,
    child.lastConnectorPaymentId,
    child.lastPaymentLinkUrl,
  );
}
```

**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', [
    'filter[deliveryMode]' => 'payment_link_submission',
    'filter[createdFrom.paymentIntentId]' => '3fa85f64-5717-4562-b3fc-2c963f66afa6',
]);

$children = $response->json('data');

foreach ($children['paymentIntents'] as $child) {
    echo implode(' ', [
        $child['_id'],
        $child['status'],
        $child['lastConnectorPaymentId'],
        $child['lastPaymentLinkUrl'],
    ]) . PHP_EOL;
}
```

The summary projection (`lastConnectorPaymentId`, `lastPaymentLinkUrl`, `bulkProgress`, `lastExecutionConnector`) is precomputed by Core so list pages stay cheap.

---

## Putting it together

**cURL**

```bash
# 1. Create the split link intent
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_with_tenant_split",
    "deliveryMode": "reusable_payment_link",
    "amountMode": "free_entry_with_portions",
    "payer": { "role": "unknown_link_payer" },
    "recipients": [
      {
        "role": "education_provider",
        "order": 1,
        "connectorPayeeId": 123,
        "splitRole": "principal_provider_amount",
        "amount": { "portionBps": 7000, "currency": "AUD" }
      },
      {
        "role": "tenant",
        "order": 2,
        "connectorPayeeId": 14001,
        "splitRole": "tenant_commission",
        "amount": { "portionBps": 3000, "currency": "AUD" }
      }
    ],
    "instructions": { "paymentLink": { "displayCurrency": "AUD", "includeCreatedByText": true } },
    "termsAccepted": true
  }'

# 2. Dry-run to confirm the rule is enabled and no requirements are missing
curl -X POST 'https://api.nexpay.com.au/v2/payment-intents/3fa85f64-5717-4562-b3fc-2c963f66afa6/dry-run' \
  -H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret'

# 3. Submit to publish the link
curl -X POST 'https://api.nexpay.com.au/v2/payment-intents/3fa85f64-5717-4562-b3fc-2c963f66afa6/submit' \
  -H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
  -H 'Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7'
```

**JavaScript**

```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 the split link intent
const { data: intent } = await fetch(`${API}/payment-intents`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    useCase: 'education_provider_tuition_with_tenant_split',
    deliveryMode: 'reusable_payment_link',
    amountMode: 'free_entry_with_portions',
    payer: { role: 'unknown_link_payer' },
    recipients: [
      {
        role: 'education_provider', order: 1, connectorPayeeId: 123,
        splitRole: 'principal_provider_amount',
        amount: { portionBps: 7000, currency: 'AUD' },
      },
      {
        role: 'tenant', order: 2, connectorPayeeId: 14001,
        splitRole: 'tenant_commission',
        amount: { portionBps: 3000, currency: 'AUD' },
      },
    ],
    instructions: { paymentLink: { displayCurrency: 'AUD', includeCreatedByText: true } },
    termsAccepted: true,
  }),
}).then(r => r.json());

// 2. Dry-run to confirm the rule is enabled and no requirements are missing
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)}`);
}

// 3. Submit to publish the link
const { data: submitted } = await fetch(
  `${API}/payment-intents/${intent.id}/submit`,
  {
    method: 'POST',
    headers: { ...headers, 'Idempotency-Key': crypto.randomUUID() },
  },
).then(r => r.json());

const linkUrl = submitted.executions?.[0]?.paymentLinkUrl;
console.log('Share this URL:', linkUrl);
console.log('Public link id:', submitted.publicLinkId);
```

**PHP (Laravel)**

```php
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

$headers = ['X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret'];
$api = 'https://api.nexpay.com.au/v2';

// 1. Create the split link intent
$intent = Http::withHeaders($headers)->post("{$api}/payment-intents", [
    'useCase' => 'education_provider_tuition_with_tenant_split',
    'deliveryMode' => 'reusable_payment_link',
    'amountMode' => 'free_entry_with_portions',
    'payer' => ['role' => 'unknown_link_payer'],
    'recipients' => [
        [
            'role' => 'education_provider', 'order' => 1, 'connectorPayeeId' => 123,
            'splitRole' => 'principal_provider_amount',
            'amount' => ['portionBps' => 7000, 'currency' => 'AUD'],
        ],
        [
            'role' => 'tenant', 'order' => 2, 'connectorPayeeId' => 14001,
            'splitRole' => 'tenant_commission',
            'amount' => ['portionBps' => 3000, 'currency' => 'AUD'],
        ],
    ],
    'instructions' => ['paymentLink' => ['displayCurrency' => 'AUD', 'includeCreatedByText' => true]],
    'termsAccepted' => true,
])->json('data');

// 2. Dry-run to confirm the rule is enabled and no requirements are missing
$dryRun = Http::withHeaders($headers)
    ->post("{$api}/payment-intents/{$intent['id']}/dry-run")
    ->json('data');

if ($dryRun['rule']['status'] !== 'enabled' || count($dryRun['rule']['missingRequirements'])) {
    throw new \RuntimeException('Not ready: ' . json_encode($dryRun['rule']));
}

// 3. Submit to publish the link
$submitted = Http::withHeaders(array_merge($headers, [
    'Idempotency-Key' => (string) Str::uuid(),
]))->post("{$api}/payment-intents/{$intent['id']}/submit")->json('data');

$linkUrl = $submitted['executions'][0]['paymentLinkUrl'] ?? null;
echo 'Share this URL: ' . $linkUrl . PHP_EOL;
echo 'Public link id: ' . $submitted['publicLinkId'] . PHP_EOL;
```

---

## Next steps

- See [Manual payment with splits](/docs/guides/payment-intents-manual.md) for the operator-executed flow that uses the same payment intent shape.
- Use [Lookup data](/docs/guides/lookup-data.md) to discover supported countries and payer types for the public page.
- Track per-payer outcomes via the child payment intents listed under `filter[createdFrom.paymentIntentId]`.
- Handle errors and quote expiry — see [Errors](/docs/errors.md).
