# Veracity Pay

> A test payment service provider built for AI-driven development. It emulates Stripe, Nets Easy, Adyen, Kustom, Swish Handel, Svea Checkout, Walley Checkout, Qliro One: hosted checkout, cards, Swish, invoice and instalments, subscriptions with renewals, refunds and signed webhooks. No money moves; a payment is approved with a six-digit code sent to the payer's e-mail. Free to use for now. A live demo shop that runs every emulated partner is at https://pay.veracity.se/demo.

Each flavour mirrors the original provider's paths, fields, auth and callbacks, so going live means changing only the keys and the host. Every guide links the original documentation and lists the deviations.

The fastest path for a coding agent is the MCP server: `claude mcp add --transport http veracity-pay https://pay.veracity.se/mcp`. Then tell the agent: "I want us to use Veracity Pay to emulate payments in this project." It asks which payment partners to emulate and which e-mail to use, registers an account and the developer receives the keys and commands by e-mail. The agent never sees a secret.

## Docs
- [Full reference for agents](https://pay.veracity.se/llms-full.txt): every endpoint, webhook, test outcome and magic value.
- [Human docs](https://pay.veracity.se/docs) (Swedish and English).
- [TypeScript client](https://pay.veracity.se/sdk.ts): dependency-free client and webhook verification.
- [Webhook listener for localhost](https://pay.veracity.se/listen.mjs).

## Integration guides
- [Stripe](https://pay.veracity.se/guides/stripe.md): Checkout Sessions, PaymentIntents, subscriptions, refunds and webhooks (complete).
- [Nets Easy](https://pay.veracity.se/guides/nets.md): Payment API v1 with the hosted payment page, charges, refunds and webhooks.
- [Adyen](https://pay.veracity.se/guides/adyen.md): Checkout API v71: sessions (Hosted Checkout), /payments and HMAC-signed notifications.
- [Kustom](https://pay.veracity.se/guides/kustom.md): Checkout v3 (formerly Klarna Checkout) with html_snippet, push and order management.
- [Swish Handel](https://pay.veracity.se/guides/swish.md): Swish Commerce API: payment requests (e-commerce and m-commerce), refunds, callbacks and recurring payments – without certificates.
- [Svea Checkout](https://pay.veracity.se/guides/svea.md): Checkout API and Payment Admin with Svea-signed calls, push and delivery/credit.
- [Walley Checkout](https://pay.veracity.se/guides/walley.md): Checkout with OAuth2, notifications and order management (activate, refund, cancel).
- [Qliro One](https://pay.veracity.se/guides/qliro.md): Merchant API with Qliro-signed calls, status push and order management.

## MCP
- Endpoint: https://pay.veracity.se/mcp (Streamable HTTP). Tools: list_providers, register_merchant, integration_guide, key_status, extend_key, complete_checkout, set_next_charge_outcome, advance_clock, simulate_outage.
- Test helpers authenticate with the developer's secret key in an Authorization header in the MCP client config, never in the chat.

## Optional
- [Dashboard](https://pay.veracity.se/dashboard): keys (valid one month at a time, one-click extend), payments, refunds, subscriptions, webhooks.
- [My payments](https://pay.veracity.se/konto): the payer's portal across merchants.

---

# Set-up with an AI agent

1. Add the MCP server: `claude mcp add --transport http veracity-pay https://pay.veracity.se/mcp` (Cursor and other clients: `{"mcpServers":{"veracity-pay":{"url":"https://pay.veracity.se/mcp"}}}`).
2. Prompt: "I want us to use Veracity Pay to emulate payments in this project."
3. The agent asks two questions (which payment partners, which e-mail) and calls `register_merchant`. The developer gets an e-mail with keys, MID, webhook secret and ready-to-run commands (.env.local lines, `vercel env add`, the webhook connect step).
4. Once the key is in the environment, add it to the MCP config so the test helpers work: `claude mcp add --transport http veracity-pay https://pay.veracity.se/mcp --header "Authorization: Bearer ${VERACITY_PAY_SECRET_KEY}"`.
5. Add this to CLAUDE.md / AGENTS.md:

```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
```

# API keys
- Secret keys `vp_sk_test_…`, publishable keys `vp_pk_test_…`. Valid one month at a time; a reminder e-mail with a one-click extend link comes 5 days before expiry.
- An expired key answers HTTP 401 `{"error":{"type":"authentication_error","code":"api_key_expired","message":"…how to extend…"}}`. Extending keeps the same key. Webhook signing secrets do not expire.

# Errors
`{"error":{"type":"invalid_request_error|authentication_error|card_error|idempotency_error|api_error","code":"…","decline_code":"…","message":"…","param":"…"}}` with 400, 401, 402, 404, 409 or 500.

# Simulated outages
Switch on, per payment partner or for all (dashboard → Störningar/Outages, or the MCP tool `simulate_outage`): invalid_key / expired_key (401 in the provider's format), slow (delay up to 25 s), timeout (28 s, then 504), offline (503), server_error (500), rate_limited (429, Retry-After 30), flaky (a share of calls get 500/503), and paused webhooks (deliveries wait until the pause ends). Each can end after a set time. Test helpers keep working. Use it to test retries, timeouts, error messages to the payer and idempotency.

# Idempotency
Send `Idempotency-Key` on POST: the same key and body replays the first response (`Idempotent-Replayed: true`); a different body gives 400 idempotency_error.

# Webhooks (Stripe flavour)
Header `Veracity-Signature: t=<unix>,v1=<hex>` (the same value is also sent as `Stripe-Signature`, so `stripe.webhooks.constructEvent` works unchanged), HMAC-SHA256 of `${t}.${rawBody}` with the endpoint's whsec_ secret; reject anything older than 5 minutes. Retries after 10 s, 30 s, 2 min, 10 min, 1 h, 6 h and 24 h (8 attempts). Events: checkout.session.completed, checkout.session.expired, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, payment_intent.created, payment_intent.requires_action, payment_intent.processing, payment_intent.succeeded, payment_intent.payment_failed, payment_intent.canceled, charge.succeeded, charge.failed, charge.refunded, refund.created, customer.created, customer.updated, customer.deleted, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.created, invoice.finalized, invoice.paid, invoice.payment_succeeded, invoice.payment_failed, invoice.marked_uncollectible, invoice.voided, product.created, price.created.

# Subscriptions
Statuses incomplete → active → past_due → canceled (and incomplete_expired). The first payment goes through checkout with a code; the payment method is saved and renewals are charged without a code at each period end. A failed renewal is retried on day 3, 5 and 7; after four failed attempts the invoice is uncollectible and the subscription canceled.

# Endpoints (Stripe flavour, base https://pay.veracity.se/v1)
POST/GET /checkout/sessions, GET /checkout/sessions/{id} (expand[]=line_items|payment_intent|customer|subscription|invoice), GET /checkout/sessions/{id}/line_items, POST /checkout/sessions/{id}/expire, POST/GET /payment_intents, GET/POST /payment_intents/{id}, POST /payment_intents/{id}/confirm|cancel, GET /charges/{id}, POST/GET /refunds, POST/GET/DELETE /customers[/{id}], POST/GET /products[/{id}], POST/GET /prices[/{id}], POST/GET /subscriptions, GET/POST/DELETE /subscriptions/{id}, POST /subscriptions/{id}/cancel, GET /invoices[/{id}], GET /events[/{id}] (type, created[gt|gte]), POST/GET /webhook_endpoints, POST/DELETE /webhook_endpoints/{id}, POST /test_helpers/advance_clock, GET /test_helpers/clock, POST /test_helpers/checkout/sessions/{id}/complete, POST /test_helpers/subscriptions/{id}/next_charge_outcome.

# Veracity Pay – Stripe flavour in Next.js

Base URL `https://pay.veracity.se/v1`, auth `Authorization: Bearer $VERACITY_PAY_SECRET_KEY`. Bodies are JSON or Stripe form encoding. Objects, ids (cs_test_, pi_test_, cus_, sub_, in_, evt_) and list pagination match Stripe.

## 1. Environment
```
VERACITY_PAY_BASE_URL=https://pay.veracity.se
VERACITY_PAY_SECRET_KEY=vp_sk_test_…            # from the registration e-mail
NEXT_PUBLIC_VERACITY_PAY_PUBLISHABLE_KEY=vp_pk_test_…
VERACITY_PAY_WEBHOOK_SECRET=whsec_…
```

## 2. Client
Download the dependency-free client: `curl -s https://pay.veracity.se/sdk.ts -o lib/veracity-pay.ts` (createVeracityPay + constructEvent). Stripe's own SDK also works for the common calls: `new Stripe(process.env.VERACITY_PAY_SECRET_KEY, { host: "pay.veracity.se", port: 443, protocol: "https" })` – Stripe's SDK has no base-path option, which is why Veracity serves the API at /v1 on the root. Webhooks carry `Stripe-Signature`, so `stripe.webhooks.constructEvent(rawBody, req.headers.get("stripe-signature"), secret)` works too.

## 3. Create a checkout (subscription in SEK)
```ts
// 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 { email, accountId } = await req.json();
  const session = await vp.checkout.sessions.create({
    mode: "subscription",                                   // or "payment"
    line_items: [{ price_data: { currency: "sek", unit_amount: 9900, product_data: { name: "Plan" }, recurring: { interval: "month" } }, quantity: 1 }],
    customer_email: email,
    client_reference_id: accountId,
    subscription_data: { metadata: { account_id: accountId } },
    success_url: `${process.env.APP_URL}/billing/done?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.APP_URL}/billing`,
  }, { idempotencyKey: `checkout-${accountId}` });
  return Response.json({ url: session.url });
}
```
Create reusable prices with `POST /v1/prices` (`recurring[interval]=month|year`, `lookup_key`) and pass `{ price: "price_…" }` instead of price_data.

## 4. Webhooks
```ts
// app/api/webhooks/veracity-pay/route.ts
import { constructEvent } from "@/lib/veracity-pay";
export async function POST(req: Request) {
  const raw = await req.text();
  const event = await constructEvent(raw, req.headers.get("veracity-signature"), process.env.VERACITY_PAY_WEBHOOK_SECRET!);
  switch (event.type) {
    case "checkout.session.completed":     // link customer + subscription to your account (client_reference_id)
    case "customer.subscription.created":
    case "customer.subscription.updated":  // status active | past_due | canceled, current_period_end
    case "customer.subscription.deleted":
    case "invoice.paid":
    case "invoice.payment_failed":
      break;
  }
  return Response.json({ received: true });
}
```
Point the endpoint at your app once: `curl https://pay.veracity.se/v1/webhook_endpoints/{we_id} -u $VERACITY_PAY_SECRET_KEY: -d url=https://YOUR-APP/api/webhooks/veracity-pay` (the id is in the registration e-mail). On localhost: `curl -s https://pay.veracity.se/listen.mjs -o veracity-listen.mjs && node veracity-listen.mjs http://localhost:3000/api/webhooks/veracity-pay`.

## 5. Cancel a subscription
- At period end: `POST /v1/subscriptions/{id}` with `cancel_at_period_end=true`.
- Now: `DELETE /v1/subscriptions/{id}`. Both end with `customer.subscription.deleted`.

### Test outcomes

- Hosted checkout: each flavour follows that provider's order of steps; every verification (3-D Secure, Swish app, BankID) is a six-digit code e-mailed to the payer. A "Test mode" panel picks approve, decline (with a reason, including authentication_failed), pending, timeout or provider error. Invoice asks for a personnummer: use 19121212-1212.
- Magic cards: `4242424242424242` Visa – succeeds; `5555555555554444` Mastercard – succeeds; `4000000000000002` Declined: card declined; `4000000000009995` Declined: insufficient funds; `4000000000000069` Declined: expired card; `4100000000000019` Declined: suspected fraud; `4000000000003220` Requires 3-D Secure authentication; `4000000000000150` Pending, succeeds after 5 s; `4000000000000168` Pending, fails after 5 s; `4000000000000119` Provider error (500); `4000000000000184` Timeout – the session expires; `4000000000000341` Succeeds, but renewals fail.
- Magic amounts (total ends in these öre/cents, at least 1.00): `.01` Declined: card declined; `.02` Declined: insufficient funds; `.03` Declined: expired card; `.04` Declined: suspected fraud; `.05` Declined: credit check failed (invoice); `.06` Requires an extra authentication step; `.07` Pending, succeeds after 5 s; `.08` Pending, fails after 5 s; `.09` Timeout – the session expires.
- `metadata[veracity_test_outcome]` on a Checkout Session or PaymentIntent: `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]` seconds for pending/timeout).
- Without a browser: `POST /v1/test_helpers/checkout/sessions/{id}/complete` with `outcome` (MCP tool `complete_checkout`).
- Next renewal of a subscription: `POST /v1/test_helpers/subscriptions/{id}/next_charge_outcome` with `outcome` = approve | insufficient_funds | restricted_card | timeout (MCP tool `set_next_charge_outcome`).
- Time: `POST /v1/test_helpers/advance_clock` with `months` / `days` (MCP tool `advance_clock`); renewals are charged without a code and send invoice.paid or invoice.payment_failed.
## Going live and deviations from the original

Original documentation: [Stripe API reference](https://docs.stripe.com/api), [Stripe Checkout](https://docs.stripe.com/payments/checkout), [Stripe webhooks](https://docs.stripe.com/webhooks).

To go live, change only the host and the keys: Test and production: `https://api.stripe.com`. Replace vp_sk_test_… with sk_test_… or sk_live_… and the whsec_… webhook secret with Stripe's. With Stripe's own SDK: remove the host/port/protocol setting.

Deviations in Veracity Pay:
- Hosted Checkout only. Stripe.js, Elements, the Payment Element and Embedded Checkout talk to Stripe directly and cannot be pointed elsewhere.
- PaymentIntents also have the statuses failed and expired (Stripe uses requires_payment_method and canceled).
- Unsupported parameters are ignored instead of returning 400.
- Missing: the billing portal, setup intents and the payment methods API, price/quantity changes with proration, trials, manual capture, coupons and tax rates.
- Webhooks are signed in both Stripe-Signature and Veracity-Signature (the same scheme).


---

# Veracity Pay – Nets Easy flavour

Base URL `https://nets.pay.veracity.se` in place of https://test.api.dibspayment.eu – Nets' own paths follow (`/v1/payments` …); the same API is also at `https://pay.veracity.se/nets/v1`. Auth: `Authorization: $NETS_EASY_SECRET_KEY` (no "Bearer", like Nets). Only `integrationType: "HostedPaymentPage"`.

```
NETS_EASY_BASE_URL=https://nets.pay.veracity.se
NETS_EASY_SECRET_KEY=vp_sk_test_…
```

1. `POST /v1/payments` with `order.items[]` (grossTotalAmount, taxRate 2500 = 25%, taxAmount), `order.amount` (the sum), `checkout.returnUrl`, `cancelUrl`, `termsUrl`, `charge`, `notifications.webHooks[]` → `{ paymentId, hostedPaymentPageUrl }`. Redirect the customer to hostedPaymentPageUrl.
2. The customer returns to `returnUrl?paymentid=…`. Read `GET /v1/payments/{paymentId}`: `payment.summary.reservedAmount`, `paymentDetails`.
3. `POST /v1/payments/{id}/charges` { amount } → chargeId; refunds via `POST /v1/charges/{chargeId}/refunds`; cancel via `POST /v1/payments/{id}/cancels`.
4. Webhooks go to the URLs in `notifications.webHooks` with your `authorization` value in the Authorization header: payment.created, payment.reservation.created.v2, payment.checkout.completed, payment.reservation.failed, payment.charge.created.v2, payment.refund.completed, payment.cancel.created.

### Test outcomes

- Hosted checkout: each flavour follows that provider's order of steps; every verification (3-D Secure, Swish app, BankID) is a six-digit code e-mailed to the payer. A "Test mode" panel picks approve, decline (with a reason, including authentication_failed), pending, timeout or provider error. Invoice asks for a personnummer: use 19121212-1212.
- Magic cards: `4242424242424242` Visa – succeeds; `5555555555554444` Mastercard – succeeds; `4000000000000002` Declined: card declined; `4000000000009995` Declined: insufficient funds; `4000000000000069` Declined: expired card; `4100000000000019` Declined: suspected fraud; `4000000000003220` Requires 3-D Secure authentication; `4000000000000150` Pending, succeeds after 5 s; `4000000000000168` Pending, fails after 5 s; `4000000000000119` Provider error (500); `4000000000000184` Timeout – the session expires; `4000000000000341` Succeeds, but renewals fail.
- Magic amounts (total ends in these öre/cents, at least 1.00): `.01` Declined: card declined; `.02` Declined: insufficient funds; `.03` Declined: expired card; `.04` Declined: suspected fraud; `.05` Declined: credit check failed (invoice); `.06` Requires an extra authentication step; `.07` Pending, succeeds after 5 s; `.08` Pending, fails after 5 s; `.09` Timeout – the session expires.
- `metadata[veracity_test_outcome]` on a Checkout Session or PaymentIntent: `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]` seconds for pending/timeout).
- Without a browser: `POST /v1/test_helpers/checkout/sessions/{id}/complete` with `outcome` (MCP tool `complete_checkout`).
- Next renewal of a subscription: `POST /v1/test_helpers/subscriptions/{id}/next_charge_outcome` with `outcome` = approve | insufficient_funds | restricted_card | timeout (MCP tool `set_next_charge_outcome`).
- Time: `POST /v1/test_helpers/advance_clock` with `months` / `days` (MCP tool `advance_clock`); renewals are charged without a code and send invoice.paid or invoice.payment_failed.
## Going live and deviations from the original

Original documentation: [Nexi Checkout Payment API v1](https://developer.nexigroup.com/nexi-checkout/en-EU/api/payment-v1/), [Nexi Checkout webhooks](https://developer.nexigroup.com/nexi-checkout/en-EU/api/webhooks/).

To go live, change only the host and the keys: Test: `https://test.api.dibspayment.eu`; Production: `https://api.dibspayment.eu`. Replace the secret key with your test or live secret key from the Nexi portal.

Deviations in Veracity Pay:
- integrationType HostedPaymentPage only. EmbeddedCheckout (checkout.js) is not supported.
- Missing: subscriptions and unscheduled subscriptions, PUT orderitems, referenceinformation and terminate.
- The webhook events refund.initiated, charge.failed and cancel.failed are not sent.


---

# Veracity Pay – Adyen flavour

Base URL `https://adyen.pay.veracity.se/v71` in place of https://checkout-test.adyen.com/v71 (also at `https://pay.veracity.se/adyen/v71`). Auth: `X-API-Key: $ADYEN_API_KEY`.

```
ADYEN_CHECKOUT_URL=https://adyen.pay.veracity.se/v71
ADYEN_API_KEY=vp_sk_test_…
ADYEN_MERCHANT_ACCOUNT=acct_…
ADYEN_HMAC_KEY=…                  # from the registration e-mail (Adyen-format notification endpoint)
```

1. `POST /sessions` { merchantAccount, amount: { value, currency }, reference, returnUrl, mode: "hosted", countryCode, shopperEmail, lineItems? } → { id: "CS…", url, sessionData }. Redirect to url.
2. The shopper returns to `returnUrl?sessionId=…&sessionResult=…`; `GET /sessions/{id}` → status completed | paymentPending | refused | canceled | expired.
3. `POST /payments` with `paymentMethod: { type: "scheme", encryptedCardNumber: "test_4111111111111111", … }` → resultCode Authorised | Refused | Pending | RedirectShopper.
4. Refunds: `POST /payments/{pspReference}/refunds`; cancels: `/cancels`.
5. Notifications (AUTHORISATION, REFUND, CANCELLATION) in Adyen's standard format to the Adyen-format endpoint: set its URL with `curl https://pay.veracity.se/v1/webhook_endpoints/{we_id} -u $ADYEN_API_KEY: -d url=https://YOUR-APP/api/adyen/notifications`. Verify additionalData.hmacSignature: base64(HMAC-SHA256(hex-key, "pspReference:originalReference:merchantAccountCode:merchantReference:value:currency:eventCode:success")). Answer `[accepted]`.

### Test outcomes

- Hosted checkout: each flavour follows that provider's order of steps; every verification (3-D Secure, Swish app, BankID) is a six-digit code e-mailed to the payer. A "Test mode" panel picks approve, decline (with a reason, including authentication_failed), pending, timeout or provider error. Invoice asks for a personnummer: use 19121212-1212.
- Magic cards: `4242424242424242` Visa – succeeds; `5555555555554444` Mastercard – succeeds; `4000000000000002` Declined: card declined; `4000000000009995` Declined: insufficient funds; `4000000000000069` Declined: expired card; `4100000000000019` Declined: suspected fraud; `4000000000003220` Requires 3-D Secure authentication; `4000000000000150` Pending, succeeds after 5 s; `4000000000000168` Pending, fails after 5 s; `4000000000000119` Provider error (500); `4000000000000184` Timeout – the session expires; `4000000000000341` Succeeds, but renewals fail.
- Magic amounts (total ends in these öre/cents, at least 1.00): `.01` Declined: card declined; `.02` Declined: insufficient funds; `.03` Declined: expired card; `.04` Declined: suspected fraud; `.05` Declined: credit check failed (invoice); `.06` Requires an extra authentication step; `.07` Pending, succeeds after 5 s; `.08` Pending, fails after 5 s; `.09` Timeout – the session expires.
- `metadata[veracity_test_outcome]` on a Checkout Session or PaymentIntent: `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]` seconds for pending/timeout).
- Without a browser: `POST /v1/test_helpers/checkout/sessions/{id}/complete` with `outcome` (MCP tool `complete_checkout`).
- Next renewal of a subscription: `POST /v1/test_helpers/subscriptions/{id}/next_charge_outcome` with `outcome` = approve | insufficient_funds | restricted_card | timeout (MCP tool `set_next_charge_outcome`).
- Time: `POST /v1/test_helpers/advance_clock` with `months` / `days` (MCP tool `advance_clock`); renewals are charged without a code and send invoice.paid or invoice.payment_failed.
## Going live and deviations from the original

Original documentation: [Adyen Checkout API v71](https://docs.adyen.com/api-explorer/Checkout/71/overview), [Adyen Hosted Checkout](https://docs.adyen.com/online-payments/build-your-integration/sessions-flow/?platform=Web&integration=Hosted+Checkout), [Adyen webhooks (HMAC)](https://docs.adyen.com/development-resources/webhooks/verify-hmac-signatures).

To go live, change only the host and the keys: Test: `https://checkout-test.adyen.com/v71`; Production: `https://{prefix}-checkout-live.adyenpayments.com/checkout/v71`. Replace X-API-Key, merchantAccount and the HMAC key with those from Adyen's Customer Area.

Deviations in Veracity Pay:
- Sessions work with mode: hosted only. Drop-in and Components load from Adyen's own servers.
- /payments takes test card numbers; /payments/details (3DS and redirects), captures, reversals and tokenisation are missing.
- Notifications: AUTHORISATION, REFUND and CANCELLATION. OFFER_CLOSED and CAPTURE are not sent.


---

# Veracity Pay – Kustom flavour (formerly Klarna Checkout)

Base URL `https://kustom.pay.veracity.se` in place of https://api.playground.kustom.co – Kustom's own paths follow (also at `https://pay.veracity.se/kustom`). Auth: HTTP Basic, username = merchant id (any), password = secret key.

```
KUSTOM_BASE_URL=https://kustom.pay.veracity.se
KUSTOM_USERNAME=acct_…
KUSTOM_PASSWORD=vp_sk_test_…
```

1. `POST /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 } } → { order_id, status: "checkout_incomplete", html_snippet }. Render html_snippet on your checkout page.
2. After the purchase the customer goes to merchant_urls.confirmation ({checkout.order.id} replaced). `GET /checkout/v3/orders/{id}` → checkout_complete.
3. A POST without body arrives at merchant_urls.push; then `GET /ordermanagement/v1/orders/{id}` and `POST /ordermanagement/v1/orders/{id}/acknowledge`.
4. Captures `POST …/captures` { captured_amount }, refunds `POST …/refunds` { refunded_amount }, cancel `POST …/cancel`.

### Test outcomes

- Hosted checkout: each flavour follows that provider's order of steps; every verification (3-D Secure, Swish app, BankID) is a six-digit code e-mailed to the payer. A "Test mode" panel picks approve, decline (with a reason, including authentication_failed), pending, timeout or provider error. Invoice asks for a personnummer: use 19121212-1212.
- Magic cards: `4242424242424242` Visa – succeeds; `5555555555554444` Mastercard – succeeds; `4000000000000002` Declined: card declined; `4000000000009995` Declined: insufficient funds; `4000000000000069` Declined: expired card; `4100000000000019` Declined: suspected fraud; `4000000000003220` Requires 3-D Secure authentication; `4000000000000150` Pending, succeeds after 5 s; `4000000000000168` Pending, fails after 5 s; `4000000000000119` Provider error (500); `4000000000000184` Timeout – the session expires; `4000000000000341` Succeeds, but renewals fail.
- Magic amounts (total ends in these öre/cents, at least 1.00): `.01` Declined: card declined; `.02` Declined: insufficient funds; `.03` Declined: expired card; `.04` Declined: suspected fraud; `.05` Declined: credit check failed (invoice); `.06` Requires an extra authentication step; `.07` Pending, succeeds after 5 s; `.08` Pending, fails after 5 s; `.09` Timeout – the session expires.
- `metadata[veracity_test_outcome]` on a Checkout Session or PaymentIntent: `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]` seconds for pending/timeout).
- Without a browser: `POST /v1/test_helpers/checkout/sessions/{id}/complete` with `outcome` (MCP tool `complete_checkout`).
- Next renewal of a subscription: `POST /v1/test_helpers/subscriptions/{id}/next_charge_outcome` with `outcome` = approve | insufficient_funds | restricted_card | timeout (MCP tool `set_next_charge_outcome`).
- Time: `POST /v1/test_helpers/advance_clock` with `months` / `days` (MCP tool `advance_clock`); renewals are charged without a code and send invoice.paid or invoice.payment_failed.
## Going live and deviations from the original

Original documentation: [Kustom Checkout API](https://docs.kustom.co/contents/checkout/integrate-kco-in-your-ecommerce/create-order), [Kustom API URLs](https://docs.kustom.co/contents/api/api-basics/api-urls).

To go live, change only the host and the keys: Playground: `https://api.playground.kustom.co`; Production: `https://api.kustom.co`. Replace the username and password (HTTP Basic) with your Kustom credentials.

Deviations in Veracity Pay:
- Missing: updating an order (POST checkout/v3/orders/{id}) and the validation, address and shipping callbacks.
- No idempotency on captures and refunds, and refunds are not checked against captures.
- Push is sent once (retried on failure), not repeated until the order is acknowledged.
- Recurring payments (recurring_token) are not supported.


---

# Veracity Pay – Swish Commerce API (Swish Handel)

Base URL `https://pay.veracity.se/swish/<secret key>` in place of https://mss.cpc.getswish.net (the key in the base URL stands in for the client certificate; every Swish SDK lets you set the base URL). Paths and bodies are Swish's own: `/swish-cpcapi/api/v2/paymentrequests/{instructionUUID}`, `/swish-cpcapi/api/v1/paymentrequests/{id}`, `/swish-cpcapi/api/v2/refunds/{instructionUUID}`, `/recurring/merchant/v1/…`, `/qrg-swish/api/v1/commerce`.

## 1. Environment
```
SWISH_BASE_URL=https://pay.veracity.se/swish/vp_sk_test_…   # from the registration e-mail
SWISH_PAYEE_ALIAS=123…                       # your Veracity Swish number (payeeAlias)
```

## 2. Create a payment request
```http
PUT https://pay.veracity.se/swish/vp_sk_test_…/swish-cpcapi/api/v2/paymentrequests/{32 upper-case hex}
Content-Type: application/json

{"payeePaymentReference":"0123456789","callbackUrl":"https://your-app/api/swish/callback","payerAlias":"46712345678","payeeAlias":"<SWISH_PAYEE_ALIAS>","amount":"100.00","currency":"SEK","message":"Order 1001"}
```
201 with `Location`. Without `payerAlias` (m-commerce) the response also has `PaymentRequestToken`.

## 3. The payer approves
- e-commerce (payerAlias): the payer opens https://pay.veracity.se/swish/app, enters the number and approves with a code sent to their e-mail.
- m-commerce: send the payer to `https://pay.veracity.se/swish/paymentrequest?token=<PaymentRequestToken>&callbackurl=<your return URL>` (instead of `swish://paymentrequest?…`).
- Without a browser: `POST https://pay.veracity.se/v1/test_helpers/checkout/sessions/{instructionUUID}/complete` with `outcome` (MCP tool `complete_checkout`).
- MSS magic messages work: `message` equal to an early code (FF08, RP03, BE18, RP01, PA02, AM06, AM02, AM03, RP02, RP06, ACMT03, ACMT01, ACMT07, UNKW) gives 422 with that code; a late code (RF07, BANKIDCL, FF10, TM01, DS24) gives status ERROR with that code once the payer acts.

## 4. Callback and status
Swish POSTs the payment request object (PAID, DECLINED, ERROR or CANCELLED) to callbackUrl, with your `callbackIdentifier` as a header. Callbacks are not signed: re-read `GET …/v1/paymentrequests/{id}` before acting. Cancel with `PATCH` and `[{"op":"replace","path":"/status","value":"cancelled"}]` (Content-Type application/json-patch+json).

## 5. Refunds and recurring payments
- `PUT …/v2/refunds/{instructionUUID}` with originalPaymentReference (the payment's paymentReference), payerAlias (your Swish number), amount, currency, callbackUrl. Callbacks: DEBITED, then PAID.
- Recurring: `POST /recurring/merchant/v1/consents/{consentId}` (payer alias) → the payer signs in https://pay.veracity.se/swish/app; then `POST /recurring/merchant/v1/payments/single/{paymentId}` charges without the payer. `SIMULATE_ERROR_CODE` as a header simulates errors, like MSS.

## Going live and deviations from the original

Original documentation: [Swish Payment Request API v2](https://developer.swish.nu/api/payment-request/v2), [Swish Refunds API v2](https://developer.swish.nu/api/refunds/v2), [Swish Recurring Payments API v1](https://developer.swish.nu/api/recurring-payments/v1), [Swish QR codes](https://developer.swish.nu/api/qr-codes/v1), [Merchant Swish Simulator](https://developer.swish.nu/api/mss/v1).

To go live, change only the host and the keys: MSS (simulator): `https://mss.cpc.getswish.net`; Production: `https://cpc.getswish.net`. Remove the key from the base URL, add your Swish certificate (.p12) and replace payeeAlias with your real Swish number.

Deviations in Veracity Pay:
- No client certificate (mTLS): the account is identified by the key in the base URL ({host}/swish/vp_sk_test_…), by Authorization: Bearer or by payeeAlias.
- The payer approves in an emulated Swish app (/swish/app) with an e-mailed code; MSS instead marks everything paid after 4 seconds.
- m-commerce: open {host}/swish/paymentrequest?token=…&callbackurl=… instead of swish://paymentrequest?…; consents: {host}/swish/r?sw=…&ret=… instead of app.swish.nu.
- The QR code holds the link to the emulated app, not D + token, and JPEG is delivered as PNG.
- Callbacks may use http outside production. Payouts are missing.


---

# Veracity Pay – Svea Checkout and Payment Admin

Host `https://svea.pay.veracity.se` in place of both https://checkoutapistage.svea.com and https://paymentadminapistage.svea.com – the paths are Svea's own (`/api/orders…` for checkout, `/api/v1/…` for Payment Admin), so SDKs that only take a host work. Without that host, the same API is at `https://pay.veracity.se/svea/checkout` and `https://pay.veracity.se/svea/admin`.
Auth on every call: `Authorization: Svea base64(<merchant id>:sha512hex(body + secret + timestamp))` and `Timestamp: yyyy-MM-dd HH:mm` (UTC). Merchant id = your MID, secret = your secret key.

## 1. Environment
```
SVEA_CHECKOUT_BASE_URL=https://svea.pay.veracity.se
SVEA_ADMIN_BASE_URL=https://svea.pay.veracity.se
SVEA_CHECKOUT_MERCHANT_ID=<your MID>
SVEA_CHECKOUT_SECRET=vp_sk_test_…
```

## 2. Create an order
`POST /api/orders` with CountryCode "SE", Currency "SEK", Locale "sv-SE", ClientOrderNumber, MerchantSettings {TermsUri, CheckoutUri, ConfirmationUri, PushUri – `{checkout.order.uri}` is replaced by the order id} and Cart.Items [{ArticleNumber, Name, Quantity ×100, UnitPrice in minor units incl. VAT, VatPercent ×100, Unit}]. The answer has OrderId (integer), Status Created and Gui.Snippet (the iframe to put on the page).

## 3. After the purchase
Svea POSTs `{"type":"Finalized"}` to PushUri (signed X-Signature-512 = base64(HMAC-SHA512(secret, "{X-Timestamp}.{body}"))). Read `GET /api/orders/{id}`: Status Final and PaymentType (INVOICE, PAYMENTPLAN, SVEACARDPAY, SWISH, TRUSTLY).

## 4. Payment Admin
`GET /api/v1/orders/{id}` (Actions tells what is allowed), `POST /api/v1/orders/{id}/deliveries` ({OrderRowIds}), `POST …/deliveries/{deliveryId}/credits`, `PATCH /api/v1/orders/{id}` ({IsCancelled:true}). Tasks answer 202 with a queue Location that redirects (303) to the order. Without a browser: `POST https://pay.veracity.se/v1/test_helpers/checkout/sessions/{OrderId}/complete`.

## Going live and deviations from the original

Original documentation: [Svea Checkout API](https://paymentsdocs.svea.com/docs/checkout), [Svea authentication](https://paymentsdocs.svea.com/docs/getting-started/authentication), [Svea Payment Admin API](https://paymentsdocs.svea.com/docs/payment-admin).

To go live, change only the host and the keys: Stage (checkout): `https://checkoutapistage.svea.com`; Stage (admin): `https://paymentadminapistage.svea.com`; Production (checkout): `https://checkoutapi.svea.com`; Production (admin): `https://paymentadminapi.svea.com`. Replace the checkout merchant id (MID) and the secret with the ones you get from Svea.

Deviations in Veracity Pay:
- One host for both checkout and admin (svea.pay.veracity.se); Svea's paths tell them apart. Svea's PHP SDK and WooCommerce plugin hard-code Svea's hosts and need a patched base URL.
- The validation callback (CheckoutValidationCallBackUri) is not made, and callback subscriptions (webhooks v2) are missing; only PushUri gets a push.
- Recurring orders and /api/tokens answer 903 (Recurring not enabled). Adding, updating and replacing order rows in Payment Admin is not supported.
- Queue tasks finish at once (never InProgress), checkout orders last 24 h instead of 48 h, and admin orders do not expire.
- NationalId is always null and IsCompany always false; the merchant country is SE.


---

# Veracity Pay – Walley Checkout

Base URL `https://walley.pay.veracity.se` in place of https://api.uat.walleydev.com – Walley's own paths follow (also at `https://pay.veracity.se/walley`). Auth: OAuth2 client credentials at `/oauth2/v2.0/token` (client_id = your account id, client_secret = your secret key; any scope), then `Authorization: Bearer <access_token>`. The legacy SharedKey header works too (username = account id, shared key = secret key).

## 1. Environment
```
WALLEY_BASE_URL=https://walley.pay.veracity.se
WALLEY_CLIENT_ID=acct_…
WALLEY_CLIENT_SECRET=vp_sk_test_…
WALLEY_STORE_ID=<your MID>
WALLEY_WEBHOOK_SECRET=…        # HMAC key for Walley-Signature, if you use webhooks
```

## 2. Initialize a checkout
`POST /checkouts` with storeId, countryCode "SE", reference, notificationUri (`{checkout.id}` is replaced), redirectPageUri (`{checkout.publictoken}`), merchantTermsUri and cart.items [{id, description, unitPrice (SEK, decimal), quantity, vat (percent)}]. The answer is `{"id":…,"data":{"privateId","publicToken","expiresAt","paymentUri"},"error":null}`.

## 3. Show the checkout
`<script src="https://walley.pay.veracity.se/walley-checkout-loader.js" data-token="<publicToken>" data-lang="sv-SE"></script>` – the iframe is inserted where the script is (or in data-container-id). The event `walleyCheckoutPurchaseCompleted` fires on the page, and the top window goes to redirectPageUri.

## 4. After the purchase
Walley GETs notificationUri; then read `GET /checkouts/{privateId}`: status PurchaseCompleted, purchase.result (Preliminary for invoice, Completed for card and Swish) and order.orderId. Webhooks (register an endpoint with `veracity_format=walley`): walley:order:created/captured/refunded/canceled, signed `Walley-Signature = hex(HMAC_SHA256(key, "v0;" + Walley-Timestamp + ";" + body))`.

## 5. Order management
`GET /manage/orders/{orderId}`, `POST /manage/orders/{orderId}/capture` ({amount}), `/refund` ({amount, description}), `/cancel`, `/extend` → 202 with Location. Without a browser: `POST https://pay.veracity.se/v1/test_helpers/checkout/sessions/{privateId}/complete` with `outcome` and `payment_method_type`.

## Going live and deviations from the original

Original documentation: [Walley Checkout API](https://dev.walleypay.com/docs/checkout/endpoints), [Walley Management API](https://dev.walleypay.com/docs/manage/orders/get), [Walley webhooks](https://dev.walleypay.com/docs/webhooks/receiving).

To go live, change only the host and the keys: UAT: `https://api.uat.walleydev.com`; Production: `https://api.walleypay.com`. Replace client_id, client_secret, storeId and the webhook key with those from Walley Merchant Hub, and the loader script's host with Walley's.

Deviations in Veracity Pay:
- Business errors answer HTTP 409 with error.code 900; Walley uses HTTP status 900, which cannot be sent from here.
- validationUri and callback (ValidateOrder, PaymentFailed) are not called. reauthorize, customer tokens, B2B and pay links are missing.
- Order operations are applied at once (the answer is still 202 with Location), and orders do not expire on their own.
- The loader script is Veracity's: the same data attributes and events (walleyCheckout…/collectorCheckout…), but only PurchaseCompleted is sent.


---

# Veracity Pay – Qliro One

Base `https://qliro.pay.veracity.se` in place of https://pago.qit.nu – Qliro's own paths follow (also at `https://pay.veracity.se/qliro`). Auth: `Authorization: Qliro base64(sha256(rawBody + secret))` (raw digest; empty body for GET) and `MerchantApiKey` in the JSON body. MerchantApiKey = your MID, secret = your secret key.

## 1. Environment
```
QLIRO_BASE_URL=https://qliro.pay.veracity.se
QLIRO_MERCHANT_API_KEY=<your MID>
QLIRO_MERCHANT_API_SECRET=vp_sk_test_…
```

## 2. Create an order
`POST /checkout/merchantapi/Orders` with MerchantApiKey, MerchantReference, Currency, Country, Language, MerchantTermsUrl, MerchantConfirmationUrl, MerchantCheckoutStatusPushUrl, MerchantOrderManagementStatusPushUrl and OrderItems [{MerchantReference, Description, Type "Product", Quantity, PricePerItemIncVat, PricePerItemExVat, VatRate 0.25}]. The answer is 201 with OrderId and PaymentLink; `GET /checkout/merchantapi/Orders/{OrderId}` gives OrderHtmlSnippet for the page.

## 3. After the purchase
Qliro POSTs the checkout status (Completed) to MerchantCheckoutStatusPushUrl; answer `{"CallbackResponse":"received"}`. Pushes are not signed, so re-read the order before acting.

## 4. Order management (`/checkout/adminapi/v2/…`)
MarkItemsAsShipped, ReturnItems, CancelOrder, UpdateItems, UpdateMerchantReference (POST, MerchantApiKey, RequestId, OrderId in the body); `GET /checkout/adminapi/v2/orders/{id}` and `…/paymentTransactions/{id}`. Each operation sends a push to MerchantOrderManagementStatusPushUrl. Without a browser: `POST https://pay.veracity.se/v1/test_helpers/checkout/sessions/{OrderId}/complete`.

## Going live and deviations from the original

Original documentation: [Qliro One API](https://developers.qliro.com/docs/api), [Qliro authorization](https://developers.qliro.com/docs/qliro-checkout/get-started/authorization).

To go live, change only the host and the keys: Test: `https://pago.qit.nu`; Production: `https://payments.qit.nu`. Replace MerchantApiKey (MID) and the secret with the ones from Qliro.

Deviations in Veracity Pay:
- The OnHold and Refused flows and Qliro's test personal numbers are not emulated; PersonalNumber is always null.
- MerchantOrderValidationUrl is not called. Upsell, saved cards, AddItemsToInvoice, retryReversal and Admin API v1 are missing.
- Order-management transactions become Success at once (the answer still says Created), and orders last 24 h instead of 48 h.
- UpdateItems works only before the first shipment and cannot raise the total. The subtypes for Swish and Trustly are estimates.

