# Repejo Payment Sessions API

> Create a payment **server-side**, send the donor through Repejo's hosted
> checkout (redirect) or an embedded `<repejo-checkout>` element, and receive
> the outcome via webhooks. This is Repejo's "Pay by Link" capability.
> Donors complete the recurring mandate
> (e.g. Autogiro) with BankID on their own device.

- Machine-readable spec (OpenAPI 3.1, JSON): https://app.repejo.se/api/v1/openapi
- Interactive API explorer (Swagger UI): https://app.repejo.se/api/v1/swagger
- Human docs: https://app.repejo.se/docs/api-payment-sessions

**For coding agents:** point your tool at the OpenAPI URL above — it is the
source of truth and contains every operation, schema, enum and error.

## Prerequisites

Before you can create a payment session you must already have **both** of the
following. They are provided by the integrating organisation — you cannot
create them through this API:

1. An **organisation-scoped API token** (the `Authorization: Bearer` value).
2. A **`checkout_id`** (`chk_…`) of an **API-type** (`:api`) checkout in that
   same organisation. Payment sessions can only be created against `:api`
   checkouts; any other checkout type returns `422 checkout_not_api`.

If you are building the **embedded** flow (the `<repejo-checkout>` element),
you also need:

3. The checkout's **`short_code`** — a short, human-readable handle (e.g.
   `aunt123`) that is **not** the same value as the `checkout_id` (`chk_…`).
   The `short_code` is what the embed element takes as its `short_code`
   attribute. It is returned on every `POST /payment_session` response, but
   for a statically embedded element you typically hardcode it, so have the
   integrating organisation provide it up front. They can copy it from the
   checkout's *Användning* tab in the Repejo back office (shown next to the
   API-checkout-ID, which is the `checkout_id`). **If it is unclear which of
   the two values you have been given, ask** — `chk_…` is the `checkout_id`
   used in the API request body; the short code is the embed handle.

### One checkout per periodicity

An `:api` checkout is locked to a single payment period. To offer both
**one-time** and **monthly** donations, create **two separate checkouts** —
one per periodicity — each with its own `checkout_id` (`chk_…`), and pick the
one that matches the donation type when creating the session. A
`payment_method` whose periodicity doesn't match the checkout is rejected with
`422 payment_method_not_valid_for_period` (e.g. `swish_onetime` on a recurring
checkout, or `autogiro` on a one-time checkout).

### Choose an integration flow

