Payment Sessions

Create a payment server-side, then 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. The donor completes the recurring mandate (e.g. Autogiro) with BankID on their own device.

How it works

NGO backend your server, holds the API token Donor's browser NGO site with <repejo-checkout> Repejo app.repejo.se 1. POST /api/v1/payment_session Bearer <api_token> · amount, currency, checkout_id, payment_method, metadata 2. 201 Created — session token returned session_token · payment_url · short_code 3. Initialize the checkout <repejo-checkout short_code=… session-token=…> (or redirect the donor to payment_url — hosted checkout) element connects & binds the session 4. Handled entirely inside Repejo Account information lookup — bank & account fetched automatically Bank / payment-method handling (e.g. Autogiro mandate) Donor signs with BankID on their own device 5. Webhook to the NGO — payment details returned payment_session.succeeded (also .created / .cancelled) payload: payment details (amount, currency, payment method, payer) + your metadata NGO reconciles the donation match on metadata / session Donor sees confirmation success_url / completion screen

Machine-readable spec

The OpenAPI document is the source of truth — it contains every operation, schema, enum and error. Coding agents: point your tool at the OpenAPI URL.

Authentication

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

Authorization: Bearer <api_token>

A missing or invalid token returns 401:

{ "errors": { "detail": "Unauthorized" } }

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.

Quickstart

  1. Mint an org-scoped API token (back office, once).
  2. Create an api ("API") type checkout per periodicity and note each checkout_id (shown on the checkout's Användning tab).
  3. Create a payment session (below).
  4. Redirect the donor to payment_url, or embed the checkout.
  5. Reconcile from the payment_session.* webhooks.
curl -X POST https://app.repejo.se/api/v1/payment_session \
  -H "Authorization: Bearer <api_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "checkout_id": "chk_0TMXp8QPR6yGvKuV2GZQ10",
    "subscription": { "amount": 200, "currency": "SEK" },
    "payment_method": { "type": "autogiro" },
    "metadata": { "lead_id": "12345" },
    "success_url": "https://example.org/success",
    "cancel_url": "https://example.org/cancel"
  }'

Response 201:

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

Request — POST /api/v1/payment_session

  • request_type (optional) — subscription (the default) or payment_method. A subscription request sets up a subscription and the payment method that pays it. A payment-method request sets up an Autogiro mandate only — no amount, no currency, no subscription — for a donor who is changing bank. Omit it and nothing changes for an existing integration.
  • subscription (object) — amount and currency, both required inside it. Required on a subscription request (422 subscription_required without it) and must be absent on a payment-method request (422 subscription_not_allowed). The amount is > 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 (mandate signed); on a one-time checkout it is the single charge. The currency is one of SEK, EUR, and must be one the checkout offers.
  • amount / currency (deprecated) — the same two values at the top level. Still accepted and identical in meaning, so no existing integration breaks. Sending both forms returns 422 subscription_conflict.
  • checkout_id (string, required) — must be an api-type checkout.
  • payment_method (optional object) — { type }, mirroring the object the webhooks return. Preselects and opens that method; the type is one of autogiro, swish, swish_onetime, card, apple_pay, google_pay, sepa_direct_debit. A bare string is deprecated but still accepted. On a payment-method request it is required (422 payment_method_required) and must be autogiro (422 payment_method_not_valid_for_request_type). Must be enabled on the checkout. apple_pay / google_pay are gated by the checkout's wallet toggles at session creation; both wallets additionally require the domain they are presented on to be a verified Apple Pay domain (back office → Settings → Payment methods), otherwise the wallet is not offered when the payment page renders. For swish / card (recurring) the consent / setup-intent fires when the donor opens the checkout, so send payer.phone_number if you have it. Omit to let the donor choose.
  • payer (optional object) — first_name, last_name, phone_number, email, address_street, address_zip, address_city, address_country. All optional, except phone_number is required when payment_method.type is swish (recurring): a Swish mandate binds to the donor's phone, so a session without it would strand the donor at the final "Fortsätt med Swish" step — omitting it returns 422 payer_phone_number_required. Values are normalized (trimmed; email lower-cased; phone to E.164 without +), but an email or phone_number that is still invalid is rejected with a 422 (see Errors) — validate donor input before calling. Max lengths: first_name / last_name 500, email 254, phone_number 32, address_* 500. The address is printed on the mandate PDF.
  • metadata (optional object) — echoed on the payment_session.* webhooks (e.g. lead_id). It is not copied to the payer or subscription, so payer.* and subscription.* webhooks do not carry it. Each value is a string of at most 500 characters.
  • success_url / cancel_url (required) — absolute http/https URLs the donor returns to. Max 2048 characters each. The scheme and host are validated: a non-http(s) scheme, a missing host, or whitespace anywhere in the host is rejected with a 400. Whitespace after the host is allowed, so an unencoded space in a query string is accepted.

