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 instead when you need a per-order page tied to a specific cart, with metadata round-tripping and a post-payment redirect.
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:
{
"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"
}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 "https://ma.manza.dev/api/payment_links?status=active" \
-H "Authorization: Bearer sk_live_..."manza payment-links list --status activeimport { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });
await manza.paymentLinks.list({ status: "active" });require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])
manza.payment_links.list(status: "active")from manza import Manza
client = Manza() # reads MANZA_API_KEY
client.payment_links.list(status="active")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"})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)?;{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))
{:ok, page} = Manza.PaymentLinks.list(client, status: "active")require "manza"
client = Manza::Client.new # reads MANZA_API_KEY
page = client.payment_links.list(status: "active")use Manza\Client;
$client = new Client(apiKey: getenv('MANZA_API_KEY'));
$page = $client->paymentLinks->list(status: 'active');{
"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. The slug, currency, and URL are generated for you; link_type defaults to single.
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"
}'manza payment-links create \
--account-id 01964a3b-0000-7000-8000-ac6000000a01 \
--amount 1500.00 \
--description "March consulting invoice"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",
});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"
)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",
)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",
})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",
}))?;{: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"
})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"
)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',
]);{
"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 "https://ma.manza.dev/api/payment_links/01964a3b-7c8d-7000-8000-deadbeef1234" \
-H "Authorization: Bearer sk_live_..."manza payment-links get 01964a3b-7c8d-7000-8000-deadbeef1234import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });
await manza.paymentLinks.get("01964a3b-7c8d-7000-8000-deadbeef1234");require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])
manza.payment_links.get("01964a3b-7c8d-7000-8000-deadbeef1234")from manza import Manza
client = Manza() # reads MANZA_API_KEY
client.payment_links.get("01964a3b-7c8d-7000-8000-deadbeef1234")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")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")?;{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))
{:ok, link} = Manza.PaymentLinks.get(client, "01964a3b-7c8d-7000-8000-deadbeef1234")require "manza"
client = Manza::Client.new # reads MANZA_API_KEY
link = client.payment_links.get("01964a3b-7c8d-7000-8000-deadbeef1234")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 -X POST "https://ma.manza.dev/api/payment_links/01964a3b-7c8d-7000-8000-deadbeef1234/cancel" \
-H "Authorization: Bearer sk_live_..."manza payment-links cancel 01964a3b-7c8d-7000-8000-deadbeef1234import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });
await manza.paymentLinks.cancel("01964a3b-7c8d-7000-8000-deadbeef1234");require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])
manza.payment_links.cancel("01964a3b-7c8d-7000-8000-deadbeef1234")from manza import Manza
client = Manza() # reads MANZA_API_KEY
client.payment_links.cancel("01964a3b-7c8d-7000-8000-deadbeef1234")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")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")?;{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))
{:ok, link} = Manza.PaymentLinks.cancel(client, "01964a3b-7c8d-7000-8000-deadbeef1234")require "manza"
client = Manza::Client.new # reads MANZA_API_KEY
link = client.payment_links.cancel("01964a3b-7c8d-7000-8000-deadbeef1234")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.
payment_reference stays the same so reconciliation is unaffected.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" }'manza invoices payment-link 01964a3b-9f0e-7000-8000-cafe12345678 \
--account-id 01964a3b-0000-7000-8000-ac6000000a01import { 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",
});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")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"
)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")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")?;{: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")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"
)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.
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.
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:
- 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) andassetlinks.json(Android) on the same domain — one URL works on mobile and desktop, with no broken-redirect failure mode. - 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.
- Custom app scheme directly. A URL like
myapp://payment-successthat 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.
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 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 | — |