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 har livemode: 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/mcp

Cursor, 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.txt

För agenter utan MCP

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

  1. 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.
  2. Ställ in namn, logotyp, färg, moms och betalsätt under Inställningar. Nycklarna kan rullas (de gamla spärras direkt).
  3. Lägg till en webhook-mottagare under Webhooks och kopiera signeringshemligheten (whsec_…).
  4. 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/kassa

Prenumeration (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.created och sedan antingen payment_intent.succeeded + invoice.paid + invoice.payment_succeeded, eller payment_intent.payment_failed + invoice.payment_failed + customer.subscription.updated med status: 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 active igen. Misslyckas alla blir fakturan uncollectible och prenumerationen canceled (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, timeout eller automatic; always=true gör den bestående). Inställningen gäller en dragning och återställs sedan. Timeout = dragningen står som processing i 30 s och misslyckas sedan. Utan sparat betalsätt misslyckas dragningen alltid. Prenumerationsobjektet visar inställningen i veracity_next_charge.
  • Uppsägning: vid periodens slut med POST /v1/subscriptions/:id och cancel_at_period_end=true (ångra med false), eller direkt med DELETE /v1/subscriptions/:id (eller POST /v1/subscriptions/:id/cancel). Båda ger customer.subscription.deleted när prenumerationen upphör.
  • Tid: förnyelser körs i realtid (Vercel Cron varje minut mot /api/cron/tick, skyddat med CRON_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 (eller trial_end) i kassan, eller trial_period_days på 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_status blir paid. Prenumerationen är trialing; customer.subscription.trial_will_end kommer tre dagar före slutet, och sedan dras första hela fakturan (subscription_cycle). trial_end=now vid uppdatering avslutar provperioden direkt.
  • Byt plan: POST /v1/subscriptions/:id med items[0][id]=si_… och items[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_invoice fakturerar och drar dem direkt (billing_reason: subscription_update); none hoppar över dem. Byte mellan månad och år startar en ny period nu och faktureras direkt.
  • Kundportal: POST /v1/billing_portal/sessions med customer och return_url ger en url dä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/coupons med percent_off, eller amount_off och currency, och duration once, repeating (med duration_in_months) eller forever. Priserna är inklusive moms, så rabatten sänker totalen och momsen räknas om på det som är kvar.
  • I kassan: discounts[0][coupon] eller discounts[0][promotion_code] när kassan skapas, eller allow_promotion_codes=true så att betalaren kan skriva en kod på betalsidan (stora och små bokstäver spelar ingen roll). Kassan visar total_details.amount_discount; i prenumerationsläget flyttar rabatten till prenumerationen, och varje faktura får total_discount_amounts så länge kupongen gäller.
  • Rabattkoder: POST /v1/promotion_codes med coupon och code; begränsa med max_redemptions, expires_at, customer eller restrictions[first_time_transaction|minimum_amount].
  • Betallänkar: POST /v1/payment_links med line_items[0][price] och [quantity] ger en url. Varje besök öppnar en ny kassa med payment_link; efteråt ser betalaren ditt after_completion-meddelande eller skickas till din adress.

Spara kort och dra senare

  • Spara utan betalning: kassa med mode=setup (inga orderrader; sessionen får setup_intent och payment_status: no_payment_required), eller POST /v1/setup_intents med customer, payment_method och confirm=true. Kortet kopplas till kunden; gör det till invoice_settings.default_payment_method och dra det senare utan kund närvarande.
  • Spara medan kunden betalar: setup_future_usage=off_session på en PaymentIntent, eller payment_intent_data[setup_future_usage]=off_session i kassan, kopplar kortet till kunden när betalningen har gått igenom.
  • Dra utan kunden (sparat kort): POST /v1/payment_intents med customer, payment_method, off_session=true och confirm=true. Ett kort som kräver 3-D Secure (pm_card_threeDSecureRequired, eller kortet 4000 0000 0000 3220 sparat tidigare) nekas med 402 authentication_required och PaymentIntent går tillbaka till requires_payment_method: be kunden komma tillbaka och bekräfta.
  • Reservera nu, dra senare: capture_method=manual på en PaymentIntent, eller payment_intent_data[capture_method]=manual i kassan. PaymentIntent stannar på requires_capture; dra med POST /v1/payment_intents/:id/capture (gärna amount_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: loadStripe använder window.Stripe om det redan finns, så ladda skriptet ovan först och synkront, i <head> (Next.js: rotlayoutens <head>, eller next/script med strategy="beforeInteractive"). Anropa sedan loadStripe(pk) som vanligt. Finns skriptet inte än hämtar loadStripe det riktiga Stripe.js; window.Stripe.__veracity visar 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]=true eller payment_method_types[]. Uppskjutet: elements({ mode, amount, currency }), sedan elements.submit(), sedan skapa PaymentIntent och bekräfta. Eller stripe.createConfirmationToken({ elements }) och POST /v1/payment_intents med confirmation_token och confirm=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_action när betalaren godkänner.
  • Embedded Checkout: POST /v1/checkout/sessions med ui_mode=embedded och return_url (eller redirect_on_completion=never), sedan stripe.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 nycklar pk_ och sk_.

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ändelseNär
checkout.session.completedKassan är slutförd. payment_status = paid, eller unpaid om betalningen behandlas (asynkron).
checkout.session.async_payment_succeeded / _failedEn väntande betalning från kassan avgjordes.
checkout.session.expiredKassan gick ut (tid, timeout-utfall eller /expire).
payment_intent.created / requires_action / processingBetalningens livscykel.
payment_intent.succeededPengarna (låtsas) dragna.
payment_intent.payment_failedNekad. last_payment_error.code/decline_code anger orsak.
payment_intent.canceledAvbruten eller utgången (status canceled/expired).
payment_intent.amount_capturable_updated / charge.captured / charge.expiredManuell capture: beloppet är reserverat, draget, eller släppt efter 7 dagar.
setup_intent.succeeded / setup_failed / canceledEtt kort sparat utan betalning (setup-läge, provperiod, API).
payment_method.attached / detachedEtt sparat kort kopplat till eller från en kund.
charge.succeeded / charge.failed / charge.refundedCharge-objektet; charge.refunded vid hel och delvis återbetalning.
refund.createdNy återbetalning.
customer.created / updated / deletedKunder.
customer.subscription.createdNy prenumeration (status active, eller incomplete vid asynkron första betalning).
customer.subscription.updatedNy period, past_due, active igen, cancel_at_period_end ändrad. previous_attributes visar vad som ändrats.
customer.subscription.trial_will_endTre dagar innan provperioden slutar.
customer.subscription.deletedPrenumerationen är avslutad (status canceled).
invoiceitem.created / updatedProrateringar vid planbyte, och när de hamnar på en faktura.
billing_portal.session.createdEn kundportal-länk skapades.
coupon.created / updated / deletedKuponger; updated även när times_redeemed ökar.
promotion_code.created / updatedRabattkoder.
customer.discount.created / deletedEn kupong läggs på en prenumeration, eller slutar gälla (tiden ute, använd en gång, borttagen).
payment_link.created / updatedBetallänkar.
invoice.created / finalizedFaktura för en period.
invoice.paid / invoice.payment_succeededPerioden är betald. billing_reason = subscription_create, subscription_cycle eller subscription_update.
invoice.payment_failedFörnyelsen kunde inte dras. Prenumerationen blir past_due.
invoice.marked_uncollectibleAlla fyra försöken misslyckades; prenumerationen avslutas (customer.subscription.deleted).
invoice.voidedAsynkron 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

KortnummerUtfall
4242 4242 4242 4242Visa – godkänns
5555 5555 5555 4444Mastercard – godkänns
4000 0000 0000 0002Nekas: kortet nekat
4000 0000 0000 9995Nekas: otillräckligt saldo
4000 0000 0000 0069Nekas: kortet har gått ut
4100 0000 0000 0019Nekas: misstänkt bedrägeri
4000 0000 0000 3220Kräver 3-D Secure-steg
4000 0000 0000 0150Väntande, lyckas efter 5 s
4000 0000 0000 0168Väntande, misslyckas efter 5 s
4000 0000 0000 0119Leverantörsfel (500)
4000 0000 0000 0184Timeout – sessionen löper ut
4000 0000 0000 0341Godkä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:

BeloppUtfall
…,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=3

Testbetalsä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)

MetodSökvägBeskrivning
POST/v1/checkout/sessionsSkapa kassa (mode payment | subscription | setup; discounts[0][coupon|promotion_code] eller allow_promotion_codes)
GET/v1/checkout/sessionsLista (payment_intent, customer, subscription, status, payment_link)
GET/v1/checkout/sessions/:idHämta (expand[]: line_items, payment_intent, setup_intent, customer, subscription, invoice)
GET/v1/checkout/sessions/:id/line_itemsOrderrader
POST/v1/checkout/sessions/:id/expireAvbryt en öppen kassa
POST/v1/payment_intentsSkapa (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/:idUppdatera metadata, description, amount
POST/v1/payment_intents/:id/confirmBekräfta med testbetalsätt
POST/v1/payment_intents/:id/cancelAvbryt (släpper även en reservation)
POST/v1/payment_intents/:id/captureDra 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 | cancelBekräfta / avbryt
GET/v1/payment_methods/:id, /v1/customers/:id/payment_methodsSparade kort
POST/v1/payment_methods/:id/attach | detachKoppla till / från en kund
POST/v1/payment_methodsBetalsätt från kortuppgifter (card, swish); Stripe.js gör det med den publicerbara nyckeln
POST/v1/confirmation_tokensConfirmationToken från Elements (publicerbar nyckel)
GET/v1/confirmation_tokens/:idHämta en ConfirmationToken
POST/v1/test_helpers/confirmation_tokensSkapa 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/:idHä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/subscriptionsSkapa 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/:idcancel_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/:idAvsluta 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/sessionsKundportal 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/discountTa 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_itemsBetallä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_clockFlytta testklockan: days, months, seconds eller frozen_time; renewal_outcome
GET/v1/test_helpers/clockHandlarens aktuella testtid
POST/v1/test_helpers/subscriptions/:id/next_charge_outcomeUtfall för nästa automatiska dragning: outcome = automatic | approve | insufficient_funds | restricted_card | timeout, always = true|false
POST/v1/test_helpers/checkout/sessions/:id/completeSlutfö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.js på sidan och kör new 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/refunds eller …/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/terminate avslutar en obetald kassa.
  • PUT …/payments/:id/orderitems ändrar ordern medan betalaren är i kassan; PUT …/referenceinformation { checkoutUrl, reference } och PUT …/myreference { myReference } ändrar referenserna.
  • Prenumerationer: subscription: { endDate, interval } och/eller unscheduledSubscription: { create: true } på betalningen (bara kort). Sedan GET /nets/v1/subscriptions/:id, POST …/subscriptions/:id/charges { order } → { paymentId, chargeId } (402 om den nekas), GET …/subscriptions/:id/charges/status med samma Idempotency-Key, bulk POST /nets/v1/subscriptions/charges → bulkId och GET …/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 → bulkId kontrollerar korten innan du drar. order.amount: 0 med ett avtal registrerar kortet utan reservation.
  • Webhooks skickas till URL:erna i notifications.webHooks med deras authorization-värde i Authorization: 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 till returnUrl?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) → resultCode Authorised, Refused, Pending, RedirectShopper (med action.url) och pspReference. Magiska kortnummer gäller.
  • POST /adyen/v72/payments/details { details: { redirectResult } } efter en redirect (kunden kommer tillbaka till returnUrl?redirectResult=…); POST /adyen/v72/paymentMethods listar betalsätt och sparade kort.
  • Ändringar svarar status: "received" och utfallet kommer som notifiering: …/payments/:pspReference/captures (med additionalData.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/amountUpdates justerar en förauktorisering (additionalData.authorisationType: "PreAuth") → AUTHORISATION_ADJUSTMENT; i v72 med adjustAuthorisationData kommer svaret authorised eller refused direkt. POST /adyen/v72/cancels avbryter med din egen paymentReference → TECHNICAL_CANCEL.
  • Pay by Link: POST /adyen/v72/paymentLinks → { id: "PL…", url, status: "active" } (engångslänk, upp till 70 dagar); GET och PATCH …/paymentLinks/:id (status: "expired"). AUTHORISATION-notifieringen har additionalData.paymentLinkId.
  • Tokens: storePaymentMethod: true, shopperReference och recurringProcessingModel på /payments (eller storePaymentMethodMode på /sessions) → additionalData["tokenization.storedPaymentMethodId"]; dra med paymentMethod: { type: "scheme", storedPaymentMethodId } och shopperInteraction: "ContAuth" (även med recurringProcessingModel: "UnscheduledCardOnFile"; aldrig med 3-D Secure, med networkTxReference). POST (spara utan betalning), GET och DELETE …/storedPaymentMethods; en nollbeloppsauktorisering (amount.value: 0) sparar också kortet. Tokeniseringswebhooks (recurring.token.created, .alreadyExisting, .disabled) går till Adyen-endpointen, signerade i headern HmacSignature.
  • Notifieringar: lägg till en mottagare med formatet Adyen i Dashboard (eller veracity_format=adyen på /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_complete efter köpet; utgången order ger 404. POST /kustom/checkout/v3/orders/:id uppdaterar 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 …/captures och POST …/captures/:id/shipping-info. Med headern Kustom-Idempotency-Key (eller Klarna-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 => …) med api.suspend(), api.resume() och api.on({ load, change, shipping_option_change, order_total_change, validation_callback, … }). POST /kustom/checkout/v3/orders/:id/abort stänger en obetald order.
  • Hosted Payment Page: POST /kustom/hpp/v1/sessions med payment_session_url till ordern → redirect_url, qr_code_url, session_url, distribution_url. Statusanrop till merchant_urls.status_update (WAITING, IN_PROGRESS, COMPLETED, FAILED, BACK, DISABLED); DELETE stänger sessionen.
  • Anrop under köpet: merchant_urls.validation får ordern när betalaren trycker på köp (200 godkänner, 303 med Location eller 400 med error_text stoppar köpet); shipping_options blir en shipping_fee-rad, eller går till shipping_option_update; address_update och country_change kan svara med en ny varukorg eller nya fraktalternativ.
  • Återkommande: recurring: true på ordern → recurring_token på 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/pglite och migreras automatiskt.
  • Med DATABASE_URL används Neon (serverless HTTP). Kör pnpm db:migrate mot 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 med CRON_SECRET).
  • Övriga miljövariabler: VERACITY_BASE_URL (publik adress), VERACITY_ALLOW_SEED=1 (tillåt /api/dev/seed i produktion).