Create many payment links at once from a CSV/XLSX file or JSON rows: preview, submit, then poll the batch for each link.
Create many payment links in one go from a spreadsheet or from JSON rows.
The flow has three steps: preview (validate, nothing is created), commit
(queue a background batch), and polling the batch for each row's link.
Bulk links use the same permission and account setting as single payment
links. If payment links are disabled for your account every endpoint returns
403.
Authentication
All endpoints use your standard API bearer token:
Authorization: Bearer <token>
Base path
/api/v1/checkout/paylink/bulk
1. Templates
GET /template.csv
Header-only CSV with the supported columns.
GET /template.xlsx
Same columns as an Excel workbook. The project column carries a dropdown
of your active projects and send_email a yes/no dropdown.
Columns:
| Column | Required | Description |
|---|---|---|
email | yes | Payer email. Matched against your existing users. |
amount | yes | EUR amount. 1234.56, 1234,56, 1.234,56 and 1,234.56 are all accepted. A single separator followed by exactly three digits (1,234) is rejected as ambiguous. |
description | no | Falls back to default_description. |
project | no | Project name as shown in the template dropdown. Falls back to default_project_id. |
full_name | no | Used only when a new user is created for the email. |
send_email | no | yes / no. Falls back to default_send_email. |
Header names are matched case-insensitively. Extra columns are ignored.
Up to 500 data rows and 5 MB per file.
2. Preview
POST /preview/file
multipart/form-data with the file in file and optional form fields
default_project_id, default_description, default_send_email
(default true) and auto_create_users (default true).
POST /preview
JSON body:
{
"rows": [
{"source_row": 2, "email": "[email protected]", "amount": "1.234,56", "description": "Rent September"},
{"source_row": 3, "email": "[email protected]", "amount": 900, "project": "Calle Mayor 12", "send_email": "no"}
],
"default_project_id": "prj_...",
"default_description": "Rent September",
"default_send_email": true,
"auto_create_users": true
}Rows may give project_id (takes precedence) or project (a name). Both
preview endpoints return the same shape and create nothing:
{
"rows": [
{
"source_row": 2,
"email": "[email protected]",
"full_name": null,
"amount": "1234.56",
"currency": "EUR",
"description": "Rent September",
"project_id": "prj_...",
"project_name": "Calle Mayor 12",
"send_email": true,
"user_id": "usr_...",
"user_exists": true,
"errors": [],
"warnings": [{"code": "duplicate_row", "message": "...", "duplicate_of": 7}]
}
],
"summary": {"total": 120, "valid": 117, "with_errors": 3, "with_warnings": 8}
}Rows with errors cannot be committed. Rows with warnings can, once you
acknowledge them.
Error codes (per row, in errors[].code):
invalid_email, invalid_amount, ambiguous_amount, amount_out_of_range,
amount_too_many_decimals, description_required, description_too_long,
project_required, project_not_found, project_ambiguous, project_frozen,
full_name_too_long, invalid_send_email, user_not_found (only when
auto_create_users is false).
Warning codes (in warnings[].code): duplicate_row (same email, amount and
description as an earlier row; duplicate_of gives that row), and
user_will_be_created (no user exists for the email yet).
Whole-file problems return 422 with {"detail": {"code": ..., "message": ...}}:
unsupported_file_type, file_too_large, missing_required_columns,
too_many_rows, empty_file, unreadable_file.
3. Commit
POST /
Requires the X-Idempotency-Key header. Body is the preview JSON body plus:
| Field | Type | Required | Description |
|---|---|---|---|
default_project_id | string | yes | Project for rows without one |
acknowledge_warnings | boolean | no | Must be true when any row has warnings |
intent_expires_at | datetime | no | Hard expiry applied to every link |
source_filename | string | no | Shown in the batch list |
Responses:
202batch created (body below, statusqueued)200the idempotency key was already used; the existing batch is returned400empty idempotency key409warnings_not_acknowledged(the offending rows are indetail.rows), or
batch_already_running(one active batch per account)422row_errorswith the offending rows indetail.rows; nothing is created
Users are created for unknown emails only when the batch runs, and only when
auto_create_users is true. No verification is started for them.
4. Batches
GET /
Paginated list of batches (page, page_size), newest first.
GET /{batch_id}
{
"id": "batch_...",
"status": "processing",
"source_filename": "rent-september.xlsx",
"total_rows": 120,
"created_count": 40,
"failed_count": 1,
"canceled_count": 0,
"default_project_id": "prj_...",
"default_description": "Rent September",
"default_send_email": true,
"auto_create_users": true,
"intent_expires_at": null,
"created_by_email": "[email protected]",
"created_at": "2026-09-02T10:00:00Z",
"started_at": "2026-09-02T10:00:05Z",
"completed_at": null,
"cancel_requested_at": null,
"rows": [
{
"id": "row_...",
"sequence": 1,
"source_row": 2,
"email": "[email protected]",
"full_name": null,
"amount": "1234.56",
"currency": "EUR",
"description": "Rent September",
"project_id": "prj_...",
"project_name": "Calle Mayor 12",
"send_email": true,
"status": "created",
"attempt": 0,
"user_id": "usr_...",
"user_created": false,
"transaction_id": "txn_...",
"payment_link": "https://.../pay/txn_...",
"error_message": null,
"processed_at": "2026-09-02T10:00:06Z"
}
]
}Batch status: queued, processing, completed, partial_failed, failed,
canceled. Row status: pending, created, failed, canceled.
created_count, failed_count and canceled_count are updated when the batch
finishes; while it runs, count the rows by status.
Poll every few seconds until the batch status is terminal. Each created row
carries a payment_link and a transaction_id you can follow with the
Transactions API.
GET /{batch_id}/results.csv
The rows as CSV: source_row, email, full_name, amount, currency, description, project, send_email, status, payment_link, transaction_id, error.
POST /{batch_id}/retry
Puts the failed rows of a partial_failed or failed batch back in the
queue. Rows already created are never touched. 409 batch_not_retryable
otherwise.
POST /{batch_id}/cancel
Asks a queued or processing batch to stop. Rows not processed yet end as
canceled; links already created stay valid. 409 batch_not_cancelable
otherwise.