Errors

  • 400 — malformed body, currency/payment_method outside the enum, a string field over its maximum length, or a success_url/cancel_url that isn't a well-formed absolute http(s) URL. The details array names the offending field, e.g. {"location":["success_url"]}.
  • 401 — 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"} — the checkout isn't an api checkout.
  • 422 {"error":"currency_not_enabled"} — the currency isn't one the checkout offers (e.g. EUR on a SEK-only checkout).
  • 422 {"error":"payment_method_not_enabled"} — that method isn't enabled on the checkout.
  • 422 {"error":"payment_method_not_valid_for_period"} — that method's periodicity doesn't match the checkout (e.g. swish_onetime on a recurring checkout, or autogiro on a one-time checkout).
  • 422 {"error":"subscription_required"} — a subscription request carried no amount and currency, in either form.
  • 422 {"error":"subscription_conflict"} — both the nested subscription object and the deprecated top-level amount/currency were sent. Send one or the other.
  • 422 {"error":"subscription_not_allowed"} — a payment_method request carried an amount or a currency. It sets up a mandate only, so there is nothing to charge.
  • 422 {"error":"payment_method_required"} — a payment_method request named no method. The donor has no amount to enter, so nothing would advance them past an empty picker.
  • 422 {"error":"payment_method_not_valid_for_request_type"} — a payment_method request named something other than autogiro.
  • 422 {"error":"request_type_not_valid_for_period"} — a payment_method request targeted a one-time checkout. Autogiro mandates are recurring.
  • 422 {"error":"invalid_payer_email"}payer.email is not a valid email address (after trimming/lower-casing).
  • 422 {"error":"invalid_payer_phone_number"}payer.phone_number is not a valid mobile number.
  • 422 {"error":"payer_phone_number_required"}payment_method.type is swish and payer.phone_number is missing or blank. A recurring Swish mandate needs the donor's phone as the Swish payer_alias; without it the donor would strand at "Fortsätt med Swish". Send payer.phone_number.
  • 422 {"error":"invalid_payer"} — another payer field failed validation (e.g. a value over 500 characters).
  • 422 {"error":"amount_below_minimum"} — on a one-time checkout, amount is below the checkout's minimum.
  • 422 {"error":"invalid_subscription"} — on a recurring checkout, the subscription failed validation — most often amount below the checkout's minimum.
  • 422 {"error":"amount_above_maximum"}amount is above 99999999.99.
  • 422 {"error":"invalid_success_url"} / 422 {"error":"invalid_cancel_url"} — the URL isn't an absolute http/https URL. Malformed URLs are normally caught earlier and answered with a 400; these codes remain as a fallback.

Documents — mandate PDFs and BankID signature XML

