# Payment endpoints

These endpoints are used when a booking requires online payment. This page is
the reference for each of them. [Payments](https://developers.cliento.com/docs/widget-api/payments.md) describes how they fit
together for each provider.
All of them take the `cbUuid` of a pending reservation, see
[Creating a booking](https://developers.cliento.com/docs/widget-api/booking.md).

## Choose payment provider

```
POST /booking/payment-provider
```

Tells you how a booking is confirmed for a given provider. Call it before
`/booking/pay` when the provider is `PayOnSite` or `None`. Those two go through
the account's SMS verification. For the other providers it just answers `Payment`.

**Request**

| Field | Required | Description |
| --- | --- | --- |
| `cbUuid` | Yes | The reservation |
| `paymentProvider` | Yes | `PayOnSite`, `None`, `Stripe`, `Swish` or `Klarna` |

```json
{ "cbUuid": "4674c65b-a64f-43a5-94c7-af3b59053cc6", "paymentProvider": "PayOnSite" }
```

**Response 200**

```json
{ "confirmationMethod": "Pin" }
```

| Value | Meaning |
| --- | --- |
| `Pin` | A PIN SMS was sent to the customer's phone. Pass the code as `pin` in `/booking/pay` |
| `NoPin` | No verification. Call `/booking/pay` with `pin` set to `null` |
| `Payment` | The provider takes online payment, no PIN is involved |

Every call with `PayOnSite` or `None` on an account with SMS verification
sends a new PIN and invalidates the previous one, so call it once per choice.
The call does not create anything on the payment provider side and does not
extend the reservation.

**Errors**

| Status | When |
| --- | --- |
| 400, code `3` | The `cbUuid` has no pending reservation |

## Pay and confirm

```
POST /booking/pay
```

Completes a booking whose `confirmationMethod` is `Payment`. Replaces
[`/booking/confirm`](https://developers.cliento.com/docs/widget-api/booking.md#confirm-the-booking) for these bookings.
Extends the reservation by five minutes.

**Request**

| Field | Required | Description |
| --- | --- | --- |
| `cbUuid` | Yes | The reservation |
| `paymentProvider` | Yes | `Stripe`, `Swish`, `Klarna`, `PayOnSite` or `None` |
| `pin` | `PayOnSite`, `None` | The PIN from the SMS when [payment-provider](#choose-payment-provider) answered `Pin`, otherwise `null`. Leave it out for the other providers |
| `stripePaymentMethodId` | `Stripe` | Payment method id (`pm_…`) from Stripe.js, first call |
| `stripePaymentIntentId` | `Stripe` | Payment intent id (`pi_…`) after 3D Secure, second call. Send one of the two Stripe fields |
| `swishPayerAlias` | No | `Swish` only. The customer's Swish number in E.164 format without `+`, for example `46701234567`. `null` lets the customer enter it in the Swish app |
| `klarnaAuthToken` | `Klarna` | The `authorization_token` from the Klarna SDK |
| `captchaResponse` | With captcha | A reCAPTCHA token when the account requires one, otherwise `null`, see [Captcha](https://developers.cliento.com/docs/widget-api.md#captcha) |

```json
{
  "cbUuid": "4674c65b-a64f-43a5-94c7-af3b59053cc6",
  "paymentProvider": "Stripe",
  "stripePaymentMethodId": "pm_1ABCdefGHIjklMNOpqrsTUVW",
  "captchaResponse": null
}
```

**Response 201**, the booking is confirmed:

```json
{
  "cbUuid": "4674c65b-a64f-43a5-94c7-af3b59053cc6",
  "cbRef": "mK0k22O3",
  "smsReminderEnabled": true,
  "emailConfirmSent": true,
  "smsConfirmSent": true,
  "paymentStatus": "Paid",
  "paymentProvider": "Stripe"
}
```

Same body as [confirm](https://developers.cliento.com/docs/widget-api/booking.md#confirm-the-booking). `paymentStatus`
and `paymentProvider` depend on the provider:

| Provider | `paymentStatus` | `paymentProvider` |
| --- | --- | --- |
| `Stripe`, `Klarna` | `Paid` | `Stripe`, `Klarna` |
| `None` | `Paid` | `None` |
| `PayOnSite` | `Unpaid` | `None` (the chosen provider is not echoed back) |
| `Swish` | Never `201` from this call. The confirmed booking comes from the [status](#booking-status) endpoint with `Paid` and `Swish` |

**Response 402**, payment not completed. The body has one of three shapes:

Stripe, 3D Secure needed. Run `stripe.handleCardAction(clientSecret)` and call
again with `stripePaymentIntentId`:

```json
{
  "providerStatus": "requires_action",
  "clientSecret": "pi_3ABCdefGHIjklMNO_secret_pqrsTUVWxyz",
  "requiresAction": true,
  "paymentSucceeded": false,
  "chargeId": null,
  "source": "Visa **** 4242"
}
```

Payment failed, for example a declined card. The reservation is still held:

```json
{
  "paymentStatus": "Unpaid",
  "paymentProvider": "Stripe",
  "failureCode": "card_declined",
  "failureMessage": "Kortet nekades"
}
```

Swish, payment request created. Poll the [status](#booking-status) endpoint
with `instructionUuid`:

```json
{
  "paymentRequestToken": "c28a4061470440d8a27a5c9a7b1f4c7e",
  "bookingRef": "mK0k22O3",
  "instructionUuid": "B9A4C7D5E2F44A1B8C3D6E7F8A9B0C1D"
}
```

`paymentRequestToken` is used for the `swish://paymentrequest?token=…` link
and is an empty string when Swish did not return one. `bookingRef` is the
`cbRef`.

**Errors**

| Status | When |
| --- | --- |
| 400, code `0` | Captcha failed, or no customer details were added to the reservation |
| 400, code `3` | The reservation is no longer pending: it is already confirmed |
| 400, code `3000` | The provider is not configured for the account, `PayOnSite` is not allowed for this booking, or `None` was used with an amount above zero |
| 400, code `3002` | Swish rejected the payment request. `message` has the Swish error codes, comma separated, for example `ACMT03` |
| 400, voucher code | The applied voucher is no longer valid, see the codes under [Apply or remove a voucher](#apply-or-remove-a-voucher) |
| 402 | Payment not completed, see above |
| 403 | Wrong PIN for `PayOnSite` or `None`, or the reservation is gone: it never existed, was released, or has expired and been removed. Empty body |
| 418 | The request was refused and the reservation is gone. Reserve a new slot |
| 429 | Rate limited, see [Rate limits](https://developers.cliento.com/docs/widget-api.md#rate-limits) |

The call is not idempotent, see
[Errors, retries and recovery](https://developers.cliento.com/docs/widget-api/payments.md#errors-retries-and-recovery).

## Booking status

```
POST /booking/{cbUuid}/status
```

Returns whether a reservation has been confirmed. Used to wait for a Swish
payment, and to recover after a lost `/booking/pay` response. The `cbUuid`
goes in the path as is (not Base64 encoded like the
[manage booking](https://developers.cliento.com/docs/widget-api/manage-booking.md) ref).

**Request**

A JSON body is required. Both fields may be `null`.

| Field | Required | Description |
| --- | --- | --- |
| `paymentProvider` | No | `Swish` makes the server check the Swish payment request and complete the booking if it is paid. Any other value or `null` only reports the booking state |
| `instructionUuid` | With `Swish` | The `instructionUuid` from the `402` response of `/booking/pay` |

```json
{ "paymentProvider": "Swish", "instructionUuid": "B9A4C7D5E2F44A1B8C3D6E7F8A9B0C1D" }
```

**Response 200**

```json
{
  "status": "Confirmed",
  "body": {
    "cbUuid": "4674c65b-a64f-43a5-94c7-af3b59053cc6",
    "cbRef": "mK0k22O3",
    "smsReminderEnabled": true,
    "emailConfirmSent": true,
    "smsConfirmSent": true,
    "paymentStatus": "Paid",
    "paymentProvider": "Swish"
  }
}
```

| `status` | `body` | Meaning |
| --- | --- | --- |
| `PendingConfirm` | `null`, or Swish status details | The reservation is still pending. Keep polling, or call `/booking/pay` (again) |
| `Confirmed` | The confirm response | The booking is confirmed. `paymentStatus` and `paymentProvider` reflect the payment that was made |
| `PaymentError` | `{"status", "errorCode", "message", "additionalInformation"}` | The Swish payment was `DECLINED`, `CANCELLED` or ended in `ERROR`. The reservation is still pending |

**Errors**

| Status | When |
| --- | --- |
| 410 | The `cbUuid` has no reservation and no booking: it expired, was released, or never existed |
| 400, code `3002` | Swish did not return a payment request for the `instructionUuid` |

## Create a Klarna session

```
POST /klarna/session
```

Creates a Klarna Payments session for the reservation. The `clientToken`
initializes the Klarna SDK in the browser. Extends the reservation by five
minutes. Create one session per reservation and reuse it.

**Request**

```json
{ "cbUuid": "4674c65b-a64f-43a5-94c7-af3b59053cc6" }
```

**Response 200**

```json
{
  "clientToken": "eyJhbGciOiJSUzI1NiIs...",
  "sessionId": "0b1d9815-165e-42e2-8867-35bc03789e00",
  "paymentMethodCategories": ["pay_later", "pay_over_time"],
  "saleItems": [
    { "itemType": "Service", "quantity": 1, "amount": 595.0, "vatPct": 25.0, "vatAmount": 119.0, "description": "Women's haircut" }
  ]
}
```

| Field | Description |
| --- | --- |
| `clientToken` | Pass to `Klarna.Payments.init` |
| `sessionId` | The Klarna session id, for your logs |
| `paymentMethodCategories` | The Klarna payment method categories available. Offer Klarna only when this is not empty |
| `saleItems` | The line items, including an applied voucher, same shape as in confirmation-options |

**Errors**

| Status | When |
| --- | --- |
| 400, code `3` | The `cbUuid` has no pending reservation |
| 400, code `3000` | Klarna is not configured for the account |

## Validate a voucher

```
POST /booking/validate-voucher
```

Checks a discount or gift card code against the reservation without applying
it. Only available when confirmation-options returned `allowDiscountVoucher`
`true`.

**Request**

```json
{ "cbUuid": "4674c65b-a64f-43a5-94c7-af3b59053cc6", "voucherCode": "GIFT123" }
```

**Response 200**

```json
{ "valid": true, "errors": [] }
```

An invalid code is also `200`:

```json
{
  "valid": false,
  "errors": [
    { "errorCode": "NotValidAfter", "message": "Voucher expired", "data": { "notValidAfter": "2026-06-30T23:59:59" } }
  ]
}
```

| `errorCode` | Meaning |
| --- | --- |
| `CodeNotFound` | No voucher with that code |
| `MinSpendNotReached` | The booking is below the voucher's minimum amount, `data.minSpend` |
| `NotValidBefore` | The voucher is not valid yet, `data.notValidBefore` |
| `NotValidAfter` | The voucher has expired, `data.notValidAfter` |
| `MaxRedeemsReached` | The voucher has been used up |
| `ServiceIdsNotAllowed` | The voucher does not apply to the booked services |
| `ResourceIdsNotAllowed` | The voucher does not apply to the booked resource |
| `NotValidForPayment` | The voucher cannot be used for online payment |

**Errors**

| Status | When |
| --- | --- |
| 400, code `3` | The `cbUuid` has no pending reservation |

## Apply or remove a voucher

```
POST /booking/voucher
```

Applies a voucher code to the reservation and returns the new amount to pay.
Send `voucherCode` as `null` to remove an applied voucher.

**Request**

```json
{ "cbUuid": "4674c65b-a64f-43a5-94c7-af3b59053cc6", "voucherCode": "GIFT123" }
```

**Response 200**

```json
{
  "amount": 395.0,
  "saleItems": [
    { "itemType": "Service", "quantity": 1, "amount": 595.0, "vatPct": 25.0, "discountAmount": 0, "description": "Women's haircut", "...": "..." },
    { "itemType": "Discount", "quantity": 1, "amount": 0, "vatPct": 0, "discountAmount": -200.0, "description": "GIFT123", "...": "..." }
  ],
  "discountVoucher": { "code": "GIFT123" }
}
```

| Field | Description |
| --- | --- |
| `amount` | The new amount to pay including VAT. Replaces `paymentAmountIncVat`. When it is `0`, complete the booking with `/booking/pay` and `paymentProvider` `None` |
| `saleItems` | The line items. A `Discount` item has `amount` `0` and the discount as a negative `discountAmount`. The items have more internal fields than shown. Rely only on `itemType`, `quantity`, `amount`, `vatPct`, `discountAmount` and `description` |
| `discountVoucher` | The applied voucher, or `null` after removal |

The response of confirmation-options does not include an applied voucher, so
keep `amount` and `saleItems` from this response. The voucher is validated again
when the booking is paid.

**Errors**

A voucher that fails [validate](#validate-a-voucher) is refused here with a
`400`, for the same reasons:

| Status | When |
| --- | --- |
| 400, code `3` | The `cbUuid` has no pending reservation |
| 400, code `1117` | `CodeNotFound` |
| 400, code `1110` | `NotValidForPayment` |
| 400, code `1118` | `MinSpendNotReached` |
| 400, code `1119` | `NotValidBefore`, `details` has `notValidBefore` |
| 400, code `1120` | `NotValidAfter`, `details` has `notValidAfter` |
| 400, code `1121` | `MaxRedeemsReached` |
| 400, code `1126` | `ServiceIdsNotAllowed` |
| 400, code `1127` | `ResourceIdsNotAllowed` |
