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).
Översikt
- En motor, flera varianter. Stripe-varianten under
/v1är komplett. Nets Easy (/nets/v1), Adyen (/adyen/v71) och Kustom (/kustom) är tunnare adaptrar för respektive leverantörs hosted checkout-flöde. - 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å fungerar complete_checkout, set_next_charge_outcome, advance_clock, key_status och extend_key – utan att nyckeln syns i chatten.
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 – 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 med filter (status, betalsätt, 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.
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). 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)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). |
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.deleted | Prenumerationen är avslutad (status canceled). |
invoice.created / finalized | Faktura för en period. |
invoice.paid / invoice.payment_succeeded | Perioden är betald. billing_reason = subscription_create eller subscription_cycle. |
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) |
GET | /v1/checkout/sessions | Lista (payment_intent, customer, subscription, status) |
GET | /v1/checkout/sessions/:id | Hämta (expand[]: line_items, payment_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) |
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 |
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 |
GET | /v1/subscriptions[/:id] | Lista (customer, price, status) / hämta (expand[]=latest_invoice) |
POST | /v1/subscriptions/:id | cancel_at_period_end, metadata |
DELETE | /v1/subscriptions/:id | Avsluta direkt (även POST …/cancel) |
GET | /v1/invoices[/:id] | Fakturor (customer, subscription, status) |
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). Endast integrationType: "HostedPaymentPage".
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=…. 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→ 204.- 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.refund.completed, payment.cancel.created. Kropp:{ id, merchantId, timestamp, event, data: { paymentId, … } }.
Adyen-variant
Bas-URL /adyen/v71 (v68–v71 accepteras), autentisering med X-API-Key: vp_sk_test_….
POST /adyen/v71/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/v71/sessions/:id→status: active, completed, paymentPending, refused, canceled, expired.POST /adyen/v71/payments– paymentMethod{ type: "scheme", encryptedCardNumber: "test_4111111111111111", … }(eller number) →resultCodeAuthorised, Refused, Pending, RedirectShopper (medaction.url) ochpspReference. Magiska kortnummer gäller.POST …/payments/:pspReference/refundsoch…/cancels→status: "received".- 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, REFUND och CANCELLATION. 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).GET /kustom/checkout/v3/orders/:id→checkout_completeefter köpet; utgången order ger 404.- 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),…/cancel,…/merchant-references.
Swish Handel-variant
Swish Commerce API: betalningsförfrågningar (e-handel och m-handel), återbetalningar, callbacks och återkommande betalningar – utan certifikat. Bas-URL /swish. Fullständig steg-för-steg-guide med anrop, callbacks och testutfall: /guides/swish.md.
Svea Checkout-variant
Checkout API och Payment Admin med Svea-signerade anrop, push och leverans/kreditering. Bas-URL /svea. Fullständig steg-för-steg-guide med anrop, callbacks och testutfall: /guides/svea.md.
Walley Checkout-variant
Checkout med OAuth2, notifieringar och orderhantering (aktivera, återbetala, makulera). Bas-URL /walley. Fullständig steg-för-steg-guide med anrop, callbacks och testutfall: /guides/walley.md.
Qliro One-variant
Merchant API med Qliro-signerade anrop, statuspush och orderhantering. Bas-URL /qliro. Fullständig steg-för-steg-guide med anrop, callbacks och testutfall: /guides/qliro.md.
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.
- 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).