Bulk Payment Links

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:

ColumnRequiredDescription
emailyesPayer email. Matched against your existing users.
amountyesEUR 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.
descriptionnoFalls back to default_description.
projectnoProject name as shown in the template dropdown. Falls back to default_project_id.
full_namenoUsed only when a new user is created for the email.
send_emailnoyes / 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:

FieldTypeRequiredDescription
default_project_idstringyesProject for rows without one
acknowledge_warningsbooleannoMust be true when any row has warnings
intent_expires_atdatetimenoHard expiry applied to every link
source_filenamestringnoShown in the batch list

Responses:

  • 202 batch created (body below, status queued)
  • 200 the idempotency key was already used; the existing batch is returned
  • 400 empty idempotency key
  • 409 warnings_not_acknowledged (the offending rows are in detail.rows), or
    batch_already_running (one active batch per account)
  • 422 row_errors with the offending rows in detail.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.