A signed recurring mandate produces documents you can download with the same Bearer token. The urls are handed to you in the documents object of payment_session.finalized and of every subscription.* webhook, so you never build them by hand. The bytes are served through the API — there is no public or redirect url.

  • GET /api/v1/subscriptions/:id/mandate_pdf_masked — the donor-facing copy of the mandate PDF, account number and personal identity number redacted. application/pdf.
  • GET /api/v1/subscriptions/:id/mandate_pdf_unmasked — the organisation's copy with full details. application/pdf.
  • GET /api/v1/mandates/:id/signature_xml — the BankID signature envelope of an Autogiro mandate (:id is the payment_method.id, aum_…). application/xml.

PDFs are owned by the subscription (one mandate can back several subscriptions, each with its own PDF); the XML is owned by the mandate. A url is null in the webhook when the document does not exist at that moment: PDFs are rendered right after signing for autogiro and sepa_direct_debit mandates on a checkout that enables the mandate language (Swedish resp. German) and never for other payment methods; the XML exists only for mandates signed with BankID (not paper, imported or hand-drawn mandates, and never SEPA). A subscription created outside a checkout gets its PDF urls on the first subscription.updated after the PDFs have been produced.

Every endpoint answers 404 {"error":"not_found"} whenever the document is not available — unknown id, an id belonging to another organisation, or a document that does not exist for that record — and 401 without a valid token. Wait for payment_session.finalized before fetching: it is sent after the PDFs have been rendered, so a url it carries is ready to download.

Integration modes

Redirect

Redirect the donor's browser to payment_url. The 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 checkout.js once), passing the short_code and session_token from the response.

<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 attributes are short_code (the kebab-case short-code is also accepted), session-token and host. When you create the element in JavaScript, set them as attributes or properties — either before appendChild or afterwards both work; the element fetches the checkout once the short_code is set.

Fundraising teams are not available on payment sessions. The team attribute assigns a donation to a fundraising team (ftm_…), but it only applies to a checkout the element opens itself — the hosted-page equivalent is ?rp_team=ftm_…. A payment session is created server-side and reused via session-token, so the attribute is ignored in that mode, and POST /payment_session has no field for a team. Use the plain <repejo-checkout> integration for team competitions.

const el = document.createElement("repejo-checkout");
el.host = "https://app.repejo.se";
el.short_code = "aunt123";        // property (or el.setAttribute("short_code", "aunt123"))
el.session_token = "abc123";      // property (attribute: session-token)
document.querySelector("#checkout").appendChild(el);

To match the element to a coloured page (avoid the white "card-in-card"), either add the bare attribute — which removes the card chrome (background, border, shadow, fixed width) and the footer (Repejo logo, back button, trust logos) entirely, leaving only the payment screen on your page's own background — or set the checkout's background/border/shadow in its branding, since those are not overridable via host-page CSS variables. bare is only honored on API checkouts (payment sessions) and also removes the in-checkout back/cancel navigation. TypeScript/React types are published at https://test.repejo.se/assets/repejo-checkout.d.ts. See the Web Component docs for both.

In embedded mode the element dispatches DOM events instead of redirecting. They bubble from the <repejo-checkout> element up to window, so listen on either. Both recurring (mandate signed) and one-time (payment captured) completions fire repejo:completed; event.detail carries type ("recurring" | "onetime").

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 to the tenant webhook endpoints you register in the back office (subscribe them to the payment_session.* events). Delivery is asynchronous, signed and retried.

type PaymentSessionEvent =
| "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.finalized"  // post-processing done — mandate documents available (data.documents)
| "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 when the single payment (Swish / card) is captured. You do not need to consume transaction.* to confirm a one-time gift.

Each delivery has this envelope; data.metadata carries your lead_id. payment_session.succeeded also embeds the resulting subscription, the signed payment_method object and the payer so you can reconcile the donor without a follow-up call:

