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 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å 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.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. Bli handlare: fyll i namn, organisationsform och kontaktadress 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 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/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). 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)

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. 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).
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.deletedPrenumerationen är avslutad (status canceled).
invoice.created / finalizedFaktura för en period.
invoice.paid / invoice.payment_succeededPerioden är betald. billing_reason = subscription_create eller subscription_cycle.
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

I kassan finns en tydligt markerad panel Testläge – välj utfall: Automatiskt, Godkänn, Neka (otillräckligt saldo, kortet nekat, kortet har gått ut, misstänkt bedrägeri, kreditprövning nekad), Extra steg (3-D Secure för kort, BankID-liknande för övriga), 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). Godkännandekoden gäller 10 minuter och tål 5 felaktiga försök.

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)
GET/v1/checkout/sessionsLista (payment_intent, customer, subscription, status)
GET/v1/checkout/sessions/:idHämta (expand[]: line_items, payment_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)
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
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
GET/v1/subscriptions[/:id]Lista (customer, price, status) / hämta (expand[]=latest_invoice)
POST/v1/subscriptions/:idcancel_at_period_end, metadata
DELETE/v1/subscriptions/:idAvsluta 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_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; signaturrubriken heter Veracity-Signature.

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/refunds eller …/payments/:id/refunds → refundId; POST …/payments/:id/cancels → 204.
  • 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.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 till returnUrl?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) → resultCode Authorised, Refused, Pending, RedirectShopper (med action.url) och pspReference. Magiska kortnummer gäller.
  • POST …/payments/:pspReference/refunds och …/cancels → status: "received".
  • 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, 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_complete efter 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.

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/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).