# digid pay — full documentation corpus > Generated by scripts/gen_llms.mjs from the same sources as the site. ======================== / index ======================== --- type: Guide project: KRW-J2PB8 status: live audience: both description: digid pay developer documentation overview and status legend. --- # digid pay documentation Everything you need to integrate **digid pay** — an API-first online payment gateway. Accept card payments under **your brand** with a checkout your customers never leave, transparent per-transaction pricing, and a token-only trust boundary: card data never touches your servers **or ours**. The integration surface is one of two paths: - **Checkout snippet** — drop a script tag into your page; digid pay Secure Fields render inside it and do the rest (zero-API path). - **Payments API** — a REST API for building your own checkout UI around the same token capture. Both paths converge on the same object model: a **PaymentIntent** you create, confirm with a payment token, and follow through webhooks to a terminal state. ## Key facts | | | |---|---| | API base | `https://api.digid.cc/v1` | | Dashboard | `https://pay.digid.cc/dashboard/login` | | Signup | `https://digid.cc/signup` | | API contract file | `/openapi.yaml` (this site) | | Webhook signature | HMAC-SHA256, `Digidpay-Signature` header | | Card data | captured inside digid pay Secure Fields; only tokens cross into digid pay systems | ## Status legend Every page in this site carries an honest status in its frontmatter and is badged where it matters. Treat a page's claims within its status: | Badge | Meaning | |---|---| | **live** | The behaviour exists today and is what merchants rely on. | | **planned** | The capability is committed and specified but not yet provisioned for public use. | | **contract** | The interface is defined exactly as the facade will implement it, but the facade is not yet built. Spec-first: nothing invented beyond the agreed contract. | Quick orienting statuses: the **sandbox environment** exists and routes test payments today; the **open public sandbox keys** are `planned`; the **REST API**, **webhooks**, and **agent surfaces** are documented as `contract` until the facade ships. ## Reading order 1. [Quickstart](/quickstart) — get a sandbox payment working. 2. [Sandbox](/sandbox) — test rails, test cards, limits. 3. [Payments concept](/concepts/payments) — the PaymentIntent lifecycle. 4. [Tokens concept](/concepts/tokens) — why no card number ever touches you. 5. [API reference](/api/index) — the REST contract. 6. [Secure Fields](/checkout/secure-fields) — style the card-entry iframes. 7. [Agents](/agents/mcp) — MCP and A2A integration for AI agents. 8. [Operations](/operations/go-live) — from sandbox to live. ## Still exploring? - New here → [Quickstart](/quickstart) - Developer → [API overview](/api/index) - Compliance question → [PCI posture](/operations/pci-posture) - AI agent integration → [MCP server](/agents/mcp) · [A2A AgentCard](/agents/a2a) ======================== / quickstart ======================== --- type: Guide project: KRW-J2PB8 status: planned audience: developer description: Two sandbox paths to a first live digid pay payment, end to end. openapi_refs: - /payment_intents - /payment_intents/{payment_intent} - /payment_intents/{payment_intent}/confirm - /payment_intents/{payment_intent}/cancel --- # Quickstart > **Status note.** The sandbox environment is live today. Open **public** > sandbox keys are `planned` (provisioning task), and the REST API contract is > `contract` until the facade ships. The examples below are written against > that exact contract — placeholder keys are `pk_sandbox_…` / `sk_sandbox_…`. Two paths, same object model: - **Snippet path (recommended)** — zero-API: your server creates a PaymentIntent, digid pay Secure Fields on your page capture the card, and the snippet confirms the payment. You never build or host a card form. - **API path** — your own UI around the same Secure Fields capture, driving the REST API directly. Both start in the **sandbox**: no money moves, test rails only, and every request is authenticated with sandbox-scoped keys. ## 0. Get sandbox keys Request access through the signup flow at . Once your sandbox merchant account exists, create a key pair in the [digid pay dashboard](https://pay.digid.cc/dashboard/login) or over the API (see [Authentication](/api/authentication)). You will receive a `pk_sandbox_…` publishable key and a `sk_sandbox_…` secret key. The secret is shown **once** — store it in your secret manager and never expose it in client-side code. ## 1. Snippet path ### 1a. Server: create a PaymentIntent ```bash curl https://api.digid.cc/v1/payment_intents \ -u sk_sandbox_...: \ -H "Idempotency-Key: order-1001" \ -H "Content-Type: application/json" \ -d '{ "amount": 2490, "currency": "eur", "merchant_reference": "order-1001" }' ``` The response carries `id` (`pi_…`) and `client_secret`, plus `status: requires_payment_method`. ### 1b. Client: embed the snippet with your client_secret The checkout snippet loads once, then renders the Secure Fields and confirms on submit: ```html
``` ```js const checkout = await digidPay.checkout({ clientSecret: 'pi_..._secret_...', // server-provided, bound to the intent publishableKey: 'pk_sandbox_...', onEvent: (event) => { // 'payment_intent.succeeded' | 'payment_intent.payment_failed' | ... console.log(event.type, event.paymentIntent) }, }) checkout.mount('#digid-checkout') ``` The snippet captures the card in digid pay Secure Fields, tokenizes it inside the digid pay tokenization vault, confirms the PaymentIntent, and runs any required SCA challenge in place. When it finishes you receive the matching event and your server gets a webhook. ### 1c. Server: confirm from your backend (alternative to client confirm) If you prefer to confirm server-side, capture the token through Secure Fields on your own form (the [Secure Fields](/checkout/secure-fields) guide), then: ```bash curl https://api.digid.cc/v1/payment_intents/pi_.../confirm \ -u sk_sandbox_...: \ -H "Idempotency-Key: order-1001-confirm" \ -H "Content-Type: application/json" \ -d '{ "payment_method": "pmt_test_..." }' ``` ## 2. API path Build your own checkout UI around Secure Fields and call the REST API for every step: 1. **Create** the PaymentIntent ([above](#1a-server-create-a-paymentintent)). 2. **Render Secure Fields** with the publishable key and `client_secret` to capture the card and mint a payment token (see the [Secure Fields guide](/checkout/secure-fields)). 3. **Confirm** with the token ([above](#1c-server-confirm-from-your-backend)). 4. **Fulfil** on the `payment_intent.succeeded` webhook — never on a client callback alone. ```text Shopper → your page → digid pay Secure Fields → token → your server → POST /payment_intents/{id}/confirm → SCA if needed → webhook payment_intent.succeeded → fulfil ``` ## 3. Observe it Every state transition emits a webhook to any endpoint you register ([Webhooks](/api/webhooks)). Use the sandbox **test cards** in [Sandbox](/sandbox) to force declines, SCA challenges, and timeouts, and see each state in the dashboard transaction list. - `succeeded` with test card `4242 4242 4242 4242` - `requires_action` with the SCA test card - `failed` with a decline card - `cancelled` when you cancel before confirm ## 4. Go live When integration is green in sandbox, request activation from the dashboard. Activation is operator-reviewed ([Accounts](/concepts/accounts)); on approval your account gains live mode and you mint live keys. Follow [Go live](/operations/go-live) for the checklist — webhooks on, monitoring set, first real payment. ## Where next - [Sandbox](/sandbox) — test rails, keys, cards, limits. - [Payments API](/api/payments) — full request/response reference. - [Tokens](/concepts/tokens) — why card data never touches you. ======================== / sandbox ======================== --- type: Guide project: KRW-J2PB8 status: planned audience: developer description: "The digid pay sandbox: open sandbox keys, test cards, limits, and PCI integrity rules." openapi_refs: - /payment_intents - /payment_intents/{payment_intent} - /payment_intents/{payment_intent}/confirm - /keys --- # Sandbox > **Status.** The sandbox **environment is live**: it routes test payments, > mints keys, and settles nothing. The **open public sandbox** merchant and > its shared keys are `planned` — a provisioning task. Until then, sandbox > keys come from your own sandbox merchant account after signup. The sandbox is a full mirror of the production API against **test rails provisioned by digid pay**: - payments never move real money, - card data follows the same token-only rules as production, - everything is labelled `livemode: false`. ## Two key tiers | Tier | Keys | Status | |---|---|---| | **Your sandbox account** | `pk_sandbox_…` / `sk_sandbox_…` per account | `live` (after signup) | | **Open public sandbox** | shared `pk_sandbox_…` / `sk_sandbox_…` issued to a dedicated "Public Sandbox" merchant | `planned` | When the open sandbox provisions, anyone can call the API with the published keys and the **exact contract below** without signing up. ## Open sandbox contract (planned) Shared credentials will be published here and mirrored in the dashboard test page. They are enforced as follows: - **Sandbox-only routing.** A sandbox key that attempts a `livemode` PaymentIntent is rejected. - **Test-PAN only.** Card entry is token-only (rule R1); where a token is minted, only test PANs in the permitted BIN series succeed. Any raw `number`-shaped field in a payment request under a sandbox key is rejected with a structured error. - **Amount caps.** PaymentIntents over the sandbox cap are refused. - **Per-key rate limits.** Edge rate limits apply per key (see [Rate limits](/api/ratelimits)). - **No settlement.** The ledger is demo-only; nothing settles and no money moves. ## PCI integrity rules (binding for the open sandbox) The empty-card-data-environment story must hold even for anonymous traffic: 1. **R1 — Token-only ingestion.** The sandbox exposes the same capture model as production. Card entry happens through digid pay Secure Fields; PANs terminate in the vault's test project, never in request bodies or logs. Raw card fields are rejected for sandbox keys. 2. **R2 — Card-testing prevention.** Test-PAN whitelisting by BIN series, low amount caps, per-key rate limits and no settlement path make stolen-card validation against the sandbox worthless. 3. **R3 — Zero PAN retention.** The same log scrubbers used in production apply to sandbox traffic. If R1 ever regresses, scrubbers and no body-logging are the safety net. 4. **R4 — Sandbox ≠ evidence.** Sandbox flows are never described as PCI-scoped. The compliance argument rests on the production token-only architecture; a test-PAN-only sandbox with R1–R3 keeps systems out of a real card-data environment even under abuse. ## Test cards Use these in digid pay Secure Fields in sandbox. They never hit a real card network. | Card | Outcome | |---|---| | `4242 4242 4242 4242`, any future expiry, any CVC | succeeds | | `4000 0000 0000 0002` | declined (`card_declined`) | | `4000 0000 0000 0008` | declined — insufficient funds | | `4000 0000 0000 0005` | declined — expired card | | `4000 0000 0000 0127` | requires SCA (`requires_action`) | | any other valid-shape test PAN | declined | Expiry must be a future month; CVC any three digits. Full brand-specific test ranges are listed in the dashboard **test page** once you are signed in. > Test PAN `4242 4242 4242 4242` is a published test card — harmless. Real > card data never appears here, and never send a real PAN into sandbox (R1 > rejects it anyway). ## Trying it ```bash # Create a PaymentIntent in sandbox (test rails) curl https://api.digid.cc/v1/payment_intents \ -u sk_sandbox_...: \ -H "Idempotency-Key: sandbox-test-1" \ -H "Content-Type: application/json" \ -d '{ "amount": 1500, "currency": "eur", "merchant_reference": "sandbox-demo-1" }' ``` Then confirm it with a token captured from Secure Fields against the test card that produces the outcome you want to exercise: ```bash curl https://api.digid.cc/v1/payment_intents/pi_.../confirm \ -u sk_sandbox_...: \ -H "Idempotency-Key: sandbox-test-1-confirm" \ -H "Content-Type: application/json" \ -d '{ "payment_method": "pmt_test_..." }' ``` Expect `status: succeeded`, or `requires_action` / `failed` matching the card you used. Every transition also emits a webhook. ## Hard limits in sandbox | Limit | Value | |---|---| | Max amount per PaymentIntent | documented in dashboard test page (low) | | Per-key request rate | see [Rate limits](/api/ratelimits) | | Settlement | none — ledger demo only | | Raw card fields | rejected (R1) | | Live-mode intents under sandbox keys | rejected | ## Between sandbox and live Sandbox and live are **modes on the same merchant account** — same dashboard, same object model, separate key pairs (`sandbox` vs `live`). Live keys are only minted after activation ([Accounts](/concepts/accounts)). A sandbox key never moves money; a live key never touches test rails. ======================== / concepts/payments ======================== --- type: Concept project: KRW-J2PB8 status: contract audience: developer description: The PaymentIntent lifecycle, state machine and SCA pauses. --- # Payments A **PaymentIntent** is the digid pay record of a single payment attempt. You create it with an amount and currency, confirm it with a payment token from digid pay Secure Fields, and follow it through webhooks to a terminal state. ## Lifecycle ```mermaid stateDiagram-v2 [*] --> requires_payment_method: create requires_payment_method --> processing: confirm with token requires_payment_method --> cancelled: cancel requires_action --> processing: SCA completed processing --> succeeded processing --> failed processing --> cancelled requires_approval --> processing: human approval requires_approval --> cancelled: declined succeeded --> [*] failed --> [*] cancelled --> [*] ``` States: | State | Meaning | |---|---| | `requires_payment_method` | Created, waiting to be confirmed with a payment token. | | `processing` | Confirmed; the acquirer is authorising. | | `requires_action` | Paused for an SCA (3-D Secure) challenge. Continues automatically once authenticated. | | `requires_approval` | Agent-initiated payment paused until a human approves (see [Agents](/agents/mcp)). | | `succeeded` | Authorised and captured. Terminal. | | `failed` | Declined or could not be processed. Terminal. | | `cancelled` | Cancelled before completion. Terminal. | ## Create and confirm ```bash curl https://api.digid.cc/v1/payment_intents \ -u sk_live_...: \ -H "Idempotency-Key: order-2041" \ -H "Content-Type: application/json" \ -d '{ "amount": 4990, "currency": "eur", "merchant_reference": "order-2041" }' ``` ```json { "id": "pi_1Ab...", "object": "payment_intent", "amount": 4990, "currency": "eur", "status": "requires_payment_method", "livemode": true, "client_secret": "pi_1Ab..._secret_...", "created_at": "2026-09-08T09:00:00Z" } ``` `client_secret` is bound to the intent and is the only secret your client-side code should hold. Confirm server-side: ```bash curl https://api.digid.cc/v1/payment_intents/pi_1Ab.../confirm \ -u sk_live_...: \ -H "Idempotency-Key: order-2041-confirm" \ -H "Content-Type: application/json" \ -d '{ "payment_method": "pmt_..." }' ``` The response reflects the new state — `processing`, `requires_action` (SCA), or a terminal state. ## Idempotency Every mutating request accepts an `Idempotency-Key` header. Replaying the same key returns the original result and never creates a second charge: ```bash curl https://api.digid.cc/v1/payment_intents/pi_1Ab.../confirm \ -u sk_live_...: \ -H "Idempotency-Key: order-2041-confirm" \ -H "Content-Type: application/json" \ -d '{ "payment_method": "pmt_..." }' ``` Send the same key again → same response, no new authorisation. A mismatch (e.g. a reused key with different payload) returns `idempotency_key_reused`. ## Money Amounts are integers in the currency's **minor units** (cents for `eur`, pence for `gbp`). Never use floats. ```json { "amount": 1000, "currency": "eur" } // €10.00 { "amount": 1500, "currency": "dkk" } // kr15.00 ``` ## Retrieve and list ```bash curl https://api.digid.cc/v1/payment_intents/pi_1Ab... -u sk_live_...: curl "https://api.digid.cc/v1/payment_intents?limit=25&starting_after=pi_..." -u sk_live_...: ``` Cross-tenant resources return `404`, never `403`, so existence is not leaked between merchants. ## Agent-initiated payments Intents created by an agent begin in `requires_approval` and only proceed after an explicit human approval: ```bash curl https://api.digid.cc/v1/payment_intents/pi_1Ab.../approve \ -u sk_live_...: \ -H "Idempotency-Key: approval-order-2041" \ -H "Content-Type: application/json" ``` See [Agents](/agents/mcp) for the full agent flow and approval semantics. ## Refunds Refunds are separate objects tied to a `succeeded` PaymentIntent — full or partial, never exceeding the original capture. See [Refunds](/api/refunds). ======================== / concepts/tokens ======================== --- type: Concept project: KRW-J2PB8 status: contract audience: developer description: The digid pay tokenization vault model and the PAN-pattern ban. --- # Tokens & card data digid pay is **token-only by construction**. A full card number exists in exactly one place during checkout — inside digid pay Secure Fields, the hosted tokenization iframes rendered on your page. What leaves those fields is a **token**; what your servers, digid pay systems, logs and database ever see is that token and payment metadata. Never a PAN. ## The model ```mermaid sequenceDiagram participant Shopper participant Your page participant Secure Fields participant Vault as digid pay tokenization vault participant digidpay as digid pay API Shopper->>Secure Fields: types card details Secure Fields->>Vault: card data (never touches your DOM or servers) Vault-->>Secure Fields: payment token Secure Fields-->>Your page: token + brand + last4 Your page->>digidpay: confirm PaymentIntent with token ``` ### What merchants never touch - A full PAN — never in your DOM, logs, database, or requests. - CVV — never stored anywhere after the Secure Fields capture. - Raw card-number API fields — the digid pay API **rejects** any `number`-shaped 13–19 digit field in payment requests (including in sandbox). There is no path to submit a raw card through the API. ### What you do receive | Value | Purpose | |---|---| | `payment_method` token (`pmt_…`) | Confirms a PaymentIntent; never usable to retrieve the card. | | Brand (`visa`, `mastercard`, …) | UI/branding only. | | Last four digits | Receipt / display confirmation. | ## The PAN-pattern ban digid pay systems run PAN-pattern scrubbers across logs (the same rule that guards production applies in sandbox — rule R3). There is no code path, log, export or repository that may contain a PAN. The API contract itself refuses card-number fields so the boundary is enforced at the edge, not only in policy. ## Why your PCI burden stays low Because card entry happens entirely inside the digid pay tokenization vault — never in your DOM or on your servers — your own checkout qualifies for the lowest SAQ class. Our [PCI posture page](/operations/pci-posture) spells out the merchant wording; digid pay operates as a service provider under its own self-assessment with an empty card-data environment. ## Token lifecycle | Lifecycle | Note | |---|---| | Created | When Secure Fields tokenizes a card (per capture or on-file). | | Used | To confirm a PaymentIntent. | | Expired / invalid | Failed confirmations surface `expired_token`, `invalid_token`, or `used_token` — a token can only charge what it was created for. | ## Rules of thumb 1. Never log request bodies that could contain token-adjacent card metadata. 2. Never attempt to build your own card form — that defeats the model. 3. Treat `pmt_` tokens as single-purpose: mint fresh from Secure Fields for each checkout, and use on-file tokens only for the flows that document them. 4. If a vendor asks you for a raw PAN to "test", stop — that is not how digid pay works (see [Sandbox rules](/sandbox)). ## Further reading - [Secure Fields](/checkout/secure-fields) — capture tokens styled to your brand. - [Payments](/concepts/payments) — confirm intents with tokens. - [PCI posture](/operations/pci-posture) — SAQ A wording for merchants. ======================== / concepts/accounts ======================== --- type: Concept project: KRW-J2PB8 status: planned audience: both description: digid pay merchant accounts, sandbox/live modes, and the activation gate. --- # Accounts & modes A **merchant account** is a fully segregated tenant on digid pay: its own keys, plans, transactions, webhook endpoints and settlement views. Nothing crosses between merchants — a request authenticated for merchant A can never read merchant B's data (cross-tenant lookups return `404`). ## Account states ```mermaid stateDiagram-v2 [*] --> sandbox: signup sandbox --> pending_activation: submit for activation pending_activation --> live: operator approval live --> suspended: operator action suspended --> live: operator action suspended --> closed live --> closed pending_activation --> sandbox: request changes ``` | State | Meaning | |---|---| | `sandbox` | Auto-created at signup. Full API + test rails; no real money. | | `pending_activation` | You submitted for live access; operator review in progress. | | `live` | Real payments; live keys minted. | | `suspended` | Live traffic stopped (keys revoked, connector disabled, webhooks halted). | | `closed` | Terminated account. | ## Modes Sandbox and live are **modes on the same account**, not separate accounts: | | Sandbox | Live | |---|---|---| | Keys | `pk_sandbox_…` / `sk_sandbox_…` | `pk_live_…` / `sk_live_…` | | Rails | test rails provisioned by digid pay | real acquiring via our European acquiring partner | | Money | none moves | real, settles directly to you | | Access | at signup | after activation | A sandbox key can never initiate a live-mode intent; a live key can never hit test rails. ## Signup Signup is self-serve () and immediately provisions an isolated sandbox account with a key pair — no human in the loop. Email verification is required; MFA is enforced at first login. > Status: signup + auto-provisioned sandbox is `planned` (being built). Once > shipped this page flips to `live`. ## Activation Going live is **operator-gated** — never automatic: 1. From the dashboard, submit your account for activation (confirm your business details and plan). 2. The digid pay operator reviews the request (FR-2). 3. On approval your account moves to `live`, you are notified, and live keys can be minted. Every activation/revocation carries an operator approval record. There is no path from signup to live capability without that recorded approval. ## Multi-merchant platform operators An agency or platform can hold many merchant accounts, each segregated. A platform's own account is itself just a merchant account — there is no implicit visibility across the merchants it manages unless each consents and is configured accordingly. ## Related - [Plans & pricing](/concepts/plans) — what you pay per transaction. - [Authentication](/api/authentication) — key pairs per mode. - [Go live](/operations/go-live) — the activation checklist. ======================== / concepts/plans ======================== --- type: Concept project: KRW-J2PB8 status: planned audience: both description: digid pay plans, per-transaction pricing, and what digid pay is not. --- # Plans & pricing Each merchant account is assigned exactly one **plan**. A plan is the pricing arrangement for that merchant — a per-transaction rate and, optionally, a monthly flat fee — agreed up front and visible in the dashboard. digid pay keeps the model deliberately simple: | Component | Meaning | |---|---| | **Per-transaction rate** | A basis-points (bps) rate on each successful live charge. | | **Monthly flat fee** (optional) | A fixed monthly fee, when a plan has one. | | **Effective rate** | What you actually pay per transaction, shown in the dashboard plan summary. | > **Status.** Plan assignment and the plan-summary views are `planned` > (being built). The exact tariff catalogue is a business decision that will > be reflected here and in the dashboard when set. ## Where the numbers live - The dashboard **Plan** page shows your current plan and effective rate. - Plan changes are versioned (plan + effective-from date per merchant). - digid pay never adjusts the rate silently: any plan change is visible before it takes effect. ## What digid pay is not - **Not an acquirer.** digid pay is not a licensed payment institution and does not hold or route merchant funds. Card acquiring and settlement run under **our European acquiring partner** (a regulated acquirer). digid pay earns a margin per the merchant's plan; funds flow from the acquirer directly to you. - **No hidden fee stack.** No monthly-minimum games, no opaque markups. One clear per-transaction model. - **No minimums games.** The rate you agree is the rate you pay. ## Example A plan at 150 bps + no monthly fee on a €100.00 charge: - amount: 10000 minor units (`eur`) - digid pay margin: €1.50 - you receive: €98.50 (settled by the acquirer directly to your account) Your dashboard shows this split on the transaction and in periodic summaries. ## Related - [Settlement](/operations/settlement) — how money reaches you (never through digid pay). - [Accounts & modes](/concepts/accounts) — the account lifecycle behind plans. ======================== / checkout/snippet ======================== --- type: Guide project: KRW-J2PB8 status: planned audience: developer description: Embed reference for the digid pay checkout snippet, SRI pinning, data attributes and lifecycle. --- # Checkout snippet > **Status.** The snippet and its immutable asset host are `planned` (build > tracks to FR-12/FR-13). The contract below is what will ship; version and > SRI hashes are published here as assets land. The checkout snippet is the **zero-API integration path**: one ` ``` - A published version is **never mutated in place**. Upgrades ship as a new version you opt into. - Assets are served with `Cache-Control: immutable`. - If the served bytes do not match the SRI hash, the browser **fails closed** — there is no silent downgrade path. You can also load it without a pinned version for convenience, but pinning the integrity hash is strongly recommended (it is what the script-integrity evidence trail expects). ## Initialise The snippet exposes a global once loaded: ```js const checkout = await digidPay.checkout({ publishableKey: 'pk_sandbox_...', // sandbox or live clientSecret: 'pi_..._secret_...', // server-provided, bound to the intent onEvent: handleEvent, }) checkout.mount('#digid-checkout') ``` | Option | Required | Purpose | |---|---|---| | `publishableKey` | yes | Identifies your account + mode (client-safe). | | `clientSecret` | yes | Binds this checkout to one PaymentIntent; the only secret the browser ever holds. | | `onEvent` | yes | Callback contract (below). | | `paymentMethods` | no | Restrict card brands (subset for Secure Fields). | | `style` | no | Field styling tokens (see [Secure Fields](/checkout/secure-fields)). | ## PaymentIntent lifecycle driven by the snippet 1. Your server creates a PaymentIntent (`requires_payment_method`) and passes `client_secret` to the page. 2. `digidPay.checkout({...})` renders Secure Fields into the mount node. 3. On submit, the card is captured inside the vault iframes, tokenized, and the snippet **confirms** the PaymentIntent (`payment_intent.succeeded` on success, `requires_action` handled inline for SCA). 4. Your server fulfils on the webhook — never on a client callback alone. ## Callback contract `onEvent(event)` fires on the merchant-relevant transitions: | Event | Meaning | |---|---| | `ready` | Fields mounted and interactive. | | `change` | Field state changed (`{complete, empty, brand}`). | | `payment_intent.processing` | Confirmed; authorising. | | `payment_intent.requires_action` | SCA challenge in progress (handled in-frame; see [SCA](/operations/sca-3ds)). | | `payment_intent.succeeded` | Authorised and captured. | | `payment_intent.payment_failed` | Declined — surface the error and allow retry. | ```js function handleEvent(event) { switch (event.type) { case 'payment_intent.succeeded': // show success; server will also get the webhook break case 'payment_intent.payment_failed': // event.error.code / event.error.message break } } ``` ## Where digid pay shows up Your shoppers interact with your page; the card-entry iframes are digid pay Secure Fields under the hood with a **minimal, merchant-controllable** digid pay presence. The code is integrity-governed, not chrome-governed — you keep the look. ## Loading considerations - Load the snippet from the asset host directly (do not self-host a copy — the SRI pin assumes the canonical bytes). - If the snippet fails the integrity check the page should degrade to your existing fallback; do not attempt to load an unpinned copy silently. ## Related - [Secure Fields](/checkout/secure-fields) — the styling guide underneath. - [Quickstart](/quickstart) — the snippet path end to end. ======================== / checkout/secure-fields ======================== --- type: Guide project: KRW-J2PB8 status: planned audience: developer description: Styling and accessibility guide for digid pay Secure Fields, per-field iframes and SAQ A wording. --- # digid pay Secure Fields > **Status.** Secure Fields are `planned` for public provisioning; this page > is the styling + integration contract (per-field iframes, state classes, > a11y, brand subsets, SCA in-frame, SAQ A wording). **digid pay Secure Fields** are the hosted tokenization iframes that capture card data on your page. Each sensitive input (card number, CVC) lives in its **own invisible iframe** served from the digid pay tokenization vault, so the card data never touches your DOM, your servers, or digid pay systems beyond the vault boundary. ## Why per-field iframes - Card data is typed **inside the vault boundary**, never in your page. - Each field is isolated: your scripts cannot reach into a field's iframe. - You still control how they **look and behave** — through a styling API, not by touching the card data. ## Basic capture (snippet) The simplest path uses the [checkout snippet](/checkout/snippet); Secure Fields are created, styled and mounted for you: ```js const checkout = await digidPay.checkout({ publishableKey: 'pk_sandbox_...', clientSecret: 'pi_..._secret_...', paymentMethods: ['visa', 'mastercard'], onEvent: handleEvent, }) checkout.mount('#digid-checkout') ``` ## Styling guide Fields accept a `style` object (or data attributes on the mount node). Supported properties per field: | Concern | Options | |---|---| | Base | `base: { color, fontSize, fontFamily, fontWeight, letterSpacing, textAlign, padding, placeholderColor }` | | Focus | `focus: { color, borderColor, boxShadow }` | | Toggled states | `valid`, `invalid`, `empty`, `identified` (each a style object) | | Placeholder | via `placeholderColor` on base (see a11y below) | | aria | `ariaLabel` per field (see a11y below) | ```js const style = { base: { color: '#0f172a', fontSize: '16px', fontFamily: 'Inter, sans-serif', placeholderColor: '#64748b', padding: '12px', }, focus: { borderColor: '#4338ca', boxShadow: '0 0 0 3px #eef2ff' }, valid: { color: '#0f172a' }, invalid: { color: '#b91c1c' }, empty: { color: '#0f172a' }, } ``` State classes are applied to a wrapper you control so you can style the surrounding field container: | Class | When | |---|---| | `.digid-field--valid` | Input is complete and valid. | | `.digid-field--invalid` | Input failed validation (e.g. bad Luhn, wrong CVC length). | | `.digid-field--empty` | Input is empty. | | `.digid-field--identified` | The card brand has been identified from the number. | ```css .digid-field--invalid { border-color: #b91c1c; } .digid-field--identified .digid-card-brand { display: block; } ``` ## Accessibility - Provide a **visible label** and set `ariaLabel` on each field (number, expiry, CVC). Never rely on the placeholder as the only label. - `placeholderColor` must meet contrast against the field background. - Respect `prefers-reduced-motion` if you animate focus states. - Keep focus visible: the `focus` style should always produce a distinguishable outline/ring. ```js digidPay.createField('cardNumber', { ariaLabel: 'Card number', placeholder: '1234 5678 9012 3456', }) ``` ## Card brand subset Limit accepted brands with `paymentMethods` — an unlisted brand is not submitted: ```js paymentMethods: ['visa', 'mastercard', 'amex'] ``` Supported brand tokens are documented in the dashboard test page; they match the scheme names (Visa, Mastercard, …). ## No external resources inside fields Secure Fields must stay self-contained: - **No external stylesheets or fonts may load inside the fields' iframes.** - To use a custom font, **upload it to your digid pay configuration** and reference it by name in `style.base.fontFamily`. This is what keeps the field surface stable, dependency-free and auditable. ## SCA in the frame When a card requires 3-D Secure, the challenge runs **inside the checkout flow** — no page navigation, no new tab (see [SCA & 3-D Secure](/operations/sca-3ds)). The snippet surfaces `payment_intent.requires_action` while the challenge is in progress and continues automatically. ## SAQ A wording for merchants Using digid pay Secure Fields means card entry happens entirely inside digid pay's vault boundary — **never in your DOM or servers** — which reduces your PCI burden to **SAQ A**. The wording you can rely on: > Card entry happens entirely inside digid pay's vault boundary, never in our > DOM or servers, so our payment page qualifies for SAQ A under the card-brand > self-assessment questionnaires. digid pay operates as a service provider under its own self-assessment with an empty card-data environment. See [PCI posture](/operations/pci-posture) for the full merchant-facing statement. ## Related - [Snippet](/checkout/snippet) — mount Secure Fields in one tag. - [Tokens](/concepts/tokens) — what crosses the vault boundary (only tokens). - [PCI posture](/operations/pci-posture) — the merchant SAQ A story. ======================== / api/index ======================== --- type: API Reference project: KRW-J2PB8 status: contract audience: both description: digid pay REST API overview, base URL, versioning and the OpenAPI contract file. --- # API reference > **Status.** The digid pay REST API is `contract`: the facade that serves it > is being built to exactly this surface. Nothing below is invented beyond > the agreed contract; when the facade ships, `api.digid.cc/openapi.json` > publishes the same file this site renders. The digid pay API is a REST API over HTTPS. JSON is returned in all responses, including errors. All timestamps are RFC 3339. ## Base URL ``` https://api.digid.cc/v1 ``` All endpoints in this reference are relative to that base. The same host and contract serve **sandbox and live**; which rail you hit is decided by the key you authenticate with (`pk_sandbox_…`/`sk_sandbox_…` vs `pk_live_…`/`sk_live_…`). ## Versioning policy - The current version is pinned in the URL path (`/v1`). - Backwards-incompatible changes ship under a new major (`/v2`) with notice; backwards-compatible additions ship within `/v1`. - The machine-readable contract is the file this site renders in the playground, also downloadable at [openapi.yaml](/openapi.yaml). ## Try it The interactive playground renders the contract file client-side (Scalar embed, loaded from its public CDN) and lets you exercise requests against the sandbox. Sandbox keys are `planned`; until they are public, use keys from your own sandbox account. Alternatively, run the same requests directly with curl: ```bash curl https://api.digid.cc/v1/payment_intents \ -u sk_sandbox_...: \ -H "Content-Type: application/json" \ -d '{ "amount": 1000, "currency": "eur" }' ``` ## Conventions - **Authentication** — `Authorization: Bearer `; publishable keys are client-safe, secret keys are server-only and rejected from browser contexts. See [Authentication](/api/authentication). - **Idempotency** — send an `Idempotency-Key` header on mutating requests to make retries safe. - **Money** — integer minor units + ISO 4217 currency. Never floats. - **Errors** — a stable envelope: `{ error: { type, code, message, param } }`. See [Errors](/api/errors). - **Rate limits** — enforced at the edge per key; see [Rate limits](/api/ratelimits). ## Resources | Resource | Base path | Read more | |---|---|---| | PaymentIntents | `/payment_intents` | [Payments](/api/payments) | | Refunds | `/refunds` | [Refunds](/api/refunds) | | Webhook endpoints | `/webhook_endpoints` | [Webhooks](/api/webhooks) | | API keys | `/keys` | [Authentication](/api/authentication) | ## OpenAPI contract file The full contract (the source this site renders) lives at [`/openapi.yaml`](/openapi.yaml) and will be published by the facade at `api.digid.cc/openapi.json` when live. ======================== / api/authentication ======================== --- type: API Reference project: KRW-J2PB8 status: contract audience: developer description: API key pairs, bearer auth, browser-UA rejection, rotation and revocation. openapi_refs: - /keys - /keys/{key_id} - /keys/{key_id}/rotate --- # Authentication Every digid pay API request is authenticated with an **API key pair** scoped to your merchant account and to one mode (`sandbox` or `live`). ## Key pairs | Key | Prefix | Use | |---|---|---| | Publishable key | `pk_live_…` / `pk_sandbox_…` | Client-safe: identifies your account, renders Secure Fields, never authorises server actions. | | Secret key | `sk_live_…` / `sk_sandbox_…` | Server-only: authenticates API requests. Shown **once** at creation. | Authentication: ``` Authorization: Bearer sk_live_... ``` Send the key as a Bearer token. Everything over HTTPS; the key is the credential — guard it like a password. ## Browser-UA rejection (FR-7) Secret keys are **server-side only**. If a request carrying a secret key originates from a browser-like context (a browser User-Agent), digid pay rejects it with a specific error code and alerts the merchant in the dashboard, where one click rotates the key: ```json { "error": { "type": "authentication_error", "code": "secret_key_from_browser", "message": "Secret keys cannot be used from browser contexts.", "param": null } } ``` Use the **publishable key** in client code; keep the secret key on your backend. ## Create keys You can create keys in the dashboard or over the API: ```bash curl https://api.digid.cc/v1/keys \ -u sk_live_...: \ -H "Content-Type: application/json" \ -d '{ "label": "production-webhook-backend", "mode": "live" }' ``` ```json { "publishable_key": "pk_live_...", "secret_key": "sk_live_...", "note": "The secret key is shown only once." } ``` The secret is **shown exactly once** and stored hashed-at-rest (compare-on-use, never retrievable again). Store it immediately in your secret manager. ## List keys Key **metadata** (label, mode, prefix, last used, created) is always retrievable; secret values are not: ```bash curl https://api.digid.cc/v1/keys -u sk_live_...: ``` ## Rotate Rotation issues a new secret and retires the old one. Any key you identify as exposed should be rotated immediately: ```bash curl -X POST https://api.digid.cc/v1/keys/key_.../rotate \ -u sk_live_...: \ -H "Idempotency-Key: rotate-key-17" \ -H "Content-Type: application/json" ``` The response contains the new `secret_key` once. Rotate the same key again to issue another pair; a rotated-out secret stops authenticating. ## Revoke Revocation is **instant** — a revoked key fails within seconds across API and webhook surfaces: ```bash curl -X DELETE https://api.digid.cc/v1/keys/key_... -u sk_live_...: ``` ## Per-key scopes Keys may carry optional scope labels restricting what they can do (for example a read-only key for monitoring). Scope enforcement is part of the facade contract; keys without scopes are unrestricted for their mode. ## Misuse detection digid pay watches for obvious secret-key leakage signals (browser-context use, suspicious geography, unexpected bursts) and surfaces an alert in the dashboard with a one-click rotate action. Treat any alert as an active exposure: rotate, then investigate. ## HMAC & integrity Key material never travels except as the Bearer credential over TLS. Webhook **payloads** are additionally signed per merchant with HMAC-SHA256 (see [Webhooks](/api/webhooks)); if you need request signing on the API itself, the pattern mirrors the webhook scheme — contact support before relying on it, since it is not yet part of the documented contract. ## Further reading - [Errors](/api/errors) — `authentication_error` vs `invalid_request_error`. - [Security](/operations/security) — key handling runbook for operators. - [Rate limits](/api/ratelimits) — per-key limits. ======================== / api/payments ======================== --- type: API Reference project: KRW-J2PB8 status: contract audience: developer description: "PaymentIntent endpoints: create, list, retrieve, confirm, cancel, approve." openapi_refs: - /payment_intents - /payment_intents/{payment_intent} - /payment_intents/{payment_intent}/confirm - /payment_intents/{payment_intent}/cancel - /payment_intents/{payment_intent}/approve --- # Payments PaymentIntents are the core resource. Full lifecycle + state machine: [Payments concept](/concepts/payments). Base path: `/v1/payment_intents` ## Create ```bash curl https://api.digid.cc/v1/payment_intents \ -u sk_live_...: \ -H "Idempotency-Key: order-1001" \ -H "Content-Type: application/json" \ -d '{ "amount": 2490, "currency": "eur", "merchant_reference": "order-1001", "metadata": { "customer_id": "cus_123" } }' ``` | Field | Type | Notes | |---|---|---| | `amount` | int | Minor units (cents for `eur`). Required. | | `currency` | string | ISO 4217. Required. | | `merchant_reference` | string | Your order reference. | | `description` | string | Human-readable descriptor. | | `metadata` | object | Free-form key/value strings. | | `capture_method` | `automatic` \| `manual` | `automatic` at launch; manual capture is a planned contract extension. | Response: ```json { "id": "pi_1Ab...", "object": "payment_intent", "amount": 2490, "currency": "eur", "status": "requires_payment_method", "livemode": true, "client_secret": "pi_1Ab..._secret_...", "created_at": "2026-09-08T09:00:00Z" } ``` ## Confirm Confirm with a payment token from digid pay Secure Fields: ```bash curl https://api.digid.cc/v1/payment_intents/pi_1Ab.../confirm \ -u sk_live_...: \ -H "Idempotency-Key: order-1001-confirm" \ -H "Content-Type: application/json" \ -d '{ "payment_method": "pmt_..." }' ``` `payment_method` is the token returned by Secure Fields. Confirmations never accept raw card fields — a `number`-shaped field is rejected (`invalid_request_error`, code `raw_card_rejected`) per the token-only contract. Possible resulting statuses: | Status | Meaning | |---|---| | `processing` | Authorising. | | `requires_action` | SCA challenge pending (see [SCA](/operations/sca-3ds)). | | `succeeded` | Authorised and captured. | | `failed` | Declined (`card_declined`, `insufficient_funds`, …). | ## Retrieve ```bash curl https://api.digid.cc/v1/payment_intents/pi_1Ab... -u sk_live_...: ``` ## List ```bash curl "https://api.digid.cc/v1/payment_intents?limit=25&starting_after=pi_..." -u sk_live_...: ``` Pagination is cursor-based: `has_more` + `starting_after` (see [Rate limits & pagination](/api/ratelimits)). ## Cancel Cancels an intent that has not reached a terminal state. Allowed from `requires_payment_method`, `requires_action`, and `requires_approval`: ```bash curl -X POST https://api.digid.cc/v1/payment_intents/pi_1Ab.../cancel \ -u sk_live_...: \ -H "Idempotency-Key: order-1001-cancel" \ -H "Content-Type: application/json" ``` `status` becomes `cancelled`; a `payment_intent.cancelled` webhook fires. ## Approve (agent-initiated payments) Intents created by an agent start in `requires_approval`. A designated human approver moves them to `processing`: ```bash curl -X POST https://api.digid.cc/v1/payment_intents/pi_1Ab.../approve \ -u sk_live_...: \ -H "Idempotency-Key: approval-order-1001" \ -H "Content-Type: application/json" ``` See [Agents](/agents/mcp) for approval semantics. Declining an agent intent = cancelling it. ## Idempotency Every mutating call accepts `Idempotency-Key`. Replays with the same key return the original result with no second authorisation; a reused key with a different payload returns `idempotency_key_reused`. ## Webhooks Every state transition emits a webhook (see [Webhooks](/api/webhooks)): `payment_intent.processing`, `.succeeded`, `.payment_failed`, `.cancelled`, `.requires_action`, `.requires_approval`. ## Further reading - [Errors](/api/errors) — the envelope and codes. - [Refunds](/api/refunds) — reverse a `succeeded` intent. ======================== / api/refunds ======================== --- type: API Reference project: KRW-J2PB8 status: contract audience: developer description: Refund endpoints and state machine, full and partial. openapi_refs: - /refunds - /refunds/{refund} --- # Refunds Refunds reverse a `succeeded` PaymentIntent, in full or in part, within acquiring rules. Base path: `/v1/refunds` ## Create ```bash curl https://api.digid.cc/v1/refunds \ -u sk_live_...: \ -H "Idempotency-Key: refund-order-1001" \ -H "Content-Type: application/json" \ -d '{ "payment_intent": "pi_1Ab...", "amount": 2490, "reason": "requested_by_customer" }' ``` | Field | Type | Notes | |---|---|---| | `payment_intent` | string | Required. The `succeeded` intent to refund. | | `amount` | int | Minor units. Omit for a full refund. | | `reason` | `duplicate` \| `fraudulent` \| `requested_by_customer` | Informational. | | `metadata` | object | Free-form. | Constraints (FR-9): - Refund amount ≤ original capture amount — excess is rejected. - A refunded intent can be refunded again only if a positive balance remains (partial refunds are additive up to the capture amount). Response: ```json { "id": "re_1Cd...", "object": "refund", "amount": 2490, "currency": "eur", "payment_intent": "pi_1Ab...", "status": "pending", "created_at": "2026-09-08T09:30:00Z" } ``` ## State machine ```mermaid stateDiagram-v2 [*] --> pending: create pending --> succeeded pending --> failed succeeded --> [*] failed --> [*] ``` `pending` → `succeeded` normally; `failed` means the acquirer rejected the refund (e.g. the original capture is being disputed). A `refund.succeeded` or `refund.failed` webhook fires on the transition. ## Retrieve ```bash curl https://api.digid.cc/v1/refunds/re_1Cd... -u sk_live_...: ``` ## List ```bash curl "https://api.digid.cc/v1/refunds?payment_intent=pi_1Ab...&limit=25" -u sk_live_...: ``` ## Further reading - [Settlement](/operations/settlement) — refunds vs settlements. - [Disputes](/operations/disputes) — what happens when a cardholder disputes. ======================== / api/webhooks ======================== --- type: API Reference project: KRW-J2PB8 status: contract audience: developer description: Webhook endpoints, the event catalog, HMAC verification, retries and replay. openapi_refs: - /webhook_endpoints - /webhook_endpoints/{webhook_endpoint} - /webhook_endpoints/{webhook_endpoint}/deliveries - /webhook_endpoints/{webhook_endpoint}/deliveries/{delivery}/replay --- # Webhooks Webhooks are how digid pay tells your server what happened. Register an HTTPS endpoint, subscribe to events, verify signatures, and reconcile — never fulfil an order on a client callback alone. ## Register an endpoint ```bash curl https://api.digid.cc/v1/webhook_endpoints \ -u sk_live_...: \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.example.com/digidpay/webhooks", "events": ["payment_intent.succeeded", "payment_intent.payment_failed"] }' ``` The response includes your signing `secret` — shown once. Keep it server-side. ## Event catalog | Event | Meaning | |---|---| | `payment_intent.processing` | Confirmed, authorising. | | `payment_intent.succeeded` | Authorised and captured. Fulfil the order. | | `payment_intent.payment_failed` | Declined / failed. | | `payment_intent.cancelled` | Cancelled before completion. | | `payment_intent.requires_action` | SCA challenge surfaced (informational). | | `payment_intent.requires_approval` | Agent intent awaiting human approval. | | `refund.created` | Refund request recorded. | | `refund.succeeded` | Refund completed. | | `refund.failed` | Refund rejected. | ## Verify signatures (HMAC) Every delivery carries a `Digidpay-Signature` header: ``` Digidpay-Signature: t=1725794400,v1=9a8f...b2 ``` Verification: build the signed payload as `{timestamp}.{body}` where `body` is the raw request body, compute HMAC-SHA256 with your endpoint secret, and compare `v1`. ```python import hashlib, hmac, json, time SECRET = "whsec_..." # your endpoint secret def verify(raw_body: bytes, header: str) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) ts, sig = parts["t"], parts["v1"] if abs(int(ts) - time.time()) > 300: # replay window return False expected = hmac.new(SECRET.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, sig) ``` ```javascript import { createHmac, timingSafeEqual } from 'node:crypto' const SECRET = 'whsec_...' export function verify(rawBody, header) { const parts = Object.fromEntries(header.split(',').map(p => p.split('='))) const { t: ts, v1: sig } = parts if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false const expected = createHmac('sha256', SECRET).update(`${ts}.`).update(rawBody).digest('hex') const a = Buffer.from(expected), b = Buffer.from(sig) return a.length === b.length && timingSafeEqual(a, b) } ``` ```bash # Verify with openssl (header values filled from the delivery) payload="$DIGIDPAY_BODY" # raw request body ts="1725794400" sig="9a8f..." # v1 value from Digidpay-Signature computed=$(printf '%s.%s' "$ts" "$payload" | openssl dgst -sha256 -hmac "whsec_..." | awk '{print $2}') [ "$computed" = "$sig" ] && echo VALID || echo INVALID ``` ## Idempotent processing Deliveries are **at-least-once** and may retry. Processing must be idempotent — key on the event `id` and ignore duplicates: ```python seen = set() def handle(event_id): if event_id in seen: return seen.add(event_id) # fulfil / update order ``` ## Retries & backoff - Deliveries retry with **exponential backoff** until acknowledged (`2xx`) or the retry budget is exhausted. - Always return `2xx` promptly once you have accepted the event. ## Manage endpoints ```bash curl https://api.digid.cc/v1/webhook_endpoints -u sk_live_...: # list curl https://api.digid.cc/v1/webhook_endpoints/we_1Ef... -u sk_live_...: # retrieve curl -X POST https://api.digid.cc/v1/webhook_endpoints/we_1Ef... \ -u sk_live_...: -H "Content-Type: application/json" -d '{"enabled": false}' # update curl -X DELETE https://api.digid.cc/v1/webhook_endpoints/we_1Ef... -u sk_live_...: # delete ``` ## Delivery log & replay Every delivery is logged and retained 12 months. Inspect failures and replay from the dashboard or the API: ```bash curl "https://api.digid.cc/v1/webhook_endpoints/we_1Ef.../deliveries" -u sk_live_...: curl -X POST "https://api.digid.cc/v1/webhook_endpoints/we_1Ef.../deliveries/dlv_.../replay" \ -u sk_live_...: ``` ## Signature key rotation Rotate an endpoint's signing secret by updating the endpoint, capturing the new secret once, and updating your verifier. Keep the old secret verifying during a short overlap if deliveries may still be in flight. ======================== / api/errors ======================== --- type: API Reference project: KRW-J2PB8 status: contract audience: developer description: The error envelope, error types, and the digid pay code catalogue. --- # Errors All errors use a stable envelope: ```json { "error": { "type": "card_error", "code": "card_declined", "message": "Your card was declined.", "param": null } } ``` | Field | Meaning | |---|---| | `type` | High-level class of error. | | `code` | Machine-readable digid pay code (stable — build on these, not messages). | | `message` | Human-readable, safe to show customers for `card_error`. | | `param` | The request field the error relates to, if any. | ## Types | Type | HTTP | Meaning | |---|---|---| | `invalid_request_error` | 400 | Malformed request, bad parameter, or contract violation (incl. raw card fields). | | `card_error` | 402 | The card/acquirer declined; safe to show to the customer. | | `authentication_error` | 401 | Missing/invalid key, or a secret key used from a browser context. | | `idempotency_error` | 400 | Idempotency-Key reused with a different payload. | | `rate_limit_error` | 429 | Too many requests. | | `approval_error` | 400 | Approval-gate violation (agent intent not approved). | | `api_error` | 500 | digid pay internal error — retry with backoff. | ## digid pay codes The canonical, buildable codes (minimum set, FR-8/9/10 + approvals): | Code | When | |---|---| | `card_declined` | The issuer declined the card. | | `insufficient_funds` | Decline — insufficient funds. | | `expired_card` | Decline — card expired. | | `invalid_card` | Decline — card invalid. | | `authentication_required` | SCA needed; intent is `requires_action`. | | `expired_token` | The payment token has expired. | | `invalid_token` | The payment token is not valid. | | `used_token` | The payment token was already used. | | `raw_card_rejected` | A raw card-number field was sent (token-only contract). | | `approval_required` | An agent intent needs human approval before it can process. | | `idempotency_key_reused` | Idempotency-Key sent again with a different payload. | | `secret_key_from_browser` | Secret key used in a browser context. | | `resource_missing` | Resource not found (cross-tenant returns `404`). | | `rate_limit_exceeded` | Per-key rate limit exceeded. | | `invalid_parameter` | A parameter failed validation (`param` is set). | | `insufficient_permissions` | Key scope does not allow this action. | | `api_error_internal` | Internal failure — retry with backoff. | ## Internal-reference mapping digid pay normalises internal engine codes into the public catalogue above so merchants never depend on internal values. Where an internal code has a public equivalent, the mapping is applied at the facade and only the public code is ever returned. If you ever see an unmapped internal code, treat it as an `api_error_internal` and report it — it is never a contract you should build on. ## Handling by status code ```text 401 → check keys / mode; never retry unauthenticated 402 → card_error: show the message, allow retry with a fresh token 429 → retry with Retry-After 400 → fix the request; read `code` + `param` 404 → resource missing or not yours 500 → retry with exponential backoff ``` ## Idempotent retries For any transient failure (`api_error`, `rate_limit_error`, network), retry the **same request with the same `Idempotency-Key`**. That guarantees no double charge. ======================== / api/ratelimits ======================== --- type: API Reference project: KRW-J2PB8 status: contract audience: developer description: Rate limit policy at the edge and pagination conventions. --- # Rate limits & pagination digid pay enforces rate limits at the edge per key. Limits are contract values and may tighten as the service hardens; the dashboard shows your current quotas. ## HTTP status When you exceed a limit you receive `429 rate_limit_exceeded`: ```json { "error": { "type": "rate_limit_error", "code": "rate_limit_exceeded", "message": "Rate limit exceeded.", "param": null } } ``` Responses include `Retry-After` (seconds). Back off and retry; do not retry in a tight loop. ## Limits (contract values) | Dimension | Contract value | |---|---| | Payments (create/confirm/cancel) | per-key, per-minute (documented in dashboard) | | Reads (list/retrieve) | per-key, per-minute (documented in dashboard) | | Webhook deliveries | firehose to you; your endpoint should be able to absorb bursts | | Sandbox open keys | lower shared ceiling — see [Sandbox](/sandbox) | If you need higher limits for a burst (promotions, migrations), contact support ahead of time. ## Backoff Standard retry posture: ``` 429 / 5xx → retry with exponential backoff (e.g. 1s, 2s, 4s, … up to ~30s) always reusing the same Idempotency-Key on mutating calls ``` ## Pagination List endpoints are cursor-based: ``` ?limit=25&starting_after=pi_... ``` | Param | Meaning | |---|---| | `limit` | Max results per page (1–100, default 25). | | `starting_after` | Opaque cursor from the previous page's last item. | Response shape: ```json { "object": "list", "data": [ /* items */ ], "has_more": true } ``` Walk pages until `has_more` is `false`. Cursors are stable for the duration of a walk; re-list for a fresh view. ======================== / agents/mcp ======================== --- type: Guide project: KRW-J2PB8 status: contract audience: agent description: "digid pay MCP server: endpoint, tool catalog, approval gate, client configs." openapi_refs: - /payment_intents - /payment_intents/{payment_intent} - /payment_intents/{payment_intent}/confirm - /payment_intents/{payment_intent}/approve - /payment_intents/{payment_intent}/cancel - /refunds - /refunds/{refund} - /webhook_endpoints - /keys --- # MCP server digid pay exposes a **Model Context Protocol (MCP)** server so AI agents can manage payments on a merchant's behalf — always through a **human approval gate** for money movement. An agent can create an intent, but a charge never reaches the acquirer without an explicit human approval. > **Status.** The MCP surface is `contract` (routes inside the facade, > day-one per FR-14 / AD-12). The examples below are the integration contract. ## Endpoint ``` https://api.digid.cc/mcp ``` Authenticate as the merchant whose keys the agent holds. Agent surfaces use the merchant's scoped secret key (or a scoped key labelled for agent use); per-merchant agent limits (max amount, daily cap) are enforced **before** an approval is even requested. ## Tool catalog | Tool | Action | Approval gate | |---|---|---| | `payment_intent.create` | Create a PaymentIntent | starts `requires_approval` for agent flows | | `payment_intent.retrieve` | Read status | none (read) | | `payment_intent.list` | List intents | none (read) | | `payment_intent.approve` | Approve an agent intent | approver-scoped | | `payment_intent.cancel` | Cancel a non-terminal intent | none | | `refund.create` | Refund a succeeded intent | none (reflects your normal controls) | | `refund.retrieve` | Read refund | none | | `webhook_endpoint.list` | List endpoints | none | | `key.list` | List key metadata | none | `payment_intent.create` from an agent returns an intent in `requires_approval`: ```json { "id": "pi_1Ab...", "status": "requires_approval", "livemode": true, "approval": { "required": true, "state": "pending" } } ``` The intent does **not** proceed until a designated human approver calls `payment_intent.approve` (dashboard or API). A decline = cancel. Every approval is recorded (approver identity + timestamp). ## Client configuration ### Claude Desktop / any MCP client ```json { "mcpServers": { "digid-pay": { "type": "http", "url": "https://api.digid.cc/mcp", "headers": { "Authorization": "Bearer sk_live_..." } } } } ``` ### Generic MCP over SSE/HTTP Point any MCP client at `https://api.digid.cc/mcp` with the Bearer header above. The server advertises its tools via the standard `tools/list` handshake; no custom wiring is required. ### Python (MCP SDK) ```python from mcp import ClientSession, StdioServerParameters # stdio not used here import httpx, json # HTTP transport against the digid pay MCP endpoint: # url: https://api.digid.cc/mcp # headers: { "Authorization": "Bearer sk_live_..." } ``` For a ready-to-run client, use the official MCP client of your runtime and point it at the endpoint with the header. digid pay publishes typed client examples alongside the contract as codegen lands (planned). ### TypeScript ```ts // Generic MCP client (any SDK), endpoint https://api.digid.cc/mcp const transport = new StreamableHTTPClientTransport( new URL('https://api.digid.cc/mcp'), { headers: { Authorization: `Bearer ${process.env.DIGIDPAY_SECRET_KEY}` } }, ) const client = new Client({ name: 'my-agent', version: '1.0.0' }) await client.connect(transport) const tools = await client.listTools() ``` ## Approval semantics (agents) 1. Agent calls `payment_intent.create` → intent is `requires_approval`. 2. Merchant's designated approver is notified (dashboard). 3. Approver approves (or declines/cancels). 4. Only then does the charge proceed to the acquirer (SCA applies per the acquirer, unchanged). 5. The full lifecycle is audit-trailed and distinguishable in reports as agent-initiated. ## Opt-in per merchant Agent-initiated payments are **opt-in per merchant**. Until a merchant enables the agent surface and configures approvers and limits, agent tools return `insufficient_permissions`. ## Further reading - [A2A AgentCard](/agents/a2a) — agent-to-agent discovery and task flow. - [Payments](/api/payments) — the REST equivalents. ======================== / agents/a2a ======================== --- type: Guide project: KRW-J2PB8 status: contract audience: agent description: digid pay A2A AgentCard at api.digid.cc/.well-known/agent.json, task flow and human approval. openapi_refs: - /payment_intents - /payment_intents/{payment_intent}/approve --- # A2A AgentCard digid pay publishes an **Agent-to-Agent (A2A)** card so other agents can discover digid pay as a payment capability and start tasks against it. As with the MCP server, **money movement always ends in a human-approved step**. > **Status.** `contract` — part of the facade's day-one agent surface > (FR-14 / AD-12). ## AgentCard ``` https://api.digid.cc/.well-known/agent.json ``` ```bash curl https://api.digid.cc/.well-known/agent.json ``` ```json { "name": "digid pay", "description": "Payment gateway agent: create and manage card PaymentIntents for a merchant. Money movement always requires human approval.", "url": "https://api.digid.cc/.well-known/agent.json", "version": "0.1.0-contract", "skills": [ { "id": "payments", "name": "digid pay payments", "description": "Create and manage PaymentIntents under a merchant's digid pay keys.", "inputModes": ["text/plain"], "outputModes": ["text/plain"] } ], "capabilities": { "requiresApproval": true } } ``` ## Task flow A2A tasks follow the standard lifecycle (`input → working → completed` / `input-required → failed`). digid pay maps its payment steps onto it: ```mermaid sequenceDiagram participant Agent participant digidpay as digid pay A2A participant Approver Agent->>digidpay: task (create PaymentIntent) digidpay-->>Agent: requires_approval (working, awaiting human) digidpay->>Approver: notify designated approver Approver->>digidpay: approve digidpay-->>Agent: completed (succeeded) / failed ``` Every agent task is opt-in per merchant and bound to merchant-scoped keys and limits. ## Client configuration (copy-paste) ### Discovery ```bash curl https://api.digid.cc/.well-known/agent.json ``` ### Start a task An A2A client sends JSON-RPC to the card's configured task URL. Example using the same payment shape as the REST API: ```json { "jsonrpc": "2.0", "id": "1", "method": "tasks/send", "params": { "taskId": "task-1", "message": { "role": "user", "parts": [ { "text": "Create a payment of 2490 eur with merchant_reference order-1001. Return the intent id." } ] } } } ``` ### Python client sketch ```python import httpx CARD_URL = "https://api.digid.cc/a2a" # agent task endpoint HEADERS = {"Authorization": "Bearer sk_live_..."} def send_task(text: str) -> dict: r = httpx.post(CARD_URL, json={ "jsonrpc": "2.0", "id": "1", "method": "tasks/send", "params": {"taskId": "task-1", "message": {"role": "user", "parts": [{"text": text}]}}, }, headers=HEADERS, timeout=30) r.raise_for_status() return r.json() ``` ### TypeScript client sketch ```ts const res = await fetch('https://api.digid.cc/a2a', { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${process.env.DIGIDPAY_SECRET_KEY}` }, body: JSON.stringify({ jsonrpc: '2.0', id: '1', method: 'tasks/send', params: { taskId: 'task-1', message: { role: 'user', parts: [{ text: 'Create a payment of 2490 eur for order-1001' }] } }, }), }) const result = await res.json() ``` ## Human-approval requirement - An agent-created PaymentIntent is held in `requires_approval`. - It proceeds only after a designated human approves — via the dashboard or the `payment_intent.approve` API call. - No autonomous money movement exists. If a task asks digid pay to charge without an approval step, it fails with `approval_error`. ## Further reading - [MCP server](/agents/mcp) — the parallel agent surface with its tool catalog. - [Accounts](/concepts/accounts) — opt-in and approver configuration. ======================== / operations/go-live ======================== --- type: Runbook project: KRW-J2PB8 status: planned audience: both description: The activation → live checklist for a digid pay merchant. --- # Go live > **Status.** Activation is operator-gated and currently `planned` (the > lifecycle is being built). This is the runbook you follow when your account > moves to live. Going live is a deliberate sequence — nothing in digid pay flips you to live without an operator-approved activation. ## Pre-flight (still in sandbox) - [ ] Integration green in sandbox: create → confirm → webhook → fulfil. - [ ] SCA card tested end to end (`requires_action` handled, no page-leave). - [ ] Decline cards handled gracefully (retry with a fresh token). - [ ] Webhook endpoint registered and **signatures verified**. - [ ] Idempotency keys in place on every mutating call. - [ ] Secret key never in client code (browser-UA rejection would catch it). ## Activation 1. From the dashboard, submit for activation (confirm business details and plan). 2. digid pay operator reviews and approves (FR-2). Every approval is recorded. 3. Your account state becomes `live`; you are notified by email. ## After activation - [ ] **Mint live keys** (dashboard → keys → create, mode `live`). Store the secret server-side; it is shown once. - [ ] Update your backend to use `sk_live_…` and the live publishable key. - [ ] **Register/point webhooks at your production endpoint** and confirm a test delivery arrives. - [ ] **First live payment**: a small real charge; confirm the webhook, the dashboard transaction, and settlement visibility. - [ ] **Monitoring on**: watch for webhook delivery failures and 429s; set alerting on `payment_intent.payment_failed` spikes and webhook backlog (see [Security](/operations/security) for the ops checklist). ## Never in production - A sandbox key in live code (it only ever hits test rails). - A raw card-number field anywhere (the API rejects it — token-only). - Fulfilling an order on a client callback. Fulfil on the webhook. - A secret key shipped to the browser. ## Rollback To pause live traffic: revoke the live secret key (instant) or request suspension from the dashboard. Revoking keys stops new authorisations immediately; suspension also disables the connector and halts webhooks. ## Related - [Accounts](/concepts/accounts) — states and modes. - [Settlement](/operations/settlement) — how funds reach you. - [Security](/operations/security) — the operator checklist. ======================== / operations/sca-3ds ======================== --- type: Runbook project: KRW-J2PB8 status: contract audience: developer description: When SCA / 3-D Secure challenges occur, requires_action handling, liability shift, no-leave-page model. --- # SCA & 3-D Secure Strong Customer Authentication (SCA) is required for many European card payments. digid pay surfaces it cleanly: when a card needs authentication, the PaymentIntent moves to `requires_action` and a 3-D Secure challenge runs **inside the checkout flow** — no page navigation, no new tab. > **Status.** The handling contract below is `contract` (built with the > facade). The acquirer performs SCA per scheme rules; digid pay never weakens > or bypasses it. ## When challenges occur A challenge is triggered by the card scheme/issuer — typically: - new merchants or high-risk categories (fraud rules), - high-value or unusual transactions, - issuer policy for certain cards/banks. You cannot predict it from the card number; you handle it whenever the API says `requires_action`. ## The no-leave-page model With the [checkout snippet](/checkout/snippet) or [Secure Fields](/checkout/secure-fields), the challenge renders in an overlay **inside the fields' frame**. Your customer authenticates with their bank (3-D Secure) without leaving your page. The snippet resumes automatically. ## Handling `requires_action` (API path) 1. Confirm the PaymentIntent. 2. Response status is `requires_action` with a `next_action`. 3. Present the challenge via Secure Fields / the snippet using the same `client_secret`. 4. On completion, retrieve the intent or await the webhook — the state is `succeeded` or `failed`. ```bash curl https://api.digid.cc/v1/payment_intents/pi_1Ab... -u sk_live_...: ``` ```json { "id": "pi_1Ab...", "status": "requires_action", "next_action": { "type": "challenge", "redirect_to_url": null } } ``` With the snippet, you don't handle the challenge yourself — the snippet does: ```js case 'payment_intent.requires_action': // snippet is presenting the 3-D Secure challenge in-frame break ``` ## Liability shift When authentication succeeds, the liability for fraudulent chargebacks shifts to the card issuer. That is why SCA is not just compliance — it protects you. digid pay does not decide the shift; the acquirer applies scheme rules and the result is visible on the transaction record. ## Test in sandbox Use the SCA test card in [Sandbox](/sandbox) to exercise `requires_action` end to end: - confirm → `requires_action` → complete the challenge → `succeeded`. ## Related - [Payments](/api/payments) — statuses and confirm. - [Secure Fields](/checkout/secure-fields) — styling the challenge surface. - [Disputes](/operations/disputes) — liability and chargebacks. ======================== / operations/disputes ======================== --- type: Runbook project: KRW-J2PB8 status: planned audience: both description: How card disputes flow from the acquirer to the merchant, and what digid pay does. --- # Disputes A **dispute** (chargeback) is a cardholder's challenge to a charge, raised with their bank. Disputes are answered by **our European acquiring partner**, not by digid pay — digid pay routes, records, and surfaces them to you. > **Status.** Dispute routing and the merchant views are `planned`. The model > below is how disputes flow; exact SLAs and the acquirer's evidence > requirements are published here when live. ## What happens ```mermaid sequenceDiagram participant Shopper participant Bank as Cardholder's bank participant Acquirer as Acquiring partner participant digidpay as digid pay participant Merchant Shopper->>Bank: files dispute Bank->>Acquirer: chargeback Acquirer->>digidpay: dispute notification digidpay->>Merchant: dispute event + dashboard view Merchant-->>digidpay: submits evidence (via acquirer portal) Acquirer-->>Bank: responds Acquirer->>digidpay: outcome digidpay->>Merchant: outcome surfaced ``` ## What the merchant receives - A dispute notification with the reason code from the scheme, - the transaction and amount involved, - the window to respond and where to submit evidence (the acquirer's portal or process), and - the outcome once decided. ## Your responsibilities 1. **Respond in the window.** Missed windows are lost by default. 2. **Submit good evidence** — order records, delivery confirmation, customer correspondence, and any SCA result. SCA success is strong evidence of customer authentication. 3. **Track separately from refunds.** A dispute is not a refund; a refund you issue pre-emptively can avoid a dispute in some reason-code cases — decide per situation. ## Routing | Step | Handled by | |---|---| | Receive dispute from scheme | our European acquiring partner | | Answer the dispute | our European acquiring partner (with your evidence) | | Notify + surface to merchant | digid pay (route + record) | | Fund movement on loss/win | direct acquirer ↔ merchant | digid pay never holds the funds in dispute; movement stays acquirer ↔ merchant per [Settlement](/operations/settlement). ## Related - [Settlement](/operations/settlement) — how funds move. - [SCA & 3-D Secure](/operations/sca-3ds) — SCA evidence and liability shift. ======================== / operations/settlement ======================== --- type: Concept project: KRW-J2PB8 status: planned audience: both description: How settlement works — direct from the acquirer to the merchant, digid pay never holds funds. --- # Settlement Settlement is the movement of money from the card networks to you. In digid pay's model, funds flow **directly from our European acquiring partner to your bank account** — digid pay never holds or routes merchant funds. > **Status.** The direct-settlement model is architectural fact (AD-4). > Settlement **views** in the dashboard are `planned` (FR-19). ## How money moves ```mermaid sequenceDiagram participant Shopper participant Acquirer as Acquiring partner participant digidpay as digid pay participant Bank as Merchant's bank account Shopper->>Acquirer: card payment (via digid pay) Acquirer-->>digidpay: status + margin record (no funds) Acquirer->>Bank: settles directly (T+n) digidpay-->>digidpay: records settlement state + margin ``` - **You settle with the acquirer directly** — digid pay is not in the money path. - **digid pay's margin** is captured via the acquirer's reseller mechanism per your [plan](/concepts/plans). It never touches your settlement. - **digid pay never holds or routes merchant funds.** That is a boundary, not a detail — it is why digid pay is not a payments institution holding your money. ## What you see Each successful PaymentIntent shows a settlement state derived from the acquirer-side record: | State | Meaning | |---|---| | `pending` | Authorised + captured; scheduled for the next payout cycle. | | `paid` | Settled to your account. | Periodic settlement summaries reconcile to the same transaction set, and all data is tenant-scoped — merchant A never sees merchant B's settlement. ## Timing (T+n) Payout timing is set by the acquirer (typically a few business days after capture). The exact schedule for your account is visible in the dashboard once settlement views ship. digid pay does not control or accelerate it. ## Refunds vs settlement - A **refund** reverses a captured amount; it nets against a future payout. - A **dispute** is a separate chargeback flow ([Disputes](/operations/disputes)). - Track both separately from your own payout reconciliation. ## Related - [Plans & pricing](/concepts/plans) — the margin model. - [Disputes](/operations/disputes) — chargebacks and evidence. - [Go live](/operations/go-live) — what to check before going live. ======================== / operations/security ======================== --- type: Runbook project: KRW-J2PB8 status: contract audience: developer description: Key handling, rotation cadence, misuse alerts, and the PCI do/don't list. --- # Security Security for a digid pay integration is mostly **key hygiene and knowing what not to touch**. Card data never enters your systems, so the attack surface you own is your keys, your webhook endpoint, and your order logic. ## Key handling - Store the **secret key** in a secret manager; never in code, config committed to git, or client bundles. - Use the **publishable key** in the browser only. - Label keys by purpose and **scope them** where possible (see [Authentication](/api/authentication)). - **Rotation cadence:** rotate on personnel change, on suspected exposure, and periodically (a reasonable default is every 90 days for high-volume servers). Rotation is instant; the old secret stops authenticating. - **Revoke immediately** any key you suspect is exposed. Revocation is effective within seconds. digid pay alerts you on misuse signals — treat an alert as an active exposure: rotate, then investigate. ## Webhook endpoint hygiene - Verify the `Digidpay-Signature` HMAC on every delivery ([Webhooks](/api/webhooks)). - Keep a 300s timestamp replay window. - Process idempotently (events are at-least-once). - Return `2xx` fast; your endpoint should be able to absorb bursts. ## Misuse alerts digid pay detects obvious signals (secret key used from a browser context, unusual volumes, unexpected geographies) and surfaces them in the dashboard with a one-click rotate action. Wire your own alerting on top for your side (e.g. watch your webhook endpoint and payment-failure rate). ## PCI do / don't list **Do** - Capture cards only through digid pay Secure Fields. - Use tokens (`pmt_…`) in confirmations. - Fulfil on verified webhooks. - Keep keys server-side, scoped, rotated. - Let the acquirer handle SCA; never bypass it. **Don't** - Don't build your own card form or collect card numbers yourself. - Don't send, store, or log raw card-number fields anywhere. The API rejects them — and you should too, at the source. - Don't ship a secret key to the browser (it is rejected, and flagged). - Don't fulfil on a client callback alone. - Don't disable or shortcut 3-D Secure to "improve conversion". ## The PCI do/don't in one line Card entry happens inside digid pay's vault boundary — keep it that way and your PCI burden stays at the lowest SAQ class ([PCI posture](/operations/pci-posture)). ## Related - [Authentication](/api/authentication) — keys and revoke/rotate endpoints. - [Webhooks](/api/webhooks) — signature verification. - [PCI posture](/operations/pci-posture) — the compliance story. ======================== / operations/pci-posture ======================== --- type: Concept project: KRW-J2PB8 status: live audience: both description: Merchant-facing PCI posture — SAQ A path and digid pay service-provider posture. --- # PCI posture This page is the merchant-facing summary of **why your integration stays out of PCI scope**, and of digid pay's own posture as a service provider. Keep it handy for your own assessor or auditor. ## The merchant story: SAQ A Because card entry happens entirely inside **digid pay Secure Fields** (the hosted tokenization iframes served from digid pay's vault boundary) — never in your DOM, your servers, or your logs — your payment page is not in scope for card data. digid pay is the entity whose systems handle the tokenization boundary. The wording you can rely on: > Card entry happens entirely inside digid pay's vault boundary, never in our > DOM or servers, so our payment page qualifies for SAQ A under the > card-brand self-assessment questionnaires. ### What makes SAQ A hold for you - The card-number field you present is a digid pay Secure Field iframe — the PAN is typed into digid pay's vault, not your page. - Your servers never receive, store, process, or transmit a PAN. - Your pages contain no script that reads card data from the fields (the fields are isolated iframes; your code cannot reach them). - Your snippet is SRI-pinned and versioned, so the code you load is the code you audited ([Snippet](/checkout/snippet)). ## digid pay's service-provider posture digid pay operates as a **payment service provider** and completes an annual **SAQ D-SP** self-assessment (Level 2 service provider). The architecture keeps digid pay's own card-data environment empty by construction: | Layer | Card data exposure | |---|---| | digid pay Secure Fields + tokenization vault | the only place card data is captured and stored — inside the vault boundary | | digid pay API, webhooks, dashboard, logs | token-only; no PAN by construction | | Your systems | none — you integrate with tokens and webhooks | digid pay holds no PAN in its systems, logs, exports, or repositories. Log scrubbers enforce the PAN-pattern ban across all environments, including the open sandbox (see [Sandbox rules](/sandbox)). ## Evidence on request digid pay makes available to merchants (on request, per the service-provider obligations) a summary of its compliance posture and evidence — including attestations from the certified parties in the chain (the tokenization vault and the acquiring partner). For a full evidence pack, contact with your merchant account reference. ## What digid pay is not - Not a certified acquirer and not a payment institution holding your funds ([Settlement](/operations/settlement)). - Not a party that ever handles card data on your behalf outside the vault boundary. - The acquiring and SCA obligations sit with our **European acquiring partner**; digid pay's role is orchestration, tokens, and brand. ## Related - [Tokens & card data](/concepts/tokens) — the model behind this posture. - [Secure Fields](/checkout/secure-fields) — the SAQ A wording in context. - [Accounts](/concepts/accounts) — merchant lifecycle. ======================== / partners/payment-flow ======================== --- type: Concept project: KRW-J2PB8 status: live audience: both description: Role and certification diagram of the digid pay payment flow (roles only, no vendor names). --- # Payment flow & roles Who does what in a digid pay payment, by **role and certification only** — no vendor names. Card data exists in exactly two places at any moment: inside the digid pay tokenization vault's hosted fields (browser → vault TLS) and in the single leg from the vault to the acquiring partner. Everything else — your systems, digid pay's gateway, its logs and database — never holds or transports a PAN. ```mermaid flowchart TD classDef pci fill:#4a1f1f,stroke:#e57373,color:#ffebee classDef merchant fill:#14301f,stroke:#66bb6a,color:#e8f5e9 classDef digidpay fill:#0d2a4a,stroke:#42a5f5,color:#e3f2fd classDef neutral fill:#263238,stroke:#90a4ae,color:#eceff1 S0(["Shopper at checkout"]):::neutral S1["MERCHANT WEBSHOP
order logic · renders the checkout
lowest SAQ class"]:::merchant S2["CHECKOUT SNIPPET (on merchant page)
embeds hosted fields · ships versioned
from the digid pay asset host, runs in merchant DOM"]:::merchant S3["DIGID PAY TOKENIZATION VAULT — capture
PCI DSS certified · hosted fields
tokenizes the card at input"]:::pci S4["DIGID PAY GATEWAY
orchestration · plans · keys · webhooks
no PAN by construction"]:::digidpay S5["ACQUIRING PARTNER
regulated acquirer
KYC · SCA/3-D Secure · clearing"]:::pci S6["CARD SCHEMES + ISSUER
authorization"]:::pci S7["digid pay status + ledger update
event-driven · no PAN"]:::digidpay S8["MERCHANT WEBHOOK (HMAC-signed)
order finalized"]:::merchant S9["SETTLEMENT
acquirer → merchant bank, T+n
margin split to digid pay
funds never touch the gateway"]:::neutral S0 -->|"opens checkout"| S1 S1 -->|"embeds"| S2 S2 -->|"renders hosted fields"| S3 S0 -.->|"PAN typed DIRECTLY into vault fields —
never merchant or gateway DOM"| S3 S3 -->|"card token returned to page"| S2 S2 -->|"token + order → gateway API"| S4 S4 -->|"tokenized charge payload"| S5 S5 -->|"auth request"| S6 S6 -->|"approve / decline"| S5 S5 -->|"result (no PAN)"| S7 S7 -->|"notifies"| S8 S8 -->|"confirmation"| S0 S5 -.->|"settles"| S9 S9 -.->|"gateway margin only"| S4 ``` **The invariant:** raw card data exists in exactly two places at any moment — inside the vault's hosted fields (browser → vault TLS) and in the single leg *from* the vault *to* the acquiring partner. The gateway, its logs, its database, and the merchant's site never hold or transport a PAN. That is what keeps merchant PCI burden minimal and the platform's compliance self-assessable. ## Role → responsibility table | Role | Certification | Responsible for | Card data exposure | |---|---|---|---| | Merchant webshop | lowest SAQ class | orders, UX, integration | none | | digid pay gateway | service provider, annual self-assessment (SAQ D-SP); empty card-data environment | orchestration, plans, keys, webhooks, brand, support | none — tokens only | | digid pay tokenization vault | **PCI DSS certified service provider** | card capture (hosted fields), secure storage, token↔card translation | yes, inside its certified boundary | | Acquiring partner | **PCI DSS certified · licensed** | merchant KYC/underwriting, SCA/3-D Secure, scheme clearing, settlement | yes, end to end as the regulated party | | Card schemes + issuer | scheme rules | authorization decisions | yes, within schemes | ## What a services partner can operate without touching anything sensitive Merchant recruitment, onboarding hand-holding (KYC completes directly with the acquiring partner, not with digid pay), integration support against these docs and the snippet, first-live-payment accompaniment, and dispute *routing* (the acquiring partner answers them). No card data, no funds, no underwriting — clean to contract. ## Related - [Tokens & card data](/concepts/tokens) — the model behind this diagram. - [PCI posture](/operations/pci-posture) — the merchant-facing summary. - [Settlement](/operations/settlement) — the T+n flow.