{
  "sent_at": "2026-06-13T12:00:00Z",
  "event_type": "payment_session.succeeded",
  "data": {
    "id": "pls_8x…",
    "checkout_id": "chk_0TMXp8QPR6yGvKuV2GZQ10",
    "status": "succeeded",
    "request_type": "subscription",
    "amount": "200",
    "currency": "SEK",
    "payment_method_type": "autogiro",
    "metadata": { "lead_id": "12345" },
    "subscription": {
      "id": "sub_9y…",
      "amount": "200",
      "status": "active",
      "reference": "Monthly gift",
      "recruiter_external_id": null,
      "recruiter_first_name": null,
      "recruiter_last_name": null
    },
    "payment_method": {
      "id": "aum_1a…",
      "type": "autogiro",
      "clearing_number": "8393",
      "account_number": "41231614",
      "bank": "Swedbank"
    },
    "payer": {
      "id": "pay_2b…",
      "name": "Anna Andersson",
      "email": "anna@example.com",
      "status": "active"
    }
  }
}

payment_session.finalized is sent once per succeeded session after post-processing (mandate PDF rendering) has finished — for a one-time payment straight after succeeded. It has exactly the succeeded shape plus a documents object — see Documents above for the endpoints and when a url is null:

{
  "sent_at": "2026-06-13T12:00:30Z",
  "event_type": "payment_session.finalized",
  "data": {
    "id": "pls_8x…",
    "status": "succeeded",
    "metadata": { "lead_id": "12345" },
    "subscription": { "id": "sub_9y…", "…": "…" },
    "payment_method": { "id": "aum_1a…", "type": "autogiro", "…": "…" },
    "payer": { "id": "pay_2b…", "…": "…" },
    "documents": {
      "mandate_pdf_masked_url": "https://app.repejo.se/api/v1/subscriptions/sub_9y…/mandate_pdf_masked",
      "mandate_pdf_unmasked_url": "https://app.repejo.se/api/v1/subscriptions/sub_9y…/mandate_pdf_unmasked",
      "signature_xml_url": "https://app.repejo.se/api/v1/mandates/aum_1a…/signature_xml"
    }
  }
}

The scalar preselect payment_method string is replaced by the embedded object plus a payment_method_type (e.g. autogiro / swish / stripe), mirroring the subscription.* webhook shape. For a one-time session there is no subscription, so subscription, payment_method and payment_method_type are null; the payer is always present. The payment_session.created and payment_session.cancelled events carry the lean envelope without these embeds (and keep the scalar payment_method preselect). See the Webhooks reference for the full subscription / payment_method / payer field lists.

Mandate without an amount

For a donor who already gives and is only changing bank, create the session with request_type: "payment_method" and payment_method: { "type": "autogiro" }, and send no amount or currency. The donor picks their bank and signs with BankID exactly as before. The webhook then carries the payment_method and the payer, and subscription is null:

{
  "sent_at": "2026-06-13T12:00:00Z",
  "event_type": "payment_session.succeeded",
  "data": {
    "id": "pls_8x…",
    "checkout_id": "chk_0TMXp8QPR6yGvKuV2GZQ10",
    "status": "succeeded",
    "request_type": "payment_method",
    "amount": null,
    "currency": null,
    "subscription": null,
    "payment_method_type": "autogiro",
    "payment_method": {
      "id": "aum_1a…",
      "type": "autogiro",
      "clearing_number": "8393",
      "account_number": "41231614",
      "bank": "Swedbank"
    },
    "payer": { "id": "pay_2b…", "name": "Anna Andersson" },
    "metadata": { "lead_id": "12345" }
  }
}

On payment_session.finalized the documents object carries the BankID signature_xml_url; both mandate_pdf_*_url are null, because the mandate PDF is rendered from a subscription and states its amount and periodicity.

The mandate is not attached to any subscription the donor already has. Repejo transmits it to Bankgirot and reports it to you; connecting it to an existing recurring gift is your side of the integration for now. Only autogiro is supported, and the checkout must be a recurring api checkout.

See the Webhooks reference for the full signing (Repejo-Signature: sha256=<hex>), retry and idempotency contract.