# Veracity Pay

> A test payment service provider built for AI-driven development. It emulates Stripe, Nets Easy, Adyen and Kustom (formerly Klarna Checkout): 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.

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.

## 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.
- 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.

# 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>`, 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 (point it at the base URL).

## 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: a "Test mode" panel picks approve, decline (with a reason), 3-D Secure, pending, timeout or provider error.
- 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.

---

# Veracity Pay – Nets Easy flavour

Base URL `https://pay.veracity.se/nets/v1` (instead of https://test.api.dibspayment.eu/v1). Auth: `Authorization: $NETS_EASY_SECRET_KEY` (no "Bearer", like Nets). Only `integrationType: "HostedPaymentPage"`.

```
NETS_EASY_BASE_URL=https://pay.veracity.se/nets
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: a "Test mode" panel picks approve, decline (with a reason), 3-D Secure, pending, timeout or provider error.
- 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.

---

# Veracity Pay – Adyen flavour

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

```
ADYEN_CHECKOUT_URL=https://pay.veracity.se/adyen/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: a "Test mode" panel picks approve, decline (with a reason), 3-D Secure, pending, timeout or provider error.
- 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.

---

# Veracity Pay – Kustom flavour (formerly Klarna Checkout)

Base URL `https://pay.veracity.se/kustom` (instead of https://api.playground.kustom.co). Auth: HTTP Basic, username = merchant id (any), password = secret key.

```
KUSTOM_BASE_URL=https://pay.veracity.se/kustom
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: a "Test mode" panel picks approve, decline (with a reason), 3-D Secure, pending, timeout or provider error.
- 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.
