Veracity Pay
En betaltjänst för test. Den ser ut och beter sig som en riktig PSP – kassa, kort, Swish, faktura, prenumerationer, webhooks – men inga pengar flyttas. En betalning godkänns med en sexsiffrig kod som skickas till betalarens e-post (Testinkorgen). Den är gjord för projektets tidiga faser: bygg och testa betalningarna innan ni har valt betalpartner eller skrivit avtal, och byt sedan bara host och nycklar. Varje betalpartner efterliknas så nära det går utifrån leverantörens publika dokumentation; det som skiljer står listat per variant. Inga avgifter tas ut, och du behöver inget företag för att skapa ett konto.
Ny på onlinebetalningar? Läs Onlinebetalningar från grunden först. Saknar du ett betalsätt eller en betaltjänst: önska ett betalsätt eller tipsa om en betaltjänst.
Översikt
- En motor, elva varianter. Adyen, Kustom, Ledyer Checkout, Nets Easy, Qliro One, Stripe, Svea Checkout, Swedbank Pay, Swish Handel, Two, Walley Checkout följer var och en originalets sökvägar, fält, autentisering och callbacks. Samma konto och nyckel fungerar mot alla, och varje variants avsnitt länkar originalets dokumentation och listar avvikelserna. Billie Direct API (företagsfaktura i Tyskland, EUR) svarar fortfarande men listas inte, eftersom svenska handlare bara når Billie via partners som Klarna och Adyen: /guides/billie.md.
- Hosted checkout på
/pay/<cs_test_…>med handlarens namn, logotyp och färg. Betalaren känns igen mellan handlare (cookie + e-post) och får sina uppgifter ifyllda. - Belopp anges i minsta valutaenhet (öre), valuta som ISO-kod i gemener (
sek). Priser är inklusive moms; momsen räknas ut med handlarens standardmoms (25 % om inget annat anges). - Testläge överallt. Alla nycklar heter
vp_sk_test_…/vp_pk_test_…och alla objekt harlivemode: false.
Byggd för AI-driven utveckling
Det snabbaste sättet är att låta din kodagent göra jobbet via vår MCP-server. Agenten frågar vilka betalpartners du vill emulera och vilken e-post kontot ska ha, registrerar det och skriver integrationen. Nycklarna kommer till din e-post – agenten ser aldrig en hemlighet.
1. Lägg till MCP-servern
Claude Code:
claude mcp add --transport http veracity-pay https://pay.veracity.se/mcpCursor, Windsurf och andra klienter med Streamable HTTP (.mcp.json eller motsvarande):
{
"mcpServers": {
"veracity-pay": {
"url": "https://pay.veracity.se/mcp",
"headers": { "Authorization": "Bearer ${VERACITY_PAY_SECRET_KEY}" }
}
}
}2. Be agenten
Jag vill att vi använder Veracity Pay för att emulera betalningar i detta projekt.Agenten anropar list_providers, frågar dig två saker och anropar register_merchant. Du får ett mejl med handlar-ID (MID), nycklar, webhook-hemlighet och färdiga kommandon (.env.local, vercel env add och webhook-kopplingen). Agenten avslutar med: ”Du har fått ett mail med dina API-nycklar och kommandon för att lägga in dem. Jag ser inga hemligheter.”
3. Ge testverktygen din nyckel
När nyckeln ligger i din miljö läser MCP-klienten den i en Authorization-rubrik. Då kan agenten testa utan webbläsare, utan att nyckeln syns i chatten: list_recent_checkouts och complete_checkout betalar en kassa i valfri betallösning med valfritt utfall, list_webhook_endpoints och update_webhook_endpoint kopplar webhooks till appen (utan att visa hemligheten), list_webhook_deliveries och resend_webhook_delivery felsöker leveranserna, och advance_clock, set_next_charge_outcome, simulate_outage, key_status och extend_key sköter klockan, förnyelserna, störningarna och nyckeln.
claude mcp add --transport http veracity-pay https://pay.veracity.se/mcp --header "Authorization: Bearer ${VERACITY_PAY_SECRET_KEY}"4. Lägg till i CLAUDE.md eller AGENTS.md
## Payments (Veracity Pay, test only)
- Payments are emulated with Veracity Pay (https://pay.veracity.se), a test payment provider. No real money moves.
- Use the MCP server `veracity-pay` (https://pay.veracity.se/mcp) for guides and test helpers. Never print or log secret keys; read them from env.
- Env: VERACITY_PAY_BASE_URL, VERACITY_PAY_SECRET_KEY, NEXT_PUBLIC_VERACITY_PAY_PUBLISHABLE_KEY, VERACITY_PAY_WEBHOOK_SECRET.
- Verify webhooks with the `Veracity-Signature` header (HMAC-SHA256 of `${t}.${rawBody}`); handle events idempotently on event.id.
- Keys are valid one month at a time. A 401 with code `api_key_expired` means: ask the developer to extend the key (dashboard or MCP tool `extend_key`).
- Full reference: https://pay.veracity.se/llms-full.txtFör agenter utan MCP
- /llms.txt och /llms-full.txt – hela referensen som Markdown.
- /guides/stripe.md, nets.md, adyen.md, kustom.md, swish.md, svea.md, walley.md, qliro.md, swedbankpay.md, ledyer.md, two.md – integrationsguider per betalpartner.
- /sdk.ts – TypeScript-klienten · /listen.mjs – vidarebefordrar webhooks till localhost.
Nycklar gäller en månad i taget
Varje nyckel har ett slutdatum. Fem dagar innan kommer ett mejl med en länk som förlänger en månad från idag; samma knapp finns i Dashboard och som verktyget extend_key. En utgången nyckel ger 401 med koden api_key_expired. Nyckeln byts inte när den förlängs, och webhook-hemligheter går inte ut.
Kom igång
- Skapa ett testkonto (gratis just nu): fyll i ditt namn och din e-post – och ett företag om du vill att det ska synas för betalarna – och bekräfta e-posten med koden (den hamnar i Testinkorgen). Kontot får direkt ett handlar-ID (MID), ett konto-ID (
acct_…) och en hemlig och en publicerbar testnyckel. Fler handlarkonton skapar du i Dashboard. - Ställ in namn, logotyp, färg, moms och betalsätt under Inställningar. Nycklarna kan rullas (de gamla spärras direkt).
- Lägg till en webhook-mottagare under Webhooks och kopiera signeringshemligheten (whsec_…).
- Sätt miljövariablerna i din app:
VERACITY_PAY_BASE_URL=http://localhost:3100 # eller den publicerade adressen
VERACITY_PAY_SECRET_KEY=vp_sk_test_…
NEXT_PUBLIC_VERACITY_PAY_PUBLISHABLE_KEY=vp_pk_test_…
VERACITY_PAY_WEBHOOK_SECRET=whsec_…Lokalt kan du också skapa en handlare utan Dashboard: POST /api/dev/seed med {"email":"du@exempel.se","name":"Min app"} (avstängt i produktion).
Handlarportalen
Dashboard visar betalningar från alla betallösningar i en lista, med filter (status, betalsätt, betallösning, datum), en detaljsida per betalning med tidslinje, avslagsorsak, kund och webhook-leveranser, återbetalning (hel eller delvis, med orsak), kunder, prenumerationer och fakturor med uppsägning, produkter och priser, händelselogg, webhooks med ”Skicka igen” samt en simulerad saldo- och utbetalningsvy.
Flera kan dela ett konto. Ägaren bjuder in under Inställningar → Medlemmar; den inbjudna loggar in med sin e-post och svarar på inbjudan. Medlemmar ser och gör allt utom att hantera medlemmar, lämna över kontot och rulla nycklarna. Mejlen om nycklarna går till ägaren.
API-grunder
Autentisering
Skicka den hemliga nyckeln som Authorization: Bearer vp_sk_test_…. HTTP Basic med nyckeln som användarnamn (curl -u vp_sk_test_…:) fungerar också. Publicerbara nycklar nekas med 401.
Format
Anropen tar både JSON och Stripes form-kodning med hakparenteser (line_items[0][price]=price_…), så både fetch med JSON och Stripes egna bibliotek fungerar. Svaren är JSON med object, id-prefix (cs_test_, pi_test_, cus_, sub_, in_, price_, prod_, evt_) och Unix-tider i sekunder.
Idempotens
Skicka Idempotency-Key: <unik sträng> på POST. Samma nyckel och samma parametrar ger samma svar igen (rubriken Idempotent-Replayed: true); samma nyckel med andra parametrar ger 400 idempotency_error. Nycklarna gäller per handlare.
Fel
HTTP 402
{
"error": {
"type": "card_error", // invalid_request_error | authentication_error | idempotency_error | api_error
"code": "card_declined",
"decline_code": "insufficient_funds",
"message": "Your card has insufficient funds.",
"param": null
}
}
HTTP 401
{ "error": { "type": "authentication_error", "code": "api_key_expired",
"message": "This API key expired on 2026-12-07. … extend it …" } }Listor och paginering
Listor svarar {"object":"list","data":[…],"has_more":true,"url":"/v1/…"}, nyast först. Använd limit (1–100, standard 10), starting_after och ending_before med ett objekt-id.
Checkout Sessions
Skapa en session på servern, skicka betalaren till url. Efter godkänd betalning skickas betalaren till success_url där {CHECKOUT_SESSION_ID} byts mot sessionens id; avbryter betalaren går hen till cancel_url. Lita aldrig på redirecten – bekräfta via webhook eller genom att hämta sessionen.
Engångsbetalning
curl http://localhost:3100/v1/checkout/sessions \
-u "$VERACITY_PAY_SECRET_KEY:" \
-H "Idempotency-Key: order-1001" \
-d mode=payment \
-d "line_items[0][price_data][currency]=sek" \
-d "line_items[0][price_data][unit_amount]=25000" \
-d "line_items[0][price_data][product_data][name]=Vägavgift 2027" \
-d "line_items[0][quantity]=1" \
-d customer_email=anna@example.com \
-d client_reference_id=order-1001 \
-d "success_url=http://localhost:3000/klar?session_id={CHECKOUT_SESSION_ID}" \
-d cancel_url=http://localhost:3000/kassaPrenumeration (månad eller år)
POST /v1/checkout/sessions
{
"mode": "subscription",
"line_items": [{ "price": "price_…", "quantity": 1 }], // eller lookup_key som price
"customer_email": "kassor@granbacken.se", // eller "customer": "cus_…"
"client_reference_id": "forening_42",
"subscription_data": { "metadata": { "association_id": "42", "plan": "bas" } },
"metadata": { "association_id": "42" },
"success_url": "https://app.example/betalning/klar?session_id={CHECKOUT_SESSION_ID}",
"cancel_url": "https://app.example/planer"
}
200 OK
{
"id": "cs_test_a1B2…",
"object": "checkout.session",
"mode": "subscription",
"status": "open",
"payment_status": "unpaid",
"url": "http://localhost:3100/pay/cs_test_a1B2…",
"amount_total": 9900,
"currency": "sek",
"total_details": { "amount_discount": 0, "amount_shipping": 0, "amount_tax": 1980 },
"customer": null,
"subscription": null,
"expires_at": 1794090000,
…
}Efter betalningen: status: "complete", payment_status: "paid", customer: "cus_…", subscription: "sub_…". Hämta med ?expand[]=subscription för hela objektet. Fler parametrar: payment_method_types[] (card, swish, invoice, pay_by_bank, apple_pay, google_pay), expires_at (högst 24 h), locale, customer_creation=always. Priser kan också skapas direkt i line_items med price_data[recurring][interval]=month.
Prenumerationer
Status: incomplete → active → past_due → canceled (samt incomplete_expired, och trialing under en provperiod). Första fakturan har billing_reason: subscription_create, förnyelser subscription_cycle. Perioden ligger både på prenumerationen (current_period_start/end) och på items.data[0].
- Återkommande dragningar: första betalningen görs i kassan och godkänns med kod. Betalsättet sparas då på kunden och prenumerationen (som Stripes
setup_future_usage=off_session). Varje förnyelse dras sedan utan kod: en faktura skapas och betalas eller misslyckas. - Händelser vid förnyelse:
invoice.created,invoice.finalized,customer.subscription.updated(ny period),payment_intent.createdoch sedan antingenpayment_intent.succeeded+invoice.paid+invoice.payment_succeeded, ellerpayment_intent.payment_failed+invoice.payment_failed+customer.subscription.updatedmedstatus: past_due. - Omförsök: en misslyckad faktura dras igen dag 3, 5 och 7 efter periodens start (totalt 4 försök). Lyckas ett försök blir prenumerationen
activeigen. Misslyckas alla blir fakturanuncollectibleoch prenumerationencanceled(customer.subscription.deleted). - Utfall för nästa dragning: sätts av betalaren i Mina betalningar eller av handlaren med
POST /v1/test_helpers/subscriptions/:id/next_charge_outcome(approve,insufficient_funds,restricted_card,timeoutellerautomatic;always=truegör den bestående). Inställningen gäller en dragning och återställs sedan. Timeout = dragningen står somprocessingi 30 s och misslyckas sedan. Utan sparat betalsätt misslyckas dragningen alltid. Prenumerationsobjektet visar inställningen iveracity_next_charge. - Uppsägning: vid periodens slut med
POST /v1/subscriptions/:idochcancel_at_period_end=true(ångra medfalse), eller direkt medDELETE /v1/subscriptions/:id(ellerPOST /v1/subscriptions/:id/cancel). Båda gercustomer.subscription.deletednär prenumerationen upphör. - Tid: förnyelser körs i realtid (Vercel Cron varje minut mot
/api/cron/tick, skyddat medCRON_SECRET; varannan sekund lokalt) och när testklockan flyttas. Varje handlare har en egen klocka – flytta den i Dashboard → Prenumerationer eller med API:
curl -X POST http://localhost:3100/v1/test_helpers/advance_clock -u "$VERACITY_PAY_SECRET_KEY:" \
-d months=1 -d renewal_outcome=fail # renewal_outcome: succeed | fail (utelämna = sparat betalsätt avgör)Provperioder, planbyten och kundportal
- Provperiod:
subscription_data[trial_period_days]=14(ellertrial_end) i kassan, ellertrial_period_dayspåPOST /v1/subscriptions. Kassan sparar kortet utan att dra pengar (betalaren bekräftar med koden),amount_totalär 0, första fakturan är 0 och betald, såpayment_statusblirpaid. Prenumerationen ärtrialing;customer.subscription.trial_will_endkommer tre dagar före slutet, och sedan dras första hela fakturan (subscription_cycle).trial_end=nowvid uppdatering avslutar provperioden direkt. - Byt plan:
POST /v1/subscriptions/:idmeditems[0][id]=si_…ochitems[0][price](eller[quantity]).proration_behavior=create_prorations(standard) lägger kredit för oanvänd tid och debitering för återstående tid som väntande fakturarader på nästa faktura;always_invoicefakturerar och drar dem direkt (billing_reason: subscription_update);nonehoppar över dem. Byte mellan månad och år startar en ny period nu och faktureras direkt. - Kundportal:
POST /v1/billing_portal/sessionsmedcustomerochreturn_urlger enurldär kunden avslutar vid periodens slut, byter plan och byter kort (bekräftas med en kod på e-post).flow_dataöppnar ett steg direkt.
Rabatter och betallänkar
- Kuponger:
POST /v1/couponsmedpercent_off, elleramount_offochcurrency, ochdurationonce,repeating(medduration_in_months) ellerforever. Priserna är inklusive moms, så rabatten sänker totalen och momsen räknas om på det som är kvar. - I kassan:
discounts[0][coupon]ellerdiscounts[0][promotion_code]när kassan skapas, ellerallow_promotion_codes=trueså att betalaren kan skriva en kod på betalsidan (stora och små bokstäver spelar ingen roll). Kassan visartotal_details.amount_discount; i prenumerationsläget flyttar rabatten till prenumerationen, och varje faktura fårtotal_discount_amountsså länge kupongen gäller. - Rabattkoder:
POST /v1/promotion_codesmedcouponochcode; begränsa medmax_redemptions,expires_at,customerellerrestrictions[first_time_transaction|minimum_amount]. - Betallänkar:
POST /v1/payment_linksmedline_items[0][price]och[quantity]ger enurl. Varje besök öppnar en ny kassa medpayment_link; efteråt ser betalaren dittafter_completion-meddelande eller skickas till din adress.
Spara kort och dra senare
- Spara utan betalning: kassa med
mode=setup(inga orderrader; sessionen fårsetup_intentochpayment_status: no_payment_required), ellerPOST /v1/setup_intentsmedcustomer,payment_methodochconfirm=true. Kortet kopplas till kunden; gör det tillinvoice_settings.default_payment_methodoch dra det senare utan kund närvarande. - Spara medan kunden betalar:
setup_future_usage=off_sessionpå en PaymentIntent, ellerpayment_intent_data[setup_future_usage]=off_sessioni kassan, kopplar kortet till kunden när betalningen har gått igenom. - Dra utan kunden (sparat kort):
POST /v1/payment_intentsmedcustomer,payment_method,off_session=trueochconfirm=true. Ett kort som kräver 3-D Secure (pm_card_threeDSecureRequired, eller kortet 4000 0000 0000 3220 sparat tidigare) nekas med 402authentication_requiredoch PaymentIntent går tillbaka tillrequires_payment_method: be kunden komma tillbaka och bekräfta. - Reservera nu, dra senare:
capture_method=manualpå en PaymentIntent, ellerpayment_intent_data[capture_method]=manuali kassan. PaymentIntent stannar pårequires_capture; dra medPOST /v1/payment_intents/:id/capture(gärnaamount_to_capture, resten släpps) eller släpp med/cancel. Dashboarden har samma knappar. En reservation som inte dras släpps efter 7 dagar.
Stripe.js, Elements och Embedded Checkout (emulerat)
Veracity Pay har ett emulerat Stripe.js på https://pay.veracity.se/stripe/v3/ (och /stripe/<release>/stripe.js, som js.stripe.com). Det är skrivet utifrån Stripes publika dokumentation, inte Stripes kod, och pratar bara med Veracity Pay. Det har Payment Element (kort, och Swish för betalningar i SEK), kortfälten (card, cardNumber, cardExpiry, cardCvc), Address Element och Embedded Checkout. Kortuppgifterna skrivs i rutor från Veracity Pay och når aldrig din sida. Det är en emulering: små justeringar kan behövas när du byter till Stripe, och alla kända finns i rutan under Endpoints. Prova i demobutiken.
<!-- Instead of https://js.stripe.com/v3/ -->
<script src="https://pay.veracity.se/stripe/v3/"></script>
const stripe = Stripe(process.env.NEXT_PUBLIC_VERACITY_PAY_PUBLISHABLE_KEY); // vp_pk_test_…
const elements = stripe.elements({ clientSecret }); // from POST /v1/payment_intents
elements.create("payment").mount("#payment-element");
const { error } = await stripe.confirmPayment({
elements,
confirmParams: { return_url: "https://your-shop.example/done" },
});- @stripe/stripe-js:
loadStripeanvänderwindow.Stripeom det redan finns, så ladda skriptet ovan först och synkront, i<head>(Next.js: rotlayoutens<head>, ellernext/scriptmedstrategy="beforeInteractive"). Anropa sedanloadStripe(pk)som vanligt. Finns skriptet inte än hämtar loadStripe det riktiga Stripe.js;window.Stripe.__veracityvisar vilket du fick. - @stripe/react-stripe-js: oförändrat –
<Elements stripe={stripePromise} options={{ clientSecret }}>,<PaymentElement />,useStripe(),useElements(),<EmbeddedCheckoutProvider>. - Servern: skapa PaymentIntent med
automatic_payment_methods[enabled]=trueellerpayment_method_types[]. Uppskjutet:elements({ mode, amount, currency }), sedanelements.submit(), sedan skapa PaymentIntent och bekräfta. Ellerstripe.createConfirmationToken({ elements })ochPOST /v1/payment_intentsmedconfirmation_tokenochconfirm=true. - 3-D Secure och Swish: 4000 0000 0000 3220 och Swish öppnar en sida för godkännande i ett fönster; PaymentIntent lämnar
requires_actionnär betalaren godkänner. - Embedded Checkout:
POST /v1/checkout/sessionsmedui_mode=embeddedochreturn_url(ellerredirect_on_completion=never), sedanstripe.initEmbeddedCheckout({ fetchClientSecret, onComplete })och.mount("#checkout"). - Gå live: ladda
https://js.stripe.com/v3/(eller låt loadStripe göra det) och använd Stripes nycklarpk_ochsk_.
Webhooks
Varje händelse POST:as som JSON (ett event-objekt) till alla aktiva mottagare. Rubriken Veracity-Signature: t=<unix>,v1=<hex> innehåller en HMAC-SHA256 av `${t}.${råKropp}` med mottagarens whsec_…-hemlighet. Samma värde skickas också som Stripe-Signature, så stripe.webhooks.constructEvent fungerar utan ändringar. Avvisa anrop äldre än 5 minuter. Svara 2xx snabbt; annat räknas som fel.
Omförsök: efter 10 s, 30 s, 2 min, 10 min, 1 h, 6 h, 24 h – totalt 8 försök. Varje försök (statuskod, svar, tid) syns under Dashboard → Webhooks där du också kan skicka igen. Händelser kan komma mer än en gång och i annan ordning – gör hanteringen idempotent på event.id.
| Händelse | När |
|---|---|
checkout.session.completed | Kassan är slutförd. payment_status = paid, eller unpaid om betalningen behandlas (asynkron). |
checkout.session.async_payment_succeeded / _failed | En väntande betalning från kassan avgjordes. |
checkout.session.expired | Kassan gick ut (tid, timeout-utfall eller /expire). |
payment_intent.created / requires_action / processing | Betalningens livscykel. |
payment_intent.succeeded | Pengarna (låtsas) dragna. |
payment_intent.payment_failed | Nekad. last_payment_error.code/decline_code anger orsak. |
payment_intent.canceled | Avbruten eller utgången (status canceled/expired). |
payment_intent.amount_capturable_updated / charge.captured / charge.expired | Manuell capture: beloppet är reserverat, draget, eller släppt efter 7 dagar. |
setup_intent.succeeded / setup_failed / canceled | Ett kort sparat utan betalning (setup-läge, provperiod, API). |
payment_method.attached / detached | Ett sparat kort kopplat till eller från en kund. |
charge.succeeded / charge.failed / charge.refunded | Charge-objektet; charge.refunded vid hel och delvis återbetalning. |
refund.created | Ny återbetalning. |
customer.created / updated / deleted | Kunder. |
customer.subscription.created | Ny prenumeration (status active, eller incomplete vid asynkron första betalning). |
customer.subscription.updated | Ny period, past_due, active igen, cancel_at_period_end ändrad. previous_attributes visar vad som ändrats. |
customer.subscription.trial_will_end | Tre dagar innan provperioden slutar. |
customer.subscription.deleted | Prenumerationen är avslutad (status canceled). |
invoiceitem.created / updated | Prorateringar vid planbyte, och när de hamnar på en faktura. |
billing_portal.session.created | En kundportal-länk skapades. |
coupon.created / updated / deleted | Kuponger; updated även när times_redeemed ökar. |
promotion_code.created / updated | Rabattkoder. |
customer.discount.created / deleted | En kupong läggs på en prenumeration, eller slutar gälla (tiden ute, använd en gång, borttagen). |
payment_link.created / updated | Betallänkar. |
invoice.created / finalized | Faktura för en period. |
invoice.paid / invoice.payment_succeeded | Perioden är betald. billing_reason = subscription_create, subscription_cycle eller subscription_update. |
invoice.payment_failed | Förnyelsen kunde inte dras. Prenumerationen blir past_due. |
invoice.marked_uncollectible | Alla fyra försöken misslyckades; prenumerationen avslutas (customer.subscription.deleted). |
invoice.voided | Asynkron första betalning misslyckades (prenumerationen blir incomplete_expired). |
Verifiera signaturen (Node)
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyVeracitySignature(rawBody: string, header: string | null, secret: string) {
if (!header) return false;
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const given = Buffer.from(parts.v1 ?? "");
return given.length === expected.length && timingSafeEqual(given, Buffer.from(expected));
}Testläge och utfall
Kassan följer respektive leverantörs ordning: Stripe på en sida med e-posten i formuläret, Nets med dina uppgifter (e-post, postnummer, mobil) först, Adyen med betalsätten först och Kustom med e-post och postnummer, sedan namn och adress och till sist Slutför köp. Varje verifiering som det riktiga betalsättet skulle kräva – 3-D Secure för kort, Swish-appen, BankID för faktura och banköverföring, Face ID för plånböcker – görs med en sexsiffrig kod till betalarens e-post (gäller 10 minuter, tål 5 felaktiga försök).Avbryt verifieringen nekar betalningen med authentication_failed. Den fällbara panelen Testläge – välj utfall har Automatiskt, Godkänn, Neka (otillräckligt saldo, kortet nekat, kortet har gått ut, misstänkt bedrägeri, kreditprövning nekad, verifieringen misslyckas), Väntande (lyckas eller misslyckas efter N sekunder, med webhook senare), Timeout (betalaren bekräftar aldrig – sessionen löper ut efter N sekunder) och Leverantörsfel (HTTP 500). Faktura frågar efter personnummer; använd testnumret 19121212-1212.
Ordning när utfallet bestäms: valet i panelen → magiskt kortnummer → metadata[veracity_test_outcome] → magiskt belopp → godkänn.
Magiska kortnummer
| Kortnummer | Utfall |
|---|---|
4242 4242 4242 4242 | Visa – godkänns |
5555 5555 5555 4444 | Mastercard – godkänns |
4000 0000 0000 0002 | Nekas: kortet nekat |
4000 0000 0000 9995 | Nekas: otillräckligt saldo |
4000 0000 0000 0069 | Nekas: kortet har gått ut |
4100 0000 0000 0019 | Nekas: misstänkt bedrägeri |
4000 0000 0000 3220 | Kräver 3-D Secure-steg |
4000 0000 0000 0150 | Väntande, lyckas efter 5 s |
4000 0000 0000 0168 | Väntande, misslyckas efter 5 s |
4000 0000 0000 0119 | Leverantörsfel (500) |
4000 0000 0000 0184 | Timeout – sessionen löper ut |
4000 0000 0000 0341 | Godkänns, men förnyelser misslyckas |
Alla andra nummer (12–19 siffror) godkänns; märket visas utifrån prefixet (4 = Visa, 51–55/2221–2720 = Mastercard, 34/37 = Amex). Giltighetstiden måste ligga i framtiden.
Magiska belopp
Slutar totalbeloppet på något av dessa ören (minst 1 kr) styr det utfallet, oavsett betalsätt:
| Belopp | Utfall |
|---|---|
…,01 kr (t.ex. 10001) | Nekas: kortet nekat |
…,02 kr (t.ex. 10002) | Nekas: otillräckligt saldo |
…,03 kr (t.ex. 10003) | Nekas: kortet har gått ut |
…,04 kr (t.ex. 10004) | Nekas: misstänkt bedrägeri |
…,05 kr (t.ex. 10005) | Nekas: kreditprövning (faktura) |
…,06 kr (t.ex. 10006) | Extra verifieringssteg |
…,07 kr (t.ex. 10007) | Väntande, lyckas efter 5 s |
…,08 kr (t.ex. 10008) | Väntande, misslyckas efter 5 s |
…,09 kr (t.ex. 10009) | Timeout – sessionen löper ut |
Utan UI: metadata och testhjälpare
Sätt metadata[veracity_test_outcome] på sessionen (eller PaymentIntent) till någon av: approve, decline_card_declined, decline_insufficient_funds, decline_expired_card, decline_fraudulent, decline_credit_rejected, challenge, pending_succeed, pending_fail, timeout, provider_error. metadata[veracity_test_delay] anger sekunder för pending/timeout. För automatiska tester kan en öppen session slutföras direkt:
curl -X POST http://localhost:3100/v1/test_helpers/checkout/sessions/cs_test_…/complete \
-u "$VERACITY_PAY_SECRET_KEY:" \
-d outcome=approve # eller decline_insufficient_funds, pending_succeed, timeout …
# valfritt: -d payment_method_type=invoice -d card_number=4000000000000341 -d email=… -d delay=3Testbetalsätt för POST /v1/payment_intents (confirm=true) och POST /v1/customers (payment_method): pm_card_visa, pm_card_mastercard, pm_card_chargeDeclined, pm_card_chargeDeclinedInsufficientFunds, pm_card_chargeDeclinedExpiredCard, pm_card_chargeDeclinedFraudulent, pm_card_threeDSecureRequired, pm_card_pendingSucceed, pm_card_pendingFail, pm_card_processingError, pm_card_renewalsFail. Kräver betalningen ett extra steg svarar API:t med status: requires_action och next_action.redirect_to_url.url.
Sparat betalsätt med kortet 4000 0000 0000 0341 (eller subscription_data[metadata][veracity_renewal_outcome]=fail) gör att alla förnyelser misslyckas.
Endpoints (Stripe-variant)
| Metod | Sökväg | Beskrivning |
|---|---|---|
POST | /v1/checkout/sessions | Skapa kassa (mode payment | subscription | setup; discounts[0][coupon|promotion_code] eller allow_promotion_codes) |
GET | /v1/checkout/sessions | Lista (payment_intent, customer, subscription, status, payment_link) |
GET | /v1/checkout/sessions/:id | Hämta (expand[]: line_items, payment_intent, setup_intent, customer, subscription, invoice) |
GET | /v1/checkout/sessions/:id/line_items | Orderrader |
POST | /v1/checkout/sessions/:id/expire | Avbryt en öppen kassa |
POST | /v1/payment_intents | Skapa (confirm=true + payment_method=pm_card_… för direktbetalning; off_session=true utan kunden; setup_future_usage) |
GET | /v1/payment_intents[/:id] | Lista / hämta |
POST | /v1/payment_intents/:id | Uppdatera metadata, description, amount |
POST | /v1/payment_intents/:id/confirm | Bekräfta med testbetalsätt |
POST | /v1/payment_intents/:id/cancel | Avbryt (släpper även en reservation) |
POST | /v1/payment_intents/:id/capture | Dra en reservation (capture_method=manual), helt eller delvis: amount_to_capture |
POST/GET | /v1/setup_intents[/:id] | Spara ett kort utan betalning (customer, payment_method, confirm) |
POST | /v1/setup_intents/:id/confirm | cancel | Bekräfta / avbryt |
GET | /v1/payment_methods/:id, /v1/customers/:id/payment_methods | Sparade kort |
POST | /v1/payment_methods/:id/attach | detach | Koppla till / från en kund |
POST | /v1/payment_methods | Betalsätt från kortuppgifter (card, swish); Stripe.js gör det med den publicerbara nyckeln |
POST | /v1/confirmation_tokens | ConfirmationToken från Elements (publicerbar nyckel) |
GET | /v1/confirmation_tokens/:id | Hämta en ConfirmationToken |
POST | /v1/test_helpers/confirmation_tokens | Skapa en ConfirmationToken på servern, för tester |
GET / POST | /v1/payment_intents/:id, …/confirm, /v1/setup_intents/:id, …/confirm | Även med publicerbar nyckel och client_secret, som Stripe.js anropar dem |
GET | /v1/charges/:id | Hämta charge (ch_test_…) |
POST | /v1/refunds | Återbetala helt eller delvis (payment_intent, amount, reason) |
GET | /v1/refunds[/:id] | Lista / hämta |
POST/GET/DELETE | /v1/customers[/:id] | Kunder (email, name, phone, address, metadata, payment_method) |
POST/GET | /v1/products[/:id] | Produkter (name, description, default_price_data) |
POST/GET | /v1/prices[/:id] | Priser (unit_amount, currency, recurring[interval]=month|year, lookup_key) |
POST | /v1/subscriptions | Skapa för kund med sparat betalsätt (trial_period_days, trial_end, default_payment_method, discounts[0][coupon|promotion_code]) |
GET | /v1/subscriptions[/:id] | Lista (customer, price, status) / hämta (expand[]=latest_invoice) |
POST | /v1/subscriptions/:id | cancel_at_period_end, metadata, default_payment_method, trial_end, discounts ("" tar bort); byt plan: items[0][id] + items[0][price|quantity], proration_behavior |
DELETE | /v1/subscriptions/:id | Avsluta direkt (även POST …/cancel) |
GET | /v1/invoices[/:id] | Fakturor (customer, subscription, status) |
POST/GET/DELETE | /v1/invoiceitems[/:id] | Fakturarader till nästa faktura, bl.a. prorateringar (customer, pending=true) |
POST | /v1/billing_portal/sessions | Kundportal för en kund (customer, return_url, flow_data) |
POST/GET/DELETE | /v1/coupons[/:id] | Kuponger (percent_off eller amount_off + currency, duration once|repeating|forever, duration_in_months, max_redemptions, redeem_by, applies_to[products]) |
POST/GET | /v1/promotion_codes[/:id] | Rabattkoder för en kupong (code, max_redemptions, expires_at, customer, restrictions[first_time_transaction|minimum_amount]) |
DELETE | /v1/subscriptions/:id/discount | Ta bort prenumerationens rabatt |
POST/GET | /v1/payment_links[/:id] | Betallänkar (line_items, after_completion, allow_promotion_codes, subscription_data, restrictions, active) |
GET | /v1/payment_links/:id/line_items | Betallänkens orderrader |
GET | /v1/events[/:id] | Händelselogg (type) |
POST/GET/DELETE | /v1/webhook_endpoints[/:id] | Webhook-mottagare (svaret innehåller secret) |
POST | /v1/test_helpers/advance_clock | Flytta testklockan: days, months, seconds eller frozen_time; renewal_outcome |
GET | /v1/test_helpers/clock | Handlarens aktuella testtid |
POST | /v1/test_helpers/subscriptions/:id/next_charge_outcome | Utfall för nästa automatiska dragning: outcome = automatic | approve | insufficient_funds | restricted_card | timeout, always = true|false |
POST | /v1/test_helpers/checkout/sessions/:id/complete | Slutför en kassa utan UI (outcome, payment_method_type, card_number, email) |
Avvikelser från Stripe: PaymentIntent har även statusarna failed och expired; prenumerationer har ett pris per prenumeration; intervall är month eller year. Hela listan över avvikelser finns i rutan överst i avsnittet.
Integrera i Next.js
Kopiera lib/sdk.ts från Veracity Pay-repot till din app (inga beroenden). Den ger en typad klient och constructEvent för webhooks.
// app/api/billing/checkout/route.ts
import { createVeracityPay } from "@/lib/veracity-pay";
const vp = createVeracityPay({
secretKey: process.env.VERACITY_PAY_SECRET_KEY!,
baseUrl: process.env.VERACITY_PAY_BASE_URL!,
});
export async function POST(req: Request) {
const { associationId, priceId, email } = await req.json();
const session = await vp.checkout.sessions.create(
{
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
customer_email: email,
client_reference_id: associationId,
subscription_data: { metadata: { association_id: associationId } },
success_url: `${process.env.APP_URL}/betalning/klar?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/planer`,
},
{ idempotencyKey: `checkout-${associationId}-${priceId}` },
);
return Response.json({ url: session.url });
}// app/api/webhooks/veracity/route.ts
import { constructEvent, type Subscription } from "@/lib/veracity-pay";
export async function POST(req: Request) {
const raw = await req.text();
let event;
try {
event = await constructEvent(raw, req.headers.get("veracity-signature"), process.env.VERACITY_PAY_WEBHOOK_SECRET!);
} catch {
return new Response("invalid signature", { status: 400 });
}
switch (event.type) {
case "checkout.session.completed": // koppla customer + subscription till föreningen
case "customer.subscription.created":
case "customer.subscription.updated":
case "customer.subscription.deleted": {
const sub = event.data.object as unknown as Subscription;
// spara sub.id, sub.status, sub.current_period_end, sub.items.data[0].price.id …
break;
}
case "invoice.paid":
case "invoice.payment_failed":
break;
}
return Response.json({ received: true });
}Nets Easy-variant
Bas-URL /nets/v1, autentisering med Authorization: vp_sk_test_… (utan Bearer, som hos Nets). Både integrationType: "HostedPaymentPage" och "EmbeddedCheckout" (standard) stöds.
POST /nets/v1/payments–order.items[](grossTotalAmount, taxRate 2500 = 25 %, taxAmount),order.amount(måste vara summan),checkout.returnUrl,cancelUrl,termsUrl,charge,notifications.webHooks[]→{ paymentId, hostedPaymentPageUrl }- Returen går till
returnUrl?paymentid=…. - Inbäddat:
checkout.url(din kassasida) i stället för returnUrl →{ paymentId }. Ladda/nets/v1/checkout.jspå sidan och körnew Dibs.Checkout({ checkoutKey, paymentId, containerId: "dibs-complete-checkout", language: "sv-SE" }); checkout key är din publicerbara nyckel (vp_pk_test_…).checkout.on("payment-completed", (r) => …)anropas när betalningen är klar. GET /nets/v1/payments/:paymentId–payment.summary(reservedAmount, chargedAmount, refundedAmount, cancelledAmount),paymentDetails,orderDetails,charges,refunds.POST …/payments/:id/charges→chargeId;POST /nets/v1/charges/:chargeId/refundseller…/payments/:id/refunds→refundId;POST …/payments/:id/cancels{ amount, orderItems? } → 204 makulerar hela eller en del av det som är reserverat och inte dragits (cancelledAmount, ingen återbetalning);PUT …/payments/:id/terminateavslutar en obetald kassa.PUT …/payments/:id/orderitemsändrar ordern medan betalaren är i kassan;PUT …/referenceinformation{ checkoutUrl, reference } ochPUT …/myreference{ myReference } ändrar referenserna.- Prenumerationer:
subscription: { endDate, interval }och/ellerunscheduledSubscription: { create: true }på betalningen (bara kort). SedanGET /nets/v1/subscriptions/:id,POST …/subscriptions/:id/charges{ order } →{ paymentId, chargeId }(402 om den nekas),GET …/subscriptions/:id/charges/statusmed samma Idempotency-Key, bulkPOST /nets/v1/subscriptions/charges→bulkIdochGET …/subscriptions/charges/:bulkId(Processing tills dragningarna körts, sedan Done); samma under/nets/v1/unscheduledsubscriptions. Ett belopp som slutar på ,02 nekas.POST /nets/v1/subscriptions/verifications→bulkIdkontrollerar korten innan du drar.order.amount: 0med ett avtal registrerar kortet utan reservation. - Webhooks skickas till URL:erna i
notifications.webHooksmed derasauthorization-värde iAuthorization: payment.created, payment.reservation.created.v2, payment.checkout.completed, payment.reservation.failed, payment.charge.created.v2, payment.charge.failed, payment.refund.initiated(.v2), payment.refund.completed, payment.cancel.created, payment.cancel.failed. Kropp:{ id, merchantId, timestamp, event, data: { paymentId, … } }.
Adyen-variant
Bas-URL /adyen/v72 (v68–v72 accepteras, även under live-värdens prefix /checkout/v72; v72 validerar värden strängare, som hos Adyen), autentisering med X-API-Key: vp_sk_test_… (eller Basic-auth med nyckeln som lösenord). Recurring API ligger på /adyen/pal/servlet/Recurring/v68.
POST /adyen/v72/sessions– merchantAccount, amount {value, currency}, reference, returnUrl,mode: "hosted", lineItems (amountIncludingTax, taxPercentage) →{ id: "CS…", url, sessionData, expiresAt, … }. Returen går tillreturnUrl?sessionId=…&sessionResult=….GET /adyen/v72/sessions/:id→status: active, completed, paymentPending, refused, canceled, expired.POST /adyen/v72/payments– paymentMethod{ type: "scheme", encryptedCardNumber: "test_4111111111111111", … }(eller number) →resultCodeAuthorised, Refused, Pending, RedirectShopper (medaction.url) ochpspReference. Magiska kortnummer gäller.POST /adyen/v72/payments/details{ details: { redirectResult } } efter en redirect (kunden kommer tillbaka tillreturnUrl?redirectResult=…);POST /adyen/v72/paymentMethodslistar betalsätt och sparade kort.- Ändringar svarar
status: "received"och utfallet kommer som notifiering:…/payments/:pspReference/captures(medadditionalData.manualCapture: "true"på betalningen),…/cancels(en auktorisation som inte dragits, eller det som är kvar efter delvisa dragningar),…/reversals(cancel eller återbetalning) och…/refunds(upp till det som dragits). …/payments/:pspReference/amountUpdatesjusterar en förauktorisering (additionalData.authorisationType: "PreAuth") → AUTHORISATION_ADJUSTMENT; i v72 medadjustAuthorisationDatakommer svaretauthorisedellerrefuseddirekt.POST /adyen/v72/cancelsavbryter med din egenpaymentReference→ TECHNICAL_CANCEL.- Pay by Link:
POST /adyen/v72/paymentLinks→{ id: "PL…", url, status: "active" }(engångslänk, upp till 70 dagar);GETochPATCH …/paymentLinks/:id(status: "expired"). AUTHORISATION-notifieringen haradditionalData.paymentLinkId. - Tokens:
storePaymentMethod: true,shopperReferenceochrecurringProcessingModelpå /payments (ellerstorePaymentMethodModepå /sessions) →additionalData["tokenization.storedPaymentMethodId"]; dra medpaymentMethod: { type: "scheme", storedPaymentMethodId }ochshopperInteraction: "ContAuth"(även medrecurringProcessingModel: "UnscheduledCardOnFile"; aldrig med 3-D Secure, mednetworkTxReference).POST(spara utan betalning),GETochDELETE …/storedPaymentMethods; en nollbeloppsauktorisering (amount.value: 0) sparar också kortet. Tokeniseringswebhooks (recurring.token.created, .alreadyExisting, .disabled) går till Adyen-endpointen, signerade i headernHmacSignature. - Notifieringar: lägg till en mottagare med formatet Adyen i Dashboard (eller
veracity_format=adyenpå/v1/webhook_endpoints). Standardformatet{ live: "false", notificationItems: [{ NotificationRequestItem: { eventCode, success, pspReference, … additionalData.hmacSignature } }] }med AUTHORISATION, CAPTURE, CANCELLATION, REFUND, CANCEL_OR_REFUND, AUTHORISATION_ADJUSTMENT, TECHNICAL_CANCEL, RECURRING_CONTRACT och OFFER_CLOSED. HMAC-nyckeln visas på mottagaren; signaturen räknas som hos Adyen (pspReference:originalReference:merchantAccountCode:merchantReference:value:currency:eventCode:success, HMAC-SHA256 med hex-nyckeln, base64).
Kustom-variant (tidigare Klarna Checkout)
Bas-URL /kustom, HTTP Basic med valfritt användarnamn (t.ex. handlar-id) och den hemliga nyckeln som lösenord.
POST /kustom/checkout/v3/orders– purchase_country, purchase_currency, locale, order_amount, order_tax_amount, order_lines (unit_price, tax_rate, total_amount, total_tax_amount), merchant_urls (terms, checkout, confirmation, push med{checkout.order.id}) →order_id,status: checkout_incomplete,html_snippet(en iframe med kassan, och dess skript).GET /kustom/checkout/v3/orders/:id→checkout_completeefter köpet; utgången order ger 404.POST /kustom/checkout/v3/orders/:iduppdaterar varukorgen före köpet (efteråt 403 READ_ONLY_ORDER).- Push: POST utan kropp till merchant_urls.push några sekunder efter köpet; omförsök vid fel.
- Order management:
GET /kustom/ordermanagement/v1/orders/:id,POST …/acknowledge,…/captures(captured_amount),…/refunds(refunded_amount, upp till det som dragits),…/cancel(släpper auktorisationen),…/release-remaining-authorization(släpper det som inte dragits),…/extend-authorization-time(28 dagar till),PATCH …/merchant-references,PATCH …/authorization(nytt belopp och nya orderrader, t.ex. vid merförsäljning),PATCH …/customer-details(leveransadress),POST …/shipping-info,GET …/capturesochPOST …/captures/:id/shipping-info. Med headernKustom-Idempotency-Key(ellerKlarna-Idempotency-Key) får ett omförsök inom 24 timmar samma svar som första gången. - Kassans JavaScript: snippeten installerar
window._klarnaCheckout(api => …)medapi.suspend(),api.resume()ochapi.on({ load, change, shipping_option_change, order_total_change, validation_callback, … }).POST /kustom/checkout/v3/orders/:id/abortstänger en obetald order. - Hosted Payment Page:
POST /kustom/hpp/v1/sessionsmedpayment_session_urltill ordern →redirect_url,qr_code_url,session_url,distribution_url. Statusanrop tillmerchant_urls.status_update(WAITING, IN_PROGRESS, COMPLETED, FAILED, BACK, DISABLED);DELETEstänger sessionen. - Anrop under köpet:
merchant_urls.validationfår ordern när betalaren trycker på köp (200 godkänner, 303 med Location eller 400 med error_text stoppar köpet);shipping_optionsblir enshipping_fee-rad, eller går tillshipping_option_update;address_updateochcountry_changekan svara med en ny varukorg eller nya fraktalternativ. - Återkommande:
recurring: truepå ordern →recurring_tokenpå den genomförda ordern.GET /kustom/customer-token/v1/tokens/:token,POST …/tokens/:token/order→{ order_id, fraud_status, authorized_payment_method }(403 PAYMENT_METHOD_FAILED om den nekas),PATCH …/tokens/:token/status{ status: "CANCELLED" }.
Swish Handel-variant
Swish Commerce API: betalningsförfrågningar (e-handel och m-handel), återbetalningar, utbetalningar, callbacks och återkommande betalningar – utan certifikat. Bas-URL /swish. Fullständig steg-för-steg-guide med anrop, callbacks och testutfall: /guides/swish.md.
Utbetalningar (Payouts API v1): POST /swish-cpcapi/api/v1/payouts med payload, signature och callbackUrl betalar ut till ett Swish-nummer och personnummer. Callbacks DEBITED och sedan PAID; GET /swish-cpcapi/api/v1/payouts/{id} läser utbetalningen. Signaturen kontrolleras bara till formen. Utbetalningarna syns under Saldo i Dashboard.
Svea Checkout-variant
Checkout API och Payment Admin med Svea-signerade anrop, validering, företagsköp, kampanjer, sparade kort, push och leverans/kreditering. Bas-URL /svea. Fullständig steg-för-steg-guide med anrop, callbacks och testutfall: /guides/svea.md.
Återkommande ordrar: skapa första ordern med Recurring: true. När den är Final har den ett RecurringToken, och POST /api/tokens/{token}/orders skapar sedan ordrar utan köparen (Final direkt, eller felkod 910 om betalningen nekas). GET och PATCH /api/tokens/{token} läser och avslutar token. POST /api/tokens/{token}/payment-methods ger ett snippet där betalaren byter kort; token behåller sitt id.
Företagsköp: kassan låter betalaren välja privatperson eller företag. PresetValues IsCompany (med NationalId och CompanyName) förväljer företag, och IsReadonly låser valet. Ordern har då Customer.IsCompany, NationalId (organisationsnumret), CustomerReference och företagsnamnet i FullName. Delbetalning: GET /api/util/GetAvailablePartPaymentCampaigns?isCompany=false&amount=… listar kampanjerna, kassan visar dem (ActivePartPaymentCampaigns, PromotedPartPaymentCampaign först) och den valda blir Payment.CampaignCode. Testa med organisationsnumret 556677-8899.
Walley Checkout-variant
Checkout med OAuth2, validering, företagsköp, kundtoken för prenumerationer, notifieringar och orderhantering (aktivera, återbetala, makulera). Bas-URL /walley. Fullständig steg-för-steg-guide med anrop, callbacks och testutfall: /guides/walley.md.
Prenumerationer: profileName "Subscription" ger efter köpet data.order.customerToken. POST /purchase/authorizations debiterar det senare (202 och webhook walley:order:created eller walley:authorization:failed, eller 200 med preferSynchronous). /purchase/customer-tokens/{id} läser, ändrar och avslutar token. profileName "TokenRegistration" (eller Subscription utan varukorg) sparar ett kort utan köp; token finns sedan i data.order.customerToken.
Företagsköp: B2B-butiken (salesSegment "B2B", eller ditt butiks-id + 1) säljer bara till företag. businessCustomerPrefill och businessCustomerPreset fyller i organisationsnummer, köpare och invoiceReference. GET /checkouts/{privateId} har då customerType BusinessCustomer och data.businessCustomer. Testa med organisationsnumret 556677-8899.
Qliro One-variant
Merchant API med Qliro-signerade anrop, validering, företagsköp, sparade kort, statuspush och orderhantering. Bas-URL /qliro. Fullständig steg-för-steg-guide med anrop, callbacks och testutfall: /guides/qliro.md.
Prenumerationer: med MerchantSavedCreditCardPushUrl sparas kortet vid ett kortköp och dess Id skickas dit. Admin API:ts MerchantPayment debiterar sedan kortet (eller en Qliro-faktura) utan kunden, och MerchantPayment/Retry försöker igen efter ett nej. RemoveMerchantSavedCreditCard tar bort kortet. CreateMerchantSavedCreditCard ger ett kortformulär (OrderForRegistrationTokenHtmlSnippet) som sparar ett kort utan köp och skickar det på samma sätt.
Företagsköp: utan EnforcedJuridicalType väljer betalaren privatperson eller företag; "Company" tillåter bara företag och "Physical" bara privatpersoner. CustomerInformation med JuridicalType "Company" och PersonalNumber fyller i organisationsnumret. Ordern har då Customer.JuridicalType Company, organisationsnumret som PersonalNumber och CompanyName i adressen. Testa med organisationsnumret 556677-8899.
Swedbank Pay-variant
Checkout v3 (Payment Menu): betalordrar, redirect och Seamless View, callbacks, capture, cancel, reversal, Verify, one-click och återkommande betalningar med token. Bas-URL /swedbankpay. Fullständig steg-för-steg-guide med anrop, callbacks och testutfall: /guides/swedbankpay.md.
Sparade kort: Verify sparar ett kort utan att reservera något, generateRecurrenceToken, generateUnscheduledToken och generatePaymentToken ger token, och Recur och UnscheduledPurchase debiterar utan betalaren. Med generatePaymentToken och samma payerReference visar betalmenyn betalarens sparade kort (one-click). payerownedtokens listar och raderar en betalares token, och abort-paymentattempt avbryter ett pågående betalningsförsök.
Ledyer Checkout-variant
B2B-kassa för företag: OAuth2, ordersessioner, inbäddad kassa med bootstrap.js, kreditprövning med Ledyers testföretag, företagssök, validering, notifieringar och orderhantering per delorder (debitera, kreditera, makulera). Bas-URL /ledyer. Fullständig steg-för-steg-guide med anrop, callbacks och testutfall: /guides/ledyer.md.
Orderhantering: GET /v1/orders/{orderId} visar delordrarna (uncaptured, captured, refunded) med ledgerId och availableActions. POST …/acknowledge, …/capture, …/partialcapture, …/refund/{ledgerId} och …/partialrefund/{ledgerId} (kreditfaktura per debitering), …/extendexpiry, …/reference, POST /v1/orders/{orderId} (ändra odebiterade rader) och DELETE /v1/orders/{orderId} (makulera det som inte debiterats). Med headern Idempotency-Key får ett omförsök samma svar. Notifieringarna (com.ledyer.order.create, ready_for_capture, part_capture, full_capture, part_refund, full_refund, cancel) innehåller bara id:n, är osignerade och skickas till settings.urls.notification med Ledyers omförsöksschema.
Bara företag: kassan frågar efter organisationsnummer, företagsnamn och referens, och Ledyer kreditprövar företaget när köparen trycker på köp. Testföretag: High Limits AB 575757-5757 godkänns och Credit Declined AB 559999-0040 nekas (Telinekataja Oy 2222222-9 och Saunakaverit Oy 3333333-8 finns i företagssöket). Enskilda firmor kan inte handla, som i Ledyers testmiljö. GET /v1/headless/search/companies?q=…&country=SE söker utan inloggning. customer.companyId på sessionen fyller i organisationsnumret.
Two-variant
B2B-faktura (köp nu, betala senare för företag): Company API, order intent, ordrar som köparen verifierar, bekräfta, leverera helt eller delvis (barnordrar), kreditera och Svix-signerade webhooks. Bas-URL /two. Fullständig steg-för-steg-guide med anrop, callbacks och testutfall: /guides/two.md.
Bara företag: köparen identifieras med Company API (GET /companies/v2/company?country=SE&q=…), POST /v1/order_intent ger ett kreditbesked i förväg och POST /v1/order ger status APPROVED eller REJECTED och state UNVERIFIED med payment_url, där köparen verifierar med en e-postad kod. Sedan confirm, fulfillments (helt eller delvis, med barnordrar), cancel och refund mot ursprungsordern; invoice_details ger fakturanummer och förfallodag från terms.duration_days. Twos sandlådeföretag finns med sina gränser och avslag, till exempel VOLUMENTAL AB 556912-4141 (godkänns) och Bad Brothers Band AB 802536-7528 (ingen kredit). Webhooks är CloudEvents signerade som Svix signerar (svix-id, svix-timestamp, svix-signature), och overifierade ordrar annulleras efter 48 timmar.
Simulerade driftstörningar
Under Störningar i Dashboard (eller med MCP-verktyget simulate_outage) slår du på en störning för en betalpartner eller alla, en viss tid eller tills vidare. Den gäller bara ditt konto och svarar i varje partners eget felformat. Testhjälparna fungerar som vanligt, så ett test kan fortfarande slutföra betalningar.
- Ogiltig nyckel (
invalid_key): Alla anrop svarar 401 som om nyckeln vore fel. - Utgången nyckel (
expired_key): Alla anrop svarar 401 som om nyckeln hade gått ut. - Långsamma svar (
slow): Varje anrop fördröjs innan det besvaras som vanligt. - Timeout (
timeout): Anropen hänger i 28 sekunder och får sedan 504 Gateway Timeout. - Tjänsten nere (
offline): Alla anrop får 503 Service Unavailable, som från en lastbalanserare. - Serverfel (
server_error): Alla anrop får 500 i leverantörens eget format. - Rate limit (
rate_limited): Alla anrop får 429 med Retry-After: 30. - Ojämn drift (
flaky): En andel av anropen får 500 eller 503, resten fungerar. - Pausade webhooks (
paused): leveranser och notifieringar väntar och skickas när pausen är slut.
Mina betalningar (kund)
På /konto loggar betalaren in med e-post och sexsiffrig kod (10 minuter, 5 försök per kod) och ser – hos alla handlare:
- Betalningshistorik med kvitton, där varje köp visar handlaren och betallösningen (Stripe, Nets Easy, Swish …), med filter för båda.
- Obetalda köp med faktura och delbetalning (”Betala om 30 dagar” eller tre månadsdelar) med förfallodag. Delarna dras automatiskt på förfallodagen (handlarens klocka) och kan betalas direkt med kod. Handlaren har redan fått hela beloppet; en återbetalning minskar skulden.
- Aktiva prenumerationer med nästa dragningsdatum och belopp, och knappar för att säga upp (vid periodens slut) eller avsluta direkt.
- En testinställning per prenumeration och per kommande delbetalning: Vid nästa dragning: Godkänn / Neka (otillräckligt saldo) / Neka (kort spärrat) / Timeout, med ”Alltid” för att behålla valet.
- Sparade betalsätt per handlare. Tas ett bort misslyckas nästa förnyelse hos den handlaren.
E-post och Testinkorg
Allt som ”skickas” – betalkoder, inloggningskoder, kvitton, fakturor, återbetalningar – hamnar i Testinkorgen, sökbar på mottagare. Om RESEND_API_KEY och EMAIL_FROM är satta skickas meddelandena även på riktigt via Resend. Med VERACITY_INBOX_KEY låses inkorgen bakom en nyckel.
Drift
- Lokalt:
pnpm dev(port 3100). Databasen är PGlite i.data/pgliteoch migreras automatiskt. - Med
DATABASE_URLanvänds Neon (serverless HTTP). Körpnpm db:migratemot databasen. - Klockan (utgångna sessioner, väntande betalningar, förnyelser, webhook-omförsök) körs varannan sekund i
next dev/next start, vid API-anrop och via Vercel Cron på/api/cron/tick(skyddas medCRON_SECRET). - Övriga miljövariabler:
VERACITY_BASE_URL(publik adress),VERACITY_ALLOW_SEED=1(tillåt /api/dev/seed i produktion).