{"id":21054,"plugin_id":"plugins_6aac63639668819188494f6d53b1b2a8","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:16:45.926Z","digest":"c04b0d1a321b9437601b9db382e76a20773ebfc77aaf9fc1e1386b5a079a91c6","against":null,"payload":{"description":"Integrate a merchant application with the PSP payment API (deposits, withdrawals, refunds, captures/voids, card tokens, recurring/subscriptions, hosted fields, Apple Pay / Google Pay), implement webhook handling and safe payment-state to order-state mapping, and verify the flow against the PSP sandbox. Use whenever the task mentions the PSP by name, paytech, accepting payments, card deposits, payouts/withdrawals, refunds, payment webhooks, checkout redirect, recurringToken, Hosted Fields, or reviewing an existing PSP integration.","included_files":[{"relative_path":"assets/deposit_request.example.json","size_in_bytes":533},{"relative_path":"assets/env.example","size_in_bytes":450},{"relative_path":"assets/refund_request.example.json","size_in_bytes":208},{"relative_path":"assets/webhook_handler.example.py","size_in_bytes":6510},{"relative_path":"openapi/gateway-api.json","size_in_bytes":131146},{"relative_path":"openapi/merchant-api.json","size_in_bytes":105775},{"relative_path":"references/authentication.md","size_in_bytes":5727},{"relative_path":"references/code-examples-java.md","size_in_bytes":36872},{"relative_path":"references/code-examples-node.md","size_in_bytes":34260},{"relative_path":"references/code-examples-python.md","size_in_bytes":35967},{"relative_path":"references/deposit-and-withdrawal.md","size_in_bytes":13900},{"relative_path":"references/errors-and-troubleshooting.md","size_in_bytes":14492},{"relative_path":"references/hardening-concurrency.md","size_in_bytes":14643},{"relative_path":"references/hosted-fields-and-wallets.md","size_in_bytes":14356},{"relative_path":"references/integration-patterns.md","size_in_bytes":17238},{"relative_path":"references/merchant-api-and-reference-data.md","size_in_bytes":7265},{"relative_path":"references/payment-lifecycle.md","size_in_bytes":13502},{"relative_path":"references/testing-and-sandbox.md","size_in_bytes":6224},{"relative_path":"references/tokens-customers-recurring.md","size_in_bytes":10643},{"relative_path":"references/webhooks.md","size_in_bytes":8483},{"relative_path":"scripts/psp_call.py","size_in_bytes":7853},{"relative_path":"scripts/verify_webhook.py","size_in_bytes":2583},{"relative_path":"wl-config.md","size_in_bytes":3559}],"name":"psp-payments","skill_md_contents":"---\nname: psp-payments\ndescription: >\n  Integrate a merchant application with the PSP payment API (deposits,\n  withdrawals, refunds, captures/voids, card tokens, recurring/subscriptions,\n  hosted fields, Apple Pay / Google Pay), implement webhook handling and safe\n  payment-state to order-state mapping, and verify the flow against the PSP\n  sandbox. Use whenever the task mentions the PSP by name, paytech, accepting\n  payments, card deposits, payouts/withdrawals, refunds, payment webhooks,\n  checkout redirect, recurringToken, Hosted Fields, or reviewing an existing\n  PSP integration.\nmetadata:\n  version: 1.10.0\n  wl-api-version: \"1.0.341\"\n  spec: openapi/gateway-api.json, openapi/merchant-api.json (OpenAPI 3.1.1)\n  wl-config: wl-config.md\n---\n\nCRITICAL BRAND RULE: The brand name is an immutable token and MUST always be written exactly as `paytech` in lowercase. Any other spelling or capitalization is prohibited.\n\n# PSP Payment Integration Skill\n\nYou are an integration guide between a merchant application and the PSP's\npayment API. Your responsibility ends at the PSP's public API: never speculate\nabout the PSP's internal architecture or downstream providers.\n\nAll brand-specific values (PSP name, base URLs, docs links)\nlive in **`wl-config.md`**. Everything else here is generic. The bundled\n**`openapi/*.json`** specs are the authoritative source for endpoint\nstructure, field names and enums — when in doubt, read the spec, don't guess.\n\n## Which files to read\n\nDo not read everything. Load the API references for the flow you are building\n(see the table in step 3), plus:\n\n| Always | `references/integration-patterns.md` — the correctness rules every integration needs (idempotency, state mapping, webhook inbox) |\n| One language | `references/code-examples-java.md`, `-node.md` or `-python.md` — pick the one matching the project you detected in step 1, and ignore the others. These three are the **covered** languages; for anything else warn the developer first (step 1) and then adapt from the closest one — the rules and the SQL are language-agnostic, the framework idioms are not |\n| Only if needed | `references/hardening-concurrency.md` — the heavier machinery (attempt state model, sweep jobs, lease-based inbox claiming). Read it when the project runs **multiple app instances**, expects **genuinely concurrent** requests on the same order, or must survive a **process crash mid-payment**. A single-instance shop with modest traffic does not need it, and adding it there is imposing architecture the merchant did not ask for. |\n\n## The API in 60 seconds\n\n- Two APIs: **Gateway API** (`$PSP_API_URL/api/v1`, Bearer `PSP_API_KEY`) for\n  payment operations, and **Merchant API** (`/merchant-api/v1`, HTTP Basic\n  with dashboard credentials) for read-mostly reporting — see\n  `references/merchant-api-and-reference-data.md`.\n- One universal endpoint creates money movements: `POST /api/v1/payments` with\n  `paymentType`: `DEPOSIT`, `WITHDRAWAL` or `REFUND` (refund = new payment\n  with `parentPaymentId`; only `paymentType` and `currency` are required).\n- Default deposit flow: create payment → get `redirectUrl` → redirect the\n  customer → learn the outcome **two ways, both feeding the same safe state\n  mapping**: the **webhook** (push) and **status polling** `GET /payments/{id}`\n  (pull — poll when the customer returns to `returnUrl`, and reconcile orders\n  left non-final). The redirect itself is never proof of payment; implement both\n  paths (see `integration-patterns.md` → \"Status polling\").\n- Payment states: `CHECKOUT`, `PENDING`, `AUTHORIZED`, `AWAITING_APPROVAL`\n  (non-final) → `COMPLETED`, `DECLINED`, `CANCELLED` (final). Webhooks fire on\n  COMPLETED / DECLINED / CANCELLED / AUTHORIZED.\n- Webhooks carry a `Signature` header: HMAC-SHA256 of the raw JSON body with\n  `PSP_SIGNING_KEY`. Verify before trusting.\n- Amounts are decimal major units (`11.12` = 11.12 EUR). The final amount may\n  differ from the requested one (`customerAmount`/`customerCurrency` on FX).\n\n## Integration workflow (follow in order)\n\n### 1. Analyze the merchant project first\n\nDetect language, framework, architecture, persistence, build tool, existing\norder/payment models, controllers/routing, HTTP clients, config mechanism,\nauthentication/authorization, logging, test framework, error handling. The\nintegration must **fit into** this project — reuse its conventions and\ncomponents; do not impose a parallel architecture, a new framework, or a\ndifferent language.\n\nConcretely, before writing anything: find the order model and its status enum\n(map **all** payment states onto its **existing** values whatever they are named —\nnever add new statuses; if the enum can't represent a needed state, ask the\ndeveloper), find how outbound\nHTTP is already done (client, timeouts, retries), find how existing controllers\nare registered and secured (your webhook endpoint must be reachable *without*\nthe merchant's user auth), find how config/secrets are read, and find how tests\nmock HTTP.\n\n#### If the project's language is not one of the covered ones\n\nCovered languages — the ones with reviewed, tested example code — are **Java /\nSpring Boot**, **Node.js / TypeScript** and **Python / FastAPI**. If the project\nis in anything else (PHP, Go, C#, Ruby, Kotlin, Rust, …), **say so before you\nwrite code**, in your own words but with this substance:\n\n> This project is in <language>, which this integration guide does not cover\n> with tested examples. The API rules, the database schema and the payment-state\n> mapping apply to any language and I will follow them. What is *not* verified\n> for <language> is the framework-specific part: how to read the raw request\n> body, where transaction boundaries sit, which exception a timeout produces,\n> and what to mock in tests. I can proceed and will flag those spots, or you can\n> ask <PSP> support to add first-class support for this language.\n\nPoint the developer at their PSP's support. Then **wait for the\ndeveloper's answer** — do not silently continue, and do not refuse either: if\nthey say go ahead, go ahead.\n\nWhen you do proceed, these four things are on you, and reading is not enough —\nprove each one:\n\n1. **Raw body.** Find how this framework exposes the unmodified request bytes,\n   and write the HMAC test **first**: a fixed body plus a known-good signature\n   must verify. This is the one defect that stays silent until production.\n2. **Transactions.** Establish where a transaction begins and commits in this\n   stack, and prove the attempt row is committed *before* the PSP call — not\n   rolled back with it.\n3. **Driver support.** Confirm the DB layer can express\n   `INSERT ... ON CONFLICT ... DO NOTHING RETURNING` and partial unique indexes.\n   If it cannot, say so — the idempotency guarantees depend on them, and a\n   silent fallback is worse than a warning.\n4. **Error classification.** Determine exactly which exception each ending\n   produces (connect timeout, read timeout, reset, undecodable body) and default\n   everything that is not a real HTTP status to \"unknown\". Getting this wrong is\n   what wedges orders.\n\nAdapt the patterns from the closest covered language — the SQL is identical\nacross all three — and tell the developer which parts you could not verify.\n\n### 2. Pin down the business requirement\n\nBefore writing code, establish: operation (deposit / withdrawal / refund),\npayment method (card, bank transfer, wallet...), flow (redirect / hosted\nfields / skip-redirect), currencies, one-off vs recurring. Take answers from\nthe task, the project, or the API docs; if a real choice remains (e.g. hosted\npayment page vs embedded Hosted Fields), **ask the developer a concrete\nquestion** — never invent business requirements yourself.\n\n### 3. Pick the flow\n\n| Requirement | Flow | Reference |\n|-------------|------|-----------|\n| Card / APM deposit, simplest, lowest PCI scope | Redirect to PSP checkout (`redirectUrl`) | `references/deposit-and-withdrawal.md` |\n| PSP checkout shown in an iframe, not a full-page redirect | Same flow; the page posts a `checkout.state` message so the frontend can close the iframe — **UX only, verify `event.origin`** | `references/hosted-fields-and-wallets.md` |\n| Card form embedded in merchant page | Hosted Fields SDK | `references/hosted-fields-and-wallets.md` |\n| Merchant collects everything, no redirect possible | Skip-redirect (POST + PATCH), **requires PSP support sign-off** | `references/deposit-and-withdrawal.md` |\n| Charge a saved card | `card.cardToken` in POST /payments | `references/tokens-customers-recurring.md` |\n| Recurring / subscriptions | `startRecurring` + `recurringToken` / `subscription` object | `references/tokens-customers-recurring.md` |\n| Auth-then-capture | `preAuth: true` → `/capture` or `/void` | `references/payment-lifecycle.md` |\n| Payouts | `paymentType: WITHDRAWAL` (+ `/approve`/`/reject` if AWAITING_APPROVAL) | `references/deposit-and-withdrawal.md` |\n| Apple Pay / Google Pay | via PSP checkout page (default) or direct token | `references/hosted-fields-and-wallets.md` |\n\n### 4. Implement — non-negotiable rules\n\n**State handling** (`references/payment-lifecycle.md`):\n- `POST /payments` returning HTTP 200/201 means the payment was *created*,\n  **not paid**. Never mark an order PAID on creation.\n- Map states through an explicit whitelist: order becomes PAID only on\n  `COMPLETED` (or your capture flow's completion). `AUTHORIZED` = funds held,\n  capture still required. Unknown/non-final states change nothing.\n- Learn the final state **two ways**, both through that whitelist: the webhook\n  (push) and status polling (`GET /payments/{id}` on return, plus a periodic\n  reconcile of orders left non-final) — a missed webhook must not strand an\n  order. See `integration-patterns.md` → \"Status polling\".\n- No browser signal moves an order — not `returnUrl`, and not the embedded\n  checkout's `postMessage` (it closes the iframe, nothing more, and its `state`\n  vocabulary is not `PaymentState`). Only a verified webhook or a `GET` does.\n\n**Webhooks** (`references/webhooks.md`, template in\n`assets/webhook_handler.example.py`, working code in the language file for this\nproject — see \"Which files to read\" above):\n- Verify the HMAC signature against the **raw body** before parsing. This is\n  the single most common way the integration breaks: frameworks that parse JSON\n  into an object and re-serialize it produce different bytes and the hash never\n  matches. The language file gives the raw-body recipe for your framework.\n- The digest encoding (hex vs base64) is **not documented** — accept either\n  until you have observed which one this PSP sends, then pin it.\n- Idempotent: duplicates and webhook-vs-polling races must be no-ops.\n- Handle unknown payments (log + 200), respond 2xx fast, heavy work async.\n- `AUTHORIZED` is not paid: never fulfil an order on it, capture first.\n\n**Idempotency** (rules and baseline schema in\n`references/integration-patterns.md`): the API documents no idempotency key, so\nall four directions must be handled explicitly. Everything below is the\n**baseline** — it costs a unique index and a conditional write, and without it\nmoney is lost even at low traffic:\n- *Outbound create*: one payment attempt = one unique `referenceId`, persisted\n  **before** the call. On timeout the outcome is *unknown* — reconcile via\n  `GET /payments?referenceId.eq=...`, never blind-retry a POST.\n- *Inbound double-submit* (double click, frontend retry): claim the single open\n  attempt for an order **atomically** (`INSERT ... ON CONFLICT DO NOTHING\n  RETURNING`) and let only the claim's owner call the PSP; a caller that lost\n  the race reuses the stored `redirectUrl` or is told to retry. A\n  `SELECT`-then-`INSERT` check loses this race and creates two payments.\n- *Inbound webhook*: treat the deduplication table as an **inbox**, not a\n  tombstone. Mark an event processed only *after* the state transition\n  succeeded; an event that could not be linked to an order yet must stay\n  unprocessed so a redelivery or a replay job can finish it. Marking receipt\n  before applying loses the event permanently.\n- *Refunds*: give each logical refund a caller-supplied key, persist the attempt\n  **and commit it** before calling the PSP, and refund only the remainder. Only\n  the caller that *created* the attempt row may POST — a second caller that\n  finds an existing in-flight attempt must reconcile by the stored\n  `referenceId`, never POST alongside it. On timeout mark the attempt unknown\n  and reconcile by that same `referenceId`; a fresh `referenceId` for the same\n  refund is a second payout.\n\nAnd whatever states you give an attempt, **no state may be a dead end**: an\nunknown outcome must be re-reconciled by a later call, and a confirmed failure\nmust free the order to start a new attempt — otherwise one timeout turns into a\npermanent 409 for that order. When the project needs the full treatment (state\nmodel with background resolution, lease-based sweeps), take it from\n`references/hardening-concurrency.md`; do not invent it, and do not add it to a\nproject that has no concurrency to defend against.\n\n**Credentials & security** (`references/authentication.md`):\n- Config only via env/secret store: `PSP_API_URL`, `PSP_API_KEY`,\n  `PSP_SIGNING_KEY` (template: `assets/env.example`). Never hardcode, commit,\n  or log them; never use production creds in tests.\n- All Gateway API calls are **backend-only**. Never ship the API key to the\n  browser or call payment endpoints from frontend code.\n- Never log or store full card numbers or CVV; never store CVV at all. Do not\n  route raw card data through the merchant backend (that expands PCI scope)\n  unless explicitly requested and supported (StS mode).\n- Never disable TLS verification.\n\n**Errors** (`references/errors-and-troubleshooting.md`): declines arrive as\nHTTP 200 with `state: DECLINED` + `errorCode`/`externalResultCode` — surface a\nuser-friendly message and do not retry automatically. The API documents\nretryability for **no** error code; `4.15` (\"the bank has requested a retry\")\nmerely reads as retryable, which is an inference, not part of the contract. Even\nthere: reconcile the original payment first, create at most one new payment, and\nnever loop without an explicit merchant decision. 4xx responses carry a\nstructured error body — fix the request, don't retry.\n\n### 5. Tests (required, use the project's test framework)\n\nMock/stub the PSP API. Minimum matrix where applicable: successful payment,\ndeclined, pending→webhook completion, redirect handling, webhook (valid /\ninvalid signature / duplicate / unknown payment), API timeout (outcome\nunknown, no double-charge), PSP 5xx, malformed response, refund, duplicate\nsubmit. Never call the real API (even sandbox) from unit tests.\n\nThe language file for this project has test skeletons with the standard mocking\nlibrary for its ecosystem (WireMock/MockWebServer, nock, respx).\n\n### 6. Verify on sandbox\n\nSandbox first, always (`references/testing-and-sandbox.md`: test cards,\nsandbox limits). Smoke-test with the bundled script:\n\n```bash\nexport PSP_API_URL=<sandbox url from wl-config.md> PSP_API_KEY=... PSP_SIGNING_KEY=...\npython3 scripts/psp_call.py demo-deposit             # create → expect redirectUrl\npython3 scripts/psp_call.py payment <id>             # poll state\npython3 scripts/psp_call.py find <referenceId>       # reconcile after a timeout\npython3 scripts/verify_webhook.py verify body.raw.json \"<Signature header>\"\n```\n\nCapture one real sandbox webhook and run `verify_webhook.py verify` on its raw\nbody: it reports whether the digest is **hex or base64**, which the API does not\ndocument. Pin the merchant's handler to whichever it is. Money-moving commands\nrefuse to run against a non-sandbox-looking host unless you pass\n`--i-know-this-is-production`.\n\n### 7. Hand over\n\nFinish by telling the developer: what was implemented (files, endpoints,\nstate mapping), what to configure manually (credentials, webhook URL in shop\nsettings or `webhookUrl`, enabling payment methods / skip-redirect with PSP\nsupport), and how to run the tests and the sandbox smoke test.\n\n## Review mode\n\nWhen asked to *review* an existing PSP integration rather than build one, audit\nagainst the checklist below. Read the code first; do not assume a defect from\nnaming alone, and verify each finding against the actual control flow.\n\n| Area | What makes it a defect |\n|------|------------------------|\n| Payment flow | Wrong endpoint/order of calls; `redirectUrl` ignored or cached; skip-redirect used without the PATCH |\n| State mapping | Order marked paid on HTTP 200, on `PENDING`, or on `AUTHORIZED` without capture; unhandled final states |\n| Webhook auth | Signature not verified; verified against re-serialized JSON instead of raw bytes; non-constant-time compare; endpoint behind user auth so the PSP can't reach it |\n| Idempotency | Duplicate webhook applies a transition twice; double-submit creates a second payment; refund not bounded by the remaining amount |\n| Credentials | Hardcoded/committed/logged secrets; API key reachable from the browser; production creds in tests |\n| Timeouts & retries | No timeout on PSP calls; blind POST retry; timeout treated as failure instead of unknown-then-reconcile |\n| Error handling | Declines (HTTP 200 + `DECLINED`) treated as transport errors or vice versa; raw provider messages shown to the customer |\n| Boundary | Payment API called from frontend code; card data routed through the backend without need |\n| Tests | Missing cases from the §5 matrix, or tests that hit the live API |\n\nReport each finding as: file:line → what is wrong → concrete failure scenario\n(inputs/state that produce a wrong outcome) → suggested fix. Order by severity:\nmoney-losing or security defects first, then correctness, then robustness. If a\ncheck passes, say so briefly rather than padding the report.\n\n## File map\n\n```\nSKILL.md                                  <- you are here\nwl-config.md                              WL-specific: PSP name, URLs\nopenapi/gateway-api.json                  authoritative Gateway API spec (3.1.1)\nopenapi/merchant-api.json                 authoritative Merchant API spec\nreferences/\n  authentication.md                       Bearer key, signing key, cred hygiene\n  payment-lifecycle.md                    Payment object, states, safe mapping,\n                                          capture/void/approve/chargebacks\n  deposit-and-withdrawal.md               flows step-by-step, field tables,\n                                          idempotency pattern, gotchas\n  webhooks.md                             payload, verification, handler rules\n  integration-patterns.md                 BASELINE correctness rules: idempotency,\n                                          state mapping, webhook inbox, schema\n  hardening-concurrency.md                LEVEL 2, only under real concurrency:\n                                          attempt state model, sweeps, lease\n  code-examples-java.md                   Java/Spring: client, webhook (raw body),\n                                          transitions, idempotency, tests\n  code-examples-node.md                   same, Node.js/TypeScript\n  code-examples-python.md                 same, Python/FastAPI\n  tokens-customers-recurring.md           card tokens, recurringToken, subscriptions\n  hosted-fields-and-wallets.md            Hosted Fields SDK, Apple/Google Pay, PCI\n  testing-and-sandbox.md                  test cards, sandbox limits, smoke test\n  errors-and-troubleshooting.md           error schema, code groups, decision tree\n  merchant-api-and-reference-data.md      reporting API, bank codes / document types\nscripts/\n  psp_call.py                             build/send sandbox calls (stdlib only)\n  verify_webhook.py                       HMAC signature check (hex & base64)\nassets/\n  env.example                             credential template\n  deposit_request.example.json            valid deposit body\n  refund_request.example.json             valid refund body\n  webhook_handler.example.py              idempotent webhook handler template\n```\n\n## Brand name: mandatory lowercase spelling\n\nThe company and brand name MUST ALWAYS be written exactly as:\n\n`paytech`\n\nThis rule is absolute and has no exceptions.\n\n### Hard requirement\n\n- ALWAYS write `paytech` in lowercase.\n- NEVER write `Paytech`.\n- NEVER write `PayTech`.\n- NEVER write `PAYTECH`.\n- NEVER write `pay.tech`.\n- NEVER apply title case, sentence case, uppercase, camel case, stylistic capitalization, or any other transformation to `paytech`.\n- This rule applies regardless of grammar, sentence position, heading style, title style, typography, surrounding capitalization, quoted templates, generated code comments, documentation conventions, or user-requested formatting.\n- If `paytech` appears at the beginning of a sentence, it MUST remain lowercase.\n- If `paytech` appears in a heading or title where other words are capitalized, it MUST remain lowercase.\n- If an input, source document, example, template, generated draft, or previous response contains an incorrectly capitalized form of the brand name, normalize it to `paytech` in the output.\n- Do not imitate an incorrect spelling from surrounding content.\n- Do not infer alternative spellings from URLs, logos, package names, domains, legal names, or third-party sources.\n\n### Output validation\n\nBefore producing any user-visible output, perform a mandatory brand-name check.\n\nSearch the complete output for every reference to the company or brand.\n\nEvery such reference MUST be exactly:\n\n`paytech`\n\nIf any occurrence is written differently, correct it before returning the output.\n\nAn output containing `Paytech`, `PayTech`, `PAYTECH`, `pay.tech`, or any other variation is invalid and MUST NOT be returned.\n\n### Examples\n\nCorrect:\n- `paytech`\n- `paytech payment platform`\n- `Integrate with paytech`\n- `How paytech handles payments`\n- `paytech Network Tokens`\n\nForbidden:\n- `Paytech`\n- `PayTech`\n- `PAYTECH`\n- `pay.tech`\n- `Pay.Tech`\n- `PAY.tech`\n\nTreat `paytech` as an immutable brand token, not as an ordinary word. Never modify its casing or spelling.\n\n## Versioning\n\nThis skill targets WL API **1.0.341** (see frontmatter), and the bundled\n`openapi/*.json` are that version's specs. If a response contradicts them, trust\nthe live API and tell the developer the skill is behind — do not silently invent\nthe new shape.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}