REST API

# Payment Links API

Create, list, and manage shareable payment links programmatically. A payment link is a hosted URL where customers pay by card — create one once and reuse it, or issue a fresh link per invoice.

## When to use payment links

Reach for payment links when you want a shareable URL you can create once and reuse across many payments, collect payment manually (WhatsApp, email, SMS, QR code) outside an automated checkout flow, or let non-developers manage links from the dashboard. Each billing cycle is a fresh link.

Reach for [checkout sessions](https://ma.manza.dev/docs/api/checkout-sessions) instead when you need a per-order page tied to a specific cart, with `metadata` round-tripping and a post-payment redirect.

> **Feature-flagged:** Payment links require the `release_payment_links` feature flag and are currently available in Morocco and South Africa.

## Required scopes

| Endpoint | Scope |
| --- | --- |
| List / Get payment links | `payment_links:read` |
| Create / Cancel payment links | `payment_links:write` |
| Create payment link from invoice | `payment_links:write` |

## Payment link object

The embedded `account` object is a compact projection (no balance or status). You are viewing the **MAD** deployment, so a payment link looks like this:

```json
{
  "id": "01964a3b-7c8d-7000-8000-deadbeef1234",
  "slug": "a1B2c3D4e5F6",
  "status": "active",
  "link_type": "single",
  "amount": "1500.00",
  "currency_code": "MAD",
  "description": "March consulting invoice",
  "payment_reference": "INV-000042",
  "payments_count": 0,
  "max_payments": null,
  "expires_at": "2026-04-15T23:59:59Z",
  "paid_at": null,
  "settled_at": null,
  "redirect_url": "https://app.example.com/thank-you",
  "url": "https://ma.manza.dev/payment-link/a1B2c3D4e5F6",
  "invoice_id": null,
  "account": {
    "id": "01964a3b-0000-7000-8000-ac6000000a01",
    "name": "Operations",
    "currency_code": "MAD",
    "rib": "007780000123456789012345"
  },
  "created_at": "2026-03-15T09:00:00Z",
  "updated_at": "2026-03-15T09:00:00Z"
}
```

> **Effective status:** The `status` field reflects effective status: a link may be `active` in the database but shown as `expired` once `expires_at` has passed or `payments_count >= max_payments`. A paid single-use link reads `clearing` until the provider settles the payment, then `paid` with `settled_at` set (a `payment_link.payment_settled` webhook fires at that moment).

## List payment links

`GET` `/api/payment_links`

Filter by `status` and `link_type`, with cursor pagination (`limit` 1–100, default 25).

cURL

CLI

JavaScript

Ruby

Python

Go

Rust

Elixir

Crystal

PHP

```console
curl "https://ma.manza.dev/api/payment_links?status=active" \
  -H "Authorization: Bearer sk_live_..."
```

```console
manza payment-links list --status active
```

```javascript
import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });

await manza.paymentLinks.list({ status: "active" });
```

```ruby
require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])

manza.payment_links.list(status: "active")
```

```python
from manza import Manza
client = Manza()  # reads MANZA_API_KEY

client.payment_links.list(status="active")
```

```go
import manza "github.com/getmanza/manza-go"

client, _ := manza.New(manza.WithAPIKey(os.Getenv("MANZA_API_KEY")))

page, err := client.PaymentLinks.List(ctx, manza.PaymentLinkListParams{Status: "active"})
```

```rust
let client = manza::Client::builder()
    .api_key(std::env::var("MANZA_API_KEY")?)
    .build()?;

let params = manza::PaymentLinkListParams { status: Some("active".into()), ..Default::default() };
let page = client.payment_links().list(params)?;
```

```elixir
{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))

{:ok, page} = Manza.PaymentLinks.list(client, status: "active")
```

```crystal
require "manza"

client = Manza::Client.new # reads MANZA_API_KEY

page = client.payment_links.list(status: "active")
```

```php
use Manza\Client;

$client = new Client(apiKey: getenv('MANZA_API_KEY'));

$page = $client->paymentLinks->list(status: 'active');
```

```json
{
  "data": [
    {
      "id": "01964a3b-7c8d-7000-8000-deadbeef1234",
      "slug": "a1B2c3D4e5F6",
      "status": "active",
      "link_type": "single",
      "amount": "1500.00",
      "currency_code": "MAD",
      "description": "March consulting invoice",
      "payment_reference": "INV-000042",
      "payments_count": 0,
      "max_payments": null,
      "expires_at": "2026-04-15T23:59:59Z",
      "paid_at": null,
      "settled_at": null,
      "redirect_url": "https://app.example.com/thank-you",
      "url": "https://ma.manza.dev/payment-link/a1B2c3D4e5F6",
      "invoice_id": null,
      "account": {
        "id": "01964a3b-0000-7000-8000-ac6000000a01",
        "name": "Operations",
        "currency_code": "MAD",
        "rib": "007780000123456789012345"
      },
      "created_at": "2026-03-15T09:00:00Z",
      "updated_at": "2026-03-15T09:00:00Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJpZCI6IjAxOTY0..."
}
```

## Create payment link

`POST` `/api/payment_links`

Required fields: `account_id` and `amount`. Discover valid account IDs via [GET /api/accounts](https://ma.manza.dev/docs/api/accounts). The slug, currency, and URL are generated for you; `link_type` defaults to `single`.

> **Amount limits:** Payment links must be for at least **10 MAD**. Amounts below this are rejected at creation. There is no API-side maximum — per-payer single-transaction and daily caps driven by the payer's KYC tier are enforced when the card is charged.

cURL

CLI

JavaScript

Ruby

Python

Go

Rust

Elixir

Crystal

PHP

```console
curl -X POST "https://ma.manza.dev/api/payment_links" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "01964a3b-0000-7000-8000-ac6000000a01",
    "amount": "1500.00",
    "description": "March consulting invoice",
    "payment_reference": "INV-000042",
    "link_type": "single"
  }'
```

```console
manza payment-links create \
  --account-id 01964a3b-0000-7000-8000-ac6000000a01 \
  --amount 1500.00 \
  --description "March consulting invoice"
```

```javascript
import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });

await manza.paymentLinks.create({
  account_id: "01964a3b-0000-7000-8000-ac6000000a01",
  amount: "1500.00",
  description: "March consulting invoice",
  payment_reference: "INV-000042",
  link_type: "single",
});
```

```ruby
require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])

manza.payment_links.create(
  account_id: "01964a3b-0000-7000-8000-ac6000000a01",
  amount: "1500.00",
  description: "March consulting invoice",
  payment_reference: "INV-000042",
  link_type: "single"
)
```

```python
from manza import Manza
client = Manza()  # reads MANZA_API_KEY

client.payment_links.create(
    account_id="01964a3b-0000-7000-8000-ac6000000a01",
    amount="1500.00",
    description="March consulting invoice",
    payment_reference="INV-000042",
    link_type="single",
)
```

```go
import manza "github.com/getmanza/manza-go"

client, _ := manza.New(manza.WithAPIKey(os.Getenv("MANZA_API_KEY")))

link, err := client.PaymentLinks.Create(ctx, manza.Attributes{
    "account_id":        "01964a3b-0000-7000-8000-ac6000000a01",
    "amount":            "1500.00",
    "description":       "March consulting invoice",
    "payment_reference": "INV-000042",
    "link_type":         "single",
})
```

```rust
let client = manza::Client::builder()
    .api_key(std::env::var("MANZA_API_KEY")?)
    .build()?;

let link = client.payment_links().create(&serde_json::json!({
    "account_id": "01964a3b-0000-7000-8000-ac6000000a01",
    "amount": "1500.00",
    "description": "March consulting invoice",
    "payment_reference": "INV-000042",
    "link_type": "single",
}))?;
```

```elixir
{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))

{:ok, link} =
  Manza.PaymentLinks.create(client, %{
    "account_id" => "01964a3b-0000-7000-8000-ac6000000a01",
    "amount" => "1500.00",
    "description" => "March consulting invoice",
    "payment_reference" => "INV-000042",
    "link_type" => "single"
  })
```

```crystal
require "manza"

client = Manza::Client.new # reads MANZA_API_KEY

link = client.payment_links.create(
  account_id: "01964a3b-0000-7000-8000-ac6000000a01",
  amount: "1500.00",
  description: "March consulting invoice",
  payment_reference: "INV-000042",
  link_type: "single"
)
```

```php
use Manza\Client;

$client = new Client(apiKey: getenv('MANZA_API_KEY'));

$link = $client->paymentLinks->create([
    'account_id' => '01964a3b-0000-7000-8000-ac6000000a01',
    'amount' => '1500.00',
    'description' => 'March consulting invoice',
    'payment_reference' => 'INV-000042',
    'link_type' => 'single',
]);
```

```json
{
  "id": "01964a3b-7c8d-7000-8000-deadbeef1234",
  "slug": "a1B2c3D4e5F6",
  "status": "active",
  "link_type": "single",
  "amount": "1500.00",
  "currency_code": "MAD",
  "description": "March consulting invoice",
  "payment_reference": "INV-000042",
  "payments_count": 0,
  "max_payments": null,
  "expires_at": "2026-04-15T23:59:59Z",
  "paid_at": null,
  "settled_at": null,
  "redirect_url": "https://app.example.com/thank-you",
  "url": "https://ma.manza.dev/payment-link/a1B2c3D4e5F6",
  "invoice_id": null,
  "account": {
    "id": "01964a3b-0000-7000-8000-ac6000000a01",
    "name": "Operations",
    "currency_code": "MAD",
    "rib": "007780000123456789012345"
  },
  "created_at": "2026-03-15T09:00:00Z",
  "updated_at": "2026-03-15T09:00:00Z"
}
```

## Get payment link

`GET` `/api/payment_links/:id`

Returns the full payment link object.

cURL

CLI

JavaScript

Ruby

Python

Go

Rust

Elixir

Crystal

PHP

```console
curl "https://ma.manza.dev/api/payment_links/01964a3b-7c8d-7000-8000-deadbeef1234" \
  -H "Authorization: Bearer sk_live_..."
```

```console
manza payment-links get 01964a3b-7c8d-7000-8000-deadbeef1234
```

```javascript
import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });

await manza.paymentLinks.get("01964a3b-7c8d-7000-8000-deadbeef1234");
```

```ruby
require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])

manza.payment_links.get("01964a3b-7c8d-7000-8000-deadbeef1234")
```

```python
from manza import Manza
client = Manza()  # reads MANZA_API_KEY

client.payment_links.get("01964a3b-7c8d-7000-8000-deadbeef1234")
```

```go
import manza "github.com/getmanza/manza-go"

client, _ := manza.New(manza.WithAPIKey(os.Getenv("MANZA_API_KEY")))

link, err := client.PaymentLinks.Get(ctx, "01964a3b-7c8d-7000-8000-deadbeef1234")
```

```rust
let client = manza::Client::builder()
    .api_key(std::env::var("MANZA_API_KEY")?)
    .build()?;

let link = client.payment_links().get("01964a3b-7c8d-7000-8000-deadbeef1234")?;
```

```elixir
{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))

{:ok, link} = Manza.PaymentLinks.get(client, "01964a3b-7c8d-7000-8000-deadbeef1234")
```

```crystal
require "manza"

client = Manza::Client.new # reads MANZA_API_KEY

link = client.payment_links.get("01964a3b-7c8d-7000-8000-deadbeef1234")
```

```php
use Manza\Client;

$client = new Client(apiKey: getenv('MANZA_API_KEY'));

$link = $client->paymentLinks->get('01964a3b-7c8d-7000-8000-deadbeef1234');
```

## Cancel payment link

`POST` `/api/payment_links/:id/cancel`

Only `active` links that have not effectively expired can be cancelled. Links past `expires_at` or at `max_payments` cannot be cancelled even if their database status is still `active`. Cancelling a payment link does **not** cancel a linked invoice.

cURL

CLI

JavaScript

Ruby

Python

Go

Rust

Elixir

Crystal

PHP

```console
curl -X POST "https://ma.manza.dev/api/payment_links/01964a3b-7c8d-7000-8000-deadbeef1234/cancel" \
  -H "Authorization: Bearer sk_live_..."
```

```console
manza payment-links cancel 01964a3b-7c8d-7000-8000-deadbeef1234
```

```javascript
import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });

await manza.paymentLinks.cancel("01964a3b-7c8d-7000-8000-deadbeef1234");
```

```ruby
require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])

manza.payment_links.cancel("01964a3b-7c8d-7000-8000-deadbeef1234")
```

```python
from manza import Manza
client = Manza()  # reads MANZA_API_KEY

client.payment_links.cancel("01964a3b-7c8d-7000-8000-deadbeef1234")
```

```go
import manza "github.com/getmanza/manza-go"

client, _ := manza.New(manza.WithAPIKey(os.Getenv("MANZA_API_KEY")))

link, err := client.PaymentLinks.Cancel(ctx, "01964a3b-7c8d-7000-8000-deadbeef1234")
```

```rust
let client = manza::Client::builder()
    .api_key(std::env::var("MANZA_API_KEY")?)
    .build()?;

let link = client.payment_links().cancel("01964a3b-7c8d-7000-8000-deadbeef1234")?;
```

```elixir
{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))

{:ok, link} = Manza.PaymentLinks.cancel(client, "01964a3b-7c8d-7000-8000-deadbeef1234")
```

```crystal
require "manza"

client = Manza::Client.new # reads MANZA_API_KEY

link = client.payment_links.cancel("01964a3b-7c8d-7000-8000-deadbeef1234")
```

```php
use Manza\Client;

$client = new Client(apiKey: getenv('MANZA_API_KEY'));

$link = $client->paymentLinks->cancel('01964a3b-7c8d-7000-8000-deadbeef1234');
```

## Create payment link from invoice

`POST` `/api/invoices/:invoice_id/payment_link`

Creates a single-use link auto-populated from an invoice — amount, payment reference, description, and currency are taken from the invoice. Required: `account_id`, the destination account for funds.

> **One payment link per invoice:** An invoice can have at most one payment link at a time (422 if one already exists). To issue a fresh link — e.g. if the original expired — cancel the existing one first, then create a new one. The invoice's `payment_reference` stays the same so reconciliation is unaffected.

cURL

CLI

JavaScript

Ruby

Python

Go

Rust

Elixir

Crystal

PHP

```console
curl -X POST "https://ma.manza.dev/api/invoices/01964a3b-9f0e-7000-8000-cafe12345678/payment_link" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "account_id": "01964a3b-0000-7000-8000-ac6000000a01" }'
```

```console
manza invoices payment-link 01964a3b-9f0e-7000-8000-cafe12345678 \
  --account-id 01964a3b-0000-7000-8000-ac6000000a01
```

```javascript
import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });

await manza.invoices.createPaymentLink("01964a3b-9f0e-7000-8000-cafe12345678", {
  account_id: "01964a3b-0000-7000-8000-ac6000000a01",
});
```

```ruby
require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])

manza.invoices.create_payment_link("01964a3b-9f0e-7000-8000-cafe12345678", account_id: "01964a3b-0000-7000-8000-ac6000000a01")
```

```python
from manza import Manza
client = Manza()  # reads MANZA_API_KEY

client.invoices.create_payment_link(
    "01964a3b-9f0e-7000-8000-cafe12345678", account_id="01964a3b-0000-7000-8000-ac6000000a01"
)
```

```go
import manza "github.com/getmanza/manza-go"

client, _ := manza.New(manza.WithAPIKey(os.Getenv("MANZA_API_KEY")))

link, err := client.Invoices.CreatePaymentLink(ctx,
    "01964a3b-9f0e-7000-8000-cafe12345678", "01964a3b-0000-7000-8000-ac6000000a01")
```

```rust
let client = manza::Client::builder()
    .api_key(std::env::var("MANZA_API_KEY")?)
    .build()?;

let link = client.invoices()
    .create_payment_link("01964a3b-9f0e-7000-8000-cafe12345678", "01964a3b-0000-7000-8000-ac6000000a01")?;
```

```elixir
{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))

{:ok, link} =
  Manza.Invoices.create_payment_link(client, "01964a3b-9f0e-7000-8000-cafe12345678", "01964a3b-0000-7000-8000-ac6000000a01")
```

```crystal
require "manza"

client = Manza::Client.new # reads MANZA_API_KEY

link = client.invoices.create_payment_link(
  "01964a3b-9f0e-7000-8000-cafe12345678", "01964a3b-0000-7000-8000-ac6000000a01"
)
```

```php
use Manza\Client;

$client = new Client(apiKey: getenv('MANZA_API_KEY'));

$link = $client->invoices->createPaymentLink(
    '01964a3b-9f0e-7000-8000-cafe12345678', '01964a3b-0000-7000-8000-ac6000000a01'
);
```

## Adding payment links to your website

Payment links are **hosted pages**. Send the customer to the Manza-hosted URL and they pay there. To put one behind a button, make the button a normal link to the payment URL — the customer pays (including 3D Secure) and is sent back to your `redirect_url`. No API key and no JavaScript run in the customer's browser.

> **Do not iframe the payment page:** The hosted page sets a `frame-ancestors` policy, so browsers block it from loading inside a frame on another domain. This is deliberate: it protects against clickjacking and is required for 3D Secure. Link out to the page (same tab or new tab) instead of framing it.

Common questions when integrating a payment link into a website:

| Topic | Answer |
| --- | --- |
| **Cross-origin requests (CORS)** | Not applicable — you link to the hosted page rather than calling it from code. The API itself is server-side only and sends no CORS headers. |
| **Referrer policy** | No restriction. Visitors may arrive from any site. |
| **IP allowlisting** | None. The payment page is reachable from any IP address. |
| **Domain restrictions** | None on which site may link to a payment URL — any site can. The only limit is the frame-ancestors rule above, which stops the page being embedded in a frame on an external domain. |

## Checkout identifiers

Append query parameters to a payment link URL to identify the payer. Only these are accepted (all others are silently ignored): `client_reference_id`, `customer_email`, `customer_id`, `customer_name`. They are stored with the payment and returned as top-level fields in the `payment_link.paid` and `payment_link.payment_settled` webhooks (always present, set to `null` when not supplied). Each webhook carries the identifiers of the payment that triggered it, so a reusable link that has taken several payments still reports the right payer on every event.

> **Privacy — these are payer-modifiable:** These parameters are passed via the URL and can be modified by the payer. Safe for identifying who is paying — never use them to authorize actions on your side. Fields like `customer_email` and `customer_name` are visible in browser history, server logs, and shared links; prefer opaque identifiers. Passing email or name is opt-in PII exposure with possible compliance implications (GDPR/CCPA). To tie a payment to an order, use a dedicated link per order and the server-side `payment_reference` field.

## Returning customers to your mobile app

After a successful payment, the customer briefly sees the hosted confirmation page and is then redirected to the URL configured in `redirect_url`. The redirect is client-side: HTTPS URLs route through the browser, custom-scheme URLs route through the OS deep-link handler. If `redirect_url` is omitted, the customer stays on the hosted success page instead.

If the customer paid inside your mobile app — an in-app browser or a system browser launched from the app — there are three options for `redirect_url`, best to worst:

1. **Universal Link / App Link (recommended). **A plain HTTPS URL on your own domain that the OS routes to your installed app, falling back to the web if it isn't installed. Configure `apple-app-site-association` (iOS) and `assetlinks.json` (Android) on the same domain — one URL works on mobile and desktop, with no broken-redirect failure mode.
2. **Dispatcher page on your own domain. **An HTTPS URL that runs JavaScript on load to redirect to your app's custom scheme, falling back to the App Store or Play Store when no scheme handler fires.
3. **Custom app scheme directly. **A URL like `myapp://payment-success` that the OS hands to your installed app. Works only if the app is installed; in-app browsers (Facebook, Instagram, some mail clients) may silently swallow it with no fallback.

> **Schemes rejected for security:** The API accepts all three forms in `redirect_url`, but always rejects `http`, `javascript`, `data`, `file`, `vbscript`, `about`, and `blob` schemes. Always treat the `payment_link.paid` webhook — never the redirect arriving — as the canonical fulfillment signal: deep links can be swallowed by in-app browsers, and custom schemes fail when the app isn't installed.

## Immutability & status

Payment links are **intentionally immutable** after creation — you cannot change the amount, currency, link type, or account. For different terms, cancel the link and create a new one. This keeps an already-shared URL a consistent commitment.

The API can only **cancel** links. Marking a link `paid` happens exclusively through the card payment flow (a webhook from the payment processor), never via the API — this prevents payment fraud by requiring actual payment verification. Set `link_type: "reusable"` with `max_payments` to accept multiple payments; the link auto-expires once the count is reached.

## Recurring billing

Manza does not auto-charge stored cards. Your application schedules a fresh payment request each billing period, and the customer authorizes each payment. Follow the [Recurring billing guide](https://ma.manza.dev/docs/recurring-billing) for the full workflow, approval options, reminders and webhook reconciliation.

## Error responses

| Scenario | Status | Type | Param |
| --- | --- | --- | --- |
| Invalid account_id | 422 | `validation_error` | `account_id` |
| Amount ≤ 0 | 422 | `validation_error` | `amount` |
| Amount below 10 MAD minimum | 422 | `validation_error` | `amount` |
| Invalid redirect_url | 422 | `validation_error` | `redirect_url` |
| Cancel non-active or expired link | 422 | `state_error` | — |
| Invoice already has payment link | 422 | `validation_error` | `invoice_id` |
| expires_at in the past | 422 | `validation_error` | `expires_at` |
| Payment links not enabled | 403 | `forbidden_error` | — |