There are **two ways** to bring the donor through the payment session, and they
drive a different front-end implementation (see [Integration modes](#integration-modes)):

- **Redirect** — send the donor's browser to `payment_url` (Repejo's hosted
  checkout) and reconcile via webhooks / `success_url`.
- **Embedded** — render the `<repejo-checkout>` element on your own page and
  listen for its DOM events.

Decide which flow you are building **before** you start. If the integrating
user hasn't made it clear which they want, **ask them** rather than guessing —
the choice changes the front-end code you write.

## Authentication

All requests use a Bearer **API token scoped to a single organisation**.
An organisation admin mints one in the Repejo back office under
*Settings → API-nycklar*; the secret is shown once.

```
Authorization: Bearer <api_token>
```

Missing/invalid token → `401`:

```json
{ "errors": { "detail": "Unauthorized" } }
```

## Quickstart

1. Mint an org-scoped API token (back office, once).
2. Create an `:api` ("API") type checkout in the back office; note its
   `checkout_id` (`chk_…`).
3. Create a payment session:

```bash
curl -X POST https://app.repejo.se/api/v1/payment_session \
  -H "Authorization: Bearer <api_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 200,
    "currency": "SEK",
    "checkout_id": "chk_123412",
    "payment_method": "autogiro",
    "metadata": { "lead_id": "12345" },
    "payer": {
      "first_name": "Anna",
      "last_name": "Andersson",
      "phone_number": "+46701234567",
      "email": "anna@example.com"
    },
    "success_url": "https://example.org/success",
    "cancel_url": "https://example.org/cancel"
  }'
```

Response `201`:

```json
{
  "payment_url": "https://app.repejo.se/s/aunt123?rp_token=abc123",
  "session_token": "abc123",
  "short_code": "aunt123"
}
```

4. Send the donor to the payment session — either **redirect** them to
   `payment_url`, or **embed** the checkout (see below).
5. Reconcile the outcome from the `payment_session.*` **webhooks**.

## Endpoint

`POST /api/v1/payment_session`

### Request body

| Field            | Type                | Required | Notes |
|------------------|---------------------|----------|-------|
| `amount`         | number              | yes      | Donation amount, > 0, in the checkout's currency. The checkout's periodicity decides how it is charged: on a recurring checkout it is the per-period amount (e.g. monthly), confirmed by `payment_session.succeeded`; on a one-time checkout it is the single charge. |
| `currency`       | enum                | yes      | One of `SEK`, `EUR`. |
| `checkout_id`    | string (`chk_…`)    | yes      | Must be a checkout of type `api`. Always include the `chk_` prefix; an unknown or malformed id returns `422 checkout_not_found`. |
| `payment_method` | enum                | no       | Preselects and opens that method directly. One of `autogiro`, `swish`, `swish_onetime`, `card`, `apple_pay`, `google_pay`, `sepa_direct_debit`. Must be enabled on the checkout, and must match the checkout's periodicity. Omit to let the donor choose. |
| `payer`          | object              | no       | `{ first_name, last_name, phone_number, email }`. All optional. If you don't send donor details, link the donor via `metadata` (e.g. `metadata.lead_id`). |
| `metadata`       | object<string,string>| no      | Echoed back on every webhook (e.g. `lead_id`). |
| `success_url`    | string (http/https) | yes      | Absolute URL the donor returns to on completion (redirect flow). |
| `cancel_url`     | string (http/https) | yes      | Absolute URL the donor returns to on cancel (redirect flow). The hosted page's back button becomes a cancel button that redirects here. |

`success_url` and `cancel_url` must be absolute `http`/`https` URLs;
other schemes (`javascript:`, `data:`, relative paths) are rejected.

### Response `201`

| Field           | Type   | Notes |
|-----------------|--------|-------|
| `payment_url`   | string | `{host}/s/{short_code}?rp_token={session_token}` — redirect the donor here. |
| `session_token` | string | The session handle; also used as the embed `session-token`. |
| `short_code`    | string | The checkout's short code; used as the embed `short_code`. |

### Errors

| Status | Body | When |
|--------|------|------|
| `400`  | validation error | Malformed body, or `currency`/`payment_method` outside the enum. The `details` array names the offending field (e.g. `{"location":["currency"]}`). |
| `401`  | `{"errors":{"detail":"Unauthorized"}}` | Missing/invalid token. |
| `422`  | `{"error":"checkout_not_found"}` | The `checkout_id` is unknown or malformed (e.g. missing the `chk_` prefix). |
| `422`  | `{"error":"checkout_not_api"}` | `checkout_id` is not an `api`-type checkout. |
| `422`  | `{"error":"payment_method_not_enabled"}` | The `payment_method` isn't enabled on that checkout. |
| `422`  | `{"error":"payment_method_not_valid_for_period"}` | The `payment_method`'s periodicity doesn't match the checkout (e.g. `swish_onetime` on a recurring checkout). |
| `422`  | `{"error":"invalid_success_url"}` / `{"error":"invalid_cancel_url"}` | The URL is not an absolute `http`/`https` URL — non-http(s) schemes (`javascript:`, `data:`), relative paths and empty strings are rejected. |

## Integration modes

### Redirect

Redirect the donor's browser to `payment_url`. Repejo's hosted checkout skips
the amount screen and opens the (optionally preselected) payment method. On
completion Repejo redirects to your `success_url`; the back button cancels and
redirects to your `cancel_url`.

### Embedded

Render the custom element on your own page (load `https://app.repejo.se/assets/checkout.js`
once). Pass the `short_code` and `session_token` from the response:

```html
<script defer src="https://app.repejo.se/assets/checkout.js" data-host="https://app.repejo.se"></script>

<repejo-checkout short_code="aunt123" session-token="abc123" host="https://app.repejo.se"></repejo-checkout>
```

The `host` attribute (and the script tag's `data-host`) **must point at the
Repejo environment you are integrating against** — `https://test.repejo.se`
for testing/staging, or `https://app.repejo.se` for production. It must match
the environment where the API token and `checkout_id` were created and where
you called `POST /payment_session`; pointing the element at a different host
than the one that issued the session will not find the session. The examples
above use `https://app.repejo.se`, the environment that served this document.

The element connects to Repejo cross-origin and skips the amount screen. It
dispatches DOM events you can listen for (no redirect). The events **bubble
from the `<repejo-checkout>` element up to `window`**, so you may listen on
either. Both recurring (mandate signed) and one-time (payment captured)
completions fire `repejo:completed`; `event.detail.type` is `"recurring"` or
`"onetime"`.

```js
window.addEventListener("repejo:completed", (e) => { /* e.detail.type → show your thank-you */ });
window.addEventListener("repejo:cancelled", (e) => { /* donor cancelled */ });
```

The element also emits informational events you can ignore:
`repejo:first-interaction` (donor started) and `repejo:registered` (left the
amount step). Only `repejo:completed` / `repejo:cancelled` are part of the
contract.

Treat the `payment_session.succeeded` webhook as the authoritative signal;
the DOM events are a UX convenience.

## Webhooks

Outcomes are delivered as webhooks to the tenant webhook endpoints you
register in the back office (subscribe them to the `payment_session.*`
events). Delivery is asynchronous, signed and retried.

Events:

```
payment_session.created     // session created (donor not yet finished)
payment_session.succeeded   // recurring mandate signed OR one-time payment captured — mark the lead done
payment_session.cancelled   // donor cancelled
```

`payment_session.succeeded` is the single reconciliation signal for **both**
periodicities: a recurring checkout fires it when the mandate is signed, a
one-time checkout (Swish / card) when the single payment is captured. You do
**not** need to consume `transaction.*` to confirm a one-time gift.

Every delivery has this envelope; `data.metadata` carries your `lead_id`:

```json
{
  "sent_at": "2026-06-13T12:00:00Z",
  "event_type": "payment_session.succeeded",
  "data": {
    "id": "pls_8x…",
    "checkout_id": "chk_123412",
    "status": "succeeded",
    "amount": "200",
    "currency": "SEK",
    "payment_method": "autogiro",
    "metadata": { "lead_id": "12345" },
    "inserted_at": "2026-06-13T11:59:00Z",
    "updated_at": "2026-06-13T12:00:00Z"
  }
}
```

`payment_session.cancelled` is identical with `event_type` /
`data.status` = `cancelled`; `payment_session.created` with `created`.
`amount` is delivered as a **string** (`"200"`), not a number.

A completed recurring mandate also emits a `subscription.created` event (from
the general webhooks family, `data.id = sub_…`). Both families fire; treat
`payment_session.succeeded` as authoritative for reconciling a Payment Session
because it carries your `metadata`. See the Webhooks reference for the
`subscription.*` payload and PII/security notes: https://app.repejo.se/docs/api-webhooks.md

### Verifying & handling deliveries

- Each POST is signed: `Repejo-Signature: sha256=<hex>` is HMAC-SHA256 over
  the **raw** body using the endpoint's secret. Compare against the raw body,
  not re-serialized JSON.
- Headers also include `X-Repejo-Event-ID` (dedupe retries),
  `X-Repejo-Event-Type` and `X-Repejo-Attempt`.
- Return `2xx` once accepted. Non-2xx is retried with exponential backoff.
- Treat handlers as idempotent (use `X-Repejo-Event-ID` / `data.id`).

See the general Webhooks reference for the full signing/retry contract:
https://app.repejo.se/docs/api-webhooks
