> For the complete documentation index, see [llms.txt](https://support.swisspay.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://support.swisspay.ai/api-reference/payments.md).

# Payments

Create, retrieve, and list payments.

`POST /api/v1/payments` is **synchronous and auto-captured** — when the response comes back, the funds are already on their way (or the card has been refused). `Idempotency-Key` is required.

## The Payment object

```json
{
  "id": "pay_01HABCXYZ...",
  "status": "succeeded",
  "amount": 2999,
  "currency": "CHF",
  "reference": "order_1001",
  "customer": "cus_01HCUSXYZ...",
  "payment_method": {
    "type": "card",
    "brand": "visa",
    "last4": "1111",
    "exp_month": 3,
    "exp_year": 2030
  },
  "next_action": null,
  "failure": null,
  "created_at": "2026-05-20T12:00:00Z"
}
```

| Field            | Type           | Notes                                                               |
| ---------------- | -------------- | ------------------------------------------------------------------- |
| `id`             | string         | Stable, prefixed with `pay_`.                                       |
| `status`         | string         | `succeeded` · `failed` · `requires_action` (3DS challenge pending). |
| `amount`         | integer        | Minor units (cents/rappen).                                         |
| `currency`       | string         | ISO 4217 (`CHF`, `EUR`, `USD`, etc.).                               |
| `reference`      | string         | Your own reference, free text.                                      |
| `customer`       | string \| null | `cus_...` ID of the attached customer, if any.                      |
| `payment_method` | object         | Brand, last 4, expiry — **never** full PAN.                         |
| `next_action`    | object \| null | Present only when `status: "requires_action"`.                      |
| `failure`        | object \| null | Present only when `status: "failed"`.                               |
| `created_at`     | string         | ISO 8601 UTC.                                                       |

***

## Create a payment

```
POST /api/v1/payments
```

### Required headers

```
Authorization: Bearer sk_test_...
Content-Type: application/json
Idempotency-Key: <unique-per-attempt>
```

### Required body fields

| Field            | Type    | Notes                     |
| ---------------- | ------- | ------------------------- |
| `amount`         | integer | Minor units. Must be > 0. |
| `currency`       | string  | ISO 4217.                 |
| `payment_method` | object  | See below.                |

### `payment_method` (card)

| Field         | Type    | Notes                                                                                                                        |
| ------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `type`        | string  | `card`                                                                                                                       |
| `number`      | string  | The card number. In test mode use a [test card](/api-reference/test-cards.md); in live mode, the shopper's real card number. |
| `exp_month`   | integer | 1-12.                                                                                                                        |
| `exp_year`    | integer | 4-digit year.                                                                                                                |
| `cvc`         | string  | 3 or 4 digits.                                                                                                               |
| `holder_name` | string  | **Required.** The name on the card.                                                                                          |

### Optional body fields

| Field                         | Type             | Notes                                                                                                                                                                                                                                                                                                                                                                                                         |
| ----------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reference`                   | string           | Your own reference. Free text.                                                                                                                                                                                                                                                                                                                                                                                |
| `customer`                    | string \| object | Either an existing `cus_...` ID or an inline customer payload.                                                                                                                                                                                                                                                                                                                                                |
| `metadata`                    | object           | Up to 20 string keys, values ≤ 500 chars. Echoed back on read.                                                                                                                                                                                                                                                                                                                                                |
| `shopper_ip`                  | string           | The shopper's IP. Improves fraud scoring.                                                                                                                                                                                                                                                                                                                                                                     |
| `browser_info`                | object           | Browser signals -- see [Browser info](#browser-info) below. For 3DS challenges, the full set is required; the two header fields (`user_agent`, `accept_header`) are enough for non-3DS fraud scoring.                                                                                                                                                                                                         |
| `browser_info_token`          | string           | Compact opaque token produced by `Swisspay.collectBrowserInfo()` (see [Browser info](#browser-info)). Lets you ship 3DS browser signals from the shopper's browser to your server in one short string, instead of passing every `browser_info` field by hand. Precedence: inline `browser_info` > `browser_info_token` > request headers.                                                                     |
| `device_fingerprint`          | string           | Device-fingerprint token from your client-side SDK. Up to 5000 characters. Forwarded to the upstream risk engine. Malformed or oversized values are silently dropped (never fail the payment).                                                                                                                                                                                                                |
| `risk_data`                   | object           | Nested risk signals (basket value, tenure, promo). Forwarded to the upstream risk engine.                                                                                                                                                                                                                                                                                                                     |
| `billing_address`             | object           | Free-text address. Forwarded to the upstream risk engine and to the processor. When a customer is also attached, a payment-level `billing_address` **overrides the customer's stored billing address for this transaction only** -- the customer record is never mutated. Malformed or incomplete addresses are silently dropped.                                                                             |
| `delivery_address`            | object           | Same shape and override semantics as `billing_address`.                                                                                                                                                                                                                                                                                                                                                       |
| `shopper_statement`           | string           | Statement descriptor shown on the shopper's bank or card statement. Length and character set are validated by the processor.                                                                                                                                                                                                                                                                                  |
| `save_payment_method`         | boolean          | Save the card to the attached `customer` so you can charge it again later without re-collecting card details. See [Saving and reusing cards](#saving-and-reusing-cards).                                                                                                                                                                                                                                      |
| `stored_payment_method`       | string           | Reference to a previously saved card on the attached `customer`. Used in place of `payment_method` to charge a saved card. See [Saving and reusing cards](#saving-and-reusing-cards).                                                                                                                                                                                                                         |
| `use_last_payment_method`     | boolean          | Charge the customer's most recently saved active card. Requires `customer`; mutually exclusive with `payment_method` and `stored_payment_method` (a request must carry exactly one payment instrument). Returns `422` if the customer has no active saved card. See [Saving and reusing cards](#saving-and-reusing-cards).                                                                                    |
| `recurring_processing_model`  | string           | One of `card_on_file` (default), `subscription`, `unscheduled_card_on_file`. Signals the reason for the recurring charge to the card networks. Resolved in this order: the explicit value on this request, the value persisted on the saved card, then `card_on_file`.                                                                                                                                        |
| `merchant_initiated`          | boolean          | `true` for an off-session merchant-initiated charge against a `stored_payment_method` (e.g. a subscription renewal, a delayed capture). When set, `success_url` / `failure_url` are not required and 3-D Secure is suppressed -- the card networks do not run a challenge when the cardholder is not present. On-session stored-card charges (the shopper is in the checkout flow) still require return URLs. |
| `authentication`              | string           | `automatic` to opt into 3-D Secure.                                                                                                                                                                                                                                                                                                                                                                           |
| `origin`                      | string           | Scheme + host (+ optional port) of the checkout page in the shopper's browser, e.g. `https://shop.example.com`. Required only when you render checkout / 3DS on your own domain and want the 3DS2 `origin` to match the browser's page origin. Must be `https` in production; no path, query, fragment, or userinfo; <= 80 chars. Omit when redirecting to SwissPay-hosted checkout.                          |
| `success_url` / `failure_url` | string           | **Required on every card payment when the connection has 3-D Secure enabled** (the default for Adyen connections, in both test and live mode) -- not only when the request sets `authentication: "automatic"`. Both must be HTTPS with no fragment.                                                                                                                                                           |

### Example — successful Visa

```bash
curl -X POST https://app.swisspay.ai/api/v1/payments \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amount": 2999,
    "currency": "CHF",
    "reference": "order_1001",
    "payment_method": {
      "type": "card",
      "number": "4111111111111111",
      "exp_month": 3,
      "exp_year": 2030,
      "cvc": "737",
      "holder_name": "Jane Doe"
    }
  }'
```

### Example — with an inline customer

The customer is created if it doesn't already exist (matched by `email` within your account) and attached to the payment.

```json
{
  "amount": 2999,
  "currency": "CHF",
  "reference": "order_1003",
  "customer": {
    "email": "new@example.com",
    "name": "New Customer",
    "external_id": "user_99"
  },
  "payment_method": { /* card */ }
}
```

### Example — with fraud signals

```json
{
  "amount": 2999,
  "currency": "CHF",
  "reference": "order_risk_1004",
  "shopper_ip": "203.0.113.42",
  "browser_info": {
    "user_agent": "Mozilla/5.0 (Macintosh) AppleWebKit/605",
    "accept_header": "text/html,application/xhtml+xml"
  },
  "risk_data": {
    "basket": { "value": 2999, "items_count": 2 },
    "customer_tenure_days": 184,
    "promo_code": "WELCOME"
  },
  "payment_method": { /* card */ }
}
```

These fields are forwarded to our upstream risk engine but **not echoed back** on the response. Confirm acceptance by checking HTTP 201 and `status: "succeeded"`.

### Browser info

The `browser_info` object on `POST /api/v1/payments` carries signals collected on the shopper's device. The full shape below is **required for any web card payment that runs through a 3-D Secure browser challenge**; smaller subsets are accepted for non-3DS fraud scoring.

```json
{
  "browser_info": {
    "user_agent": "Mozilla/5.0 ...",
    "accept_header": "text/html,application/xhtml+xml",
    "language": "en-GB",
    "color_depth": 24,
    "screen_width": 1920,
    "screen_height": 1080,
    "time_zone_offset": -60,
    "java_enabled": false,
    "java_script_enabled": true
  },
  "channel": "Web",
  "origin": "https://merchant.example"
}
```

| Field                              | Type    | Notes                                                                                                                                                                                                                |
| ---------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `browser_info.user_agent`          | string  | If omitted, we fall back to the `User-Agent` request header.                                                                                                                                                         |
| `browser_info.accept_header`       | string  | Defaults to `text/html` when omitted. (The shopper's browser cannot read its own `Accept` header, so we use a fixed default rather than the request `Accept` header.) Override inline if you need a different value. |
| `browser_info.language`            | string  | IETF language tag (`en-GB`, `de-CH`).                                                                                                                                                                                |
| `browser_info.color_depth`         | integer | Screen colour depth in bits (`24`, `32`).                                                                                                                                                                            |
| `browser_info.screen_width`        | integer | Browser screen width in pixels.                                                                                                                                                                                      |
| `browser_info.screen_height`       | integer | Browser screen height in pixels.                                                                                                                                                                                     |
| `browser_info.time_zone_offset`    | integer | `Date.getTimezoneOffset()` value, in minutes. `0` is a valid value -- pass it explicitly.                                                                                                                            |
| `browser_info.java_enabled`        | boolean | `navigator.javaEnabled()`. `false` is a valid value -- pass it explicitly.                                                                                                                                           |
| `browser_info.java_script_enabled` | boolean | Optional. Defaults to `true` when omitted on a 3DS payment.                                                                                                                                                          |
| `channel`                          | string  | Top-level. Set to `"Web"` for browser flows. Auto-emitted when `browser_info` is present.                                                                                                                            |
| `origin`                           | string  | Top-level. The browser origin, **scheme + host only, no path**. Max 80 characters. Required for any web 3DS request.                                                                                                 |

Per-field coercion is best-effort: malformed values are dropped rather than failing the payment. A partial `browser_info` will still feed fraud scoring on a non-3DS connection but will degrade a 3DS2 browser challenge.

### Example — 3-D Secure (automatic)

```json
{
  "amount": 8999,
  "currency": "EUR",
  "reference": "order_3ds_visa",
  "authentication": "automatic",
  "success_url": "https://merchant.example/checkout/return",
  "failure_url": "https://merchant.example/checkout/return",
  "payment_method": {
    "type": "card",
    "number": "4871049999999910",
    "exp_month": 3,
    "exp_year": 2030,
    "cvc": "737",
    "holder_name": "Jane Doe"
  }
}
```

Two possible outcomes:

1. **Frictionless.** HTTP 201, `status: "succeeded"`, identical to a normal sale.
2. **Challenge required.** HTTP 200, `status: "requires_action"`, plus:

   ```json
   "next_action": {
     "type": "redirect_to_url",
     "redirect_url": "https://app.swisspay.ai/checkout/3ds/3ds_..."
   }
   ```

   Redirect the customer to `next_action.redirect_url`. After they complete (or abandon) the challenge, we redirect them to `success_url` or `failure_url` with `?payment_id=pay_...` appended.

The **source of truth** for the final state is `GET /api/v1/payments/:id`.

### Collecting 3DS browser info

A 3-D Secure browser challenge needs more from the shopper's browser than the request headers alone can supply (screen size, language, color depth, time zone, etc.). The easiest way to collect those signals -- without writing the snippet yourself -- is to embed our hosted helper on your checkout page and pass the resulting `browser_info_token` to your server. See [Browser info](#browser-info) for the full field reference.

### Hosting checkout on your own domain

If you render the card form (and the 3DS challenge) on your own domain rather than redirecting to SwissPay-hosted checkout, pass that page's origin in `origin` so the 3DS2 request matches the shopper's browser origin:

```json
{
  "amount": 8999,
  "currency": "EUR",
  "authentication": "automatic",
  "origin": "https://shop.example.com",
  "success_url": "https://shop.example.com/checkout/return",
  "failure_url": "https://shop.example.com/checkout/return",
  "payment_method": { "type": "card", "...": "..." }
}
```

Omit `origin` when you redirect shoppers to SwissPay-hosted checkout -- we will use our own origin.

### Saving and reusing cards

You can save a card to a customer on the first charge, then charge it again later without re-collecting card details -- useful for subscription renewals, top-ups, "buy again" flows, or any kind of merchant-initiated recurring charge.

**1. Save the card on the first charge.** Attach a `customer`, pass `save_payment_method: true`, and submit `payment_method` as you would for any card payment:

```json
{
  "amount": 2999,
  "currency": "CHF",
  "customer": "cus_01HCUSXYZ...",
  "save_payment_method": true,
  "payment_method": {
    "type": "card",
    "number": "4111111111111111",
    "exp_month": 3,
    "exp_year": 2030,
    "cvc": "737",
    "holder_name": "Jane Doe"
  }
}
```

A customer is required when `save_payment_method: true` -- a saved card is always anchored to a customer.

**2. Charge the saved card later.** On subsequent payments, omit `payment_method` and pass `stored_payment_method` with the reference to the saved card on the customer, plus the `recurring_processing_model` that matches the reason for the charge:

```json
{
  "amount": 2999,
  "currency": "CHF",
  "customer": "cus_01HCUSXYZ...",
  "stored_payment_method": "<saved-card-reference>",
  "recurring_processing_model": "subscription"
}
```

Use:

* `subscription` for fixed-interval recurring billing.
* `unscheduled_card_on_file` for variable-amount or variable-interval recurring charges (top-ups, "save card for later" reuse).
* `card_on_file` (the default) for everything else, including the very first save.

**3. Run merchant-initiated (off-session) charges with `merchant_initiated: true`.** Off-session means the shopper is not in the checkout flow -- you are charging them from a background job, a cron, a webhook handler, etc. In that mode 3-D Secure cannot run (the cardholder is not there to challenge), so we skip it and you do not need to supply `success_url` / `failure_url`:

```json
{
  "amount": 2999,
  "currency": "CHF",
  "customer": "cus_01HCUSXYZ...",
  "stored_payment_method": "<saved-card-reference>",
  "recurring_processing_model": "subscription",
  "merchant_initiated": true
}
```

If the shopper **is** present in the checkout flow when you charge a saved card (e.g. one-click reuse on your "saved cards" UI), leave `merchant_initiated` off and keep `success_url` / `failure_url` -- a 3DS challenge may still trigger and we will return a `requires_action` redirect just like on a regular card payment.

#### Charging the customer's last saved card

If you do not want to track each saved card's reference yourself, pass `use_last_payment_method: true` with an attached `customer` and we will pick the customer's most recently saved active card for you:

```json
{
  "amount": 2999,
  "currency": "CHF",
  "customer": "cus_01HCUSXYZ...",
  "use_last_payment_method": true,
  "recurring_processing_model": "subscription",
  "merchant_initiated": true
}
```

The resolved card inherits its stored `recurring_processing_model`; pass an explicit value on the request to override. `use_last_payment_method` is mutually exclusive with `payment_method` and `stored_payment_method` -- a request must carry exactly one payment instrument. If the customer has no active saved card, the request fails with `422`.

#### Saving cards by default

If you want every payment with an attached customer to save its card without setting `save_payment_method` per request, flip the **Save cards by default** toggle on the processor connection in **Settings -> Connected providers**. When the toggle is on, payments that omit `save_payment_method` will save the card automatically; payments that set `save_payment_method: false` still opt out per-request.

### Validation rules for return URLs

* `success_url` and `failure_url` are required on **every** card payment whenever the merchant's connection has 3-D Secure enabled — including the frictionless happy path and payments that never set `authentication: "automatic"`. Adyen connections ship with 3DS enabled in both test and live mode, so treat these URLs as required unless you have explicitly disabled 3DS on the connection.
* Both must be HTTPS. No URL fragment (`#...`) allowed.
* `payment_method.holder_name` is required on every card payment.

### Configuring 3DS

3DS is enabled per processor connection from **Settings → Connected providers** in the dashboard.

***

## Retrieve a payment

```
GET /api/v1/payments/{id}
```

Returns the same Payment object as `POST /payments`.

```bash
curl https://app.swisspay.ai/api/v1/payments/pay_01HABCXYZ \
  -H "Authorization: Bearer sk_test_..."
```

Unknown IDs — including IDs that belong to another merchant — return `404 Not Found` with no body.

***

## List payments

```
GET /api/v1/payments?page=1&per_page=20
```

| Query param | Default | Max                                                                                                                        |
| ----------- | ------- | -------------------------------------------------------------------------------------------------------------------------- |
| `page`      | `1`     | —                                                                                                                          |
| `per_page`  | `20`    | `100`                                                                                                                      |
| `customer`  | —       | A `cus_...` ID. Returns only payments attached to that customer. Unknown IDs return an empty list (no cross-account leak). |

### Example — payments for one customer

```bash
curl 'https://app.swisspay.ai/api/v1/payments?customer=cus_01HCUSXYZ' \
  -H "Authorization: Bearer sk_test_..."
```

Response:

```json
{
  "data": [ /* payment objects */ ],
  "page": 1,
  "per_page": 20,
  "total_count": 156,
  "has_more": true
}
```

***

## See also

* [Idempotency](/api-reference/idempotency.md) — pick the right `Idempotency-Key`.
* [Errors](/api-reference/errors.md) — every error code we return.
* [Test cards](/api-reference/test-cards.md) — test-mode card numbers.
