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.
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.
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" } }
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.
api
("API") type checkout per periodicity
and note each checkout_id
(shown on the checkout's Användning
tab).
payment_url, or embed the checkout.
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 '{
"amount": 200,
"currency": "SEK",
"checkout_id": "chk_0TMXp8QPR6yGvKuV2GZQ10",
"payment_method": "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"
}
amount
(number, required) — the 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
(mandate signed); on a one-time checkout it is the single charge.
currency
(required) — one of SEK, EUR.
Must be a currency the checkout offers.
checkout_id
(string, required) — must be an api-type checkout.
payment_method
(optional) — preselects and opens that method; one of autogiro, swish, swish_onetime, card, apple_pay, google_pay, sepa_direct_debit, invoice. 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.
All optional, except
phone_number
is
required when
payment_method
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.
metadata
(optional object) — echoed on every webhook (e.g. lead_id). 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.
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":"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
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.
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.
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.
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://app.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.
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",
"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.
See the
Webhooks reference
for the full signing (Repejo-Signature: sha256=<hex>),
retry and idempotency contract.