← Plugin catalog
Finance
paytech
paytech v1.12.0
Publisher description
From the marketplace listing
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.
Language: English · Automatically detected from descriptions.
Files & skills
File archives
Plugin package31 files · 1.14 MBBrowse files →
Skill instructions
psp-payments22 KB
---
name: psp-payments
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.
metadata:
version: 1.10.0
wl-api-version: "1.0.341"
spec: openapi/gateway-api.json, openapi/merchant-api.json (OpenAPI 3.1.1)
wl-config: wl-config.md
---
CRITICAL 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.
# PSP Payment Integration Skill
You are an integration guide between a merchant application and the PSP's
payment API. Your responsibility ends at the PSP's public API: never speculate
about the PSP's internal architecture or downstream providers.
All brand-specific values (PSP name, base URLs, docs links)
live in **`wl-config.md`**. Everything else here is generic. The bundled
**`openapi/*.json`** specs are the authoritative source for endpoint
structure, field names and enums — when in doubt, read the spec, don't guess.
## Which files to read
Do not read everything. Load the API references for the flow you are building
(see the table in step 3), plus:
| Always | `references/integration-patterns.md` — the correctness rules every integration needs (idempotency, state mapping, webhook inbox) |
| 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 |
| 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. |
## The API in 60 seconds
- Two APIs: **Gateway API** (`$PSP_API_URL/api/v1`, Bearer `PSP_API_KEY`) for
payment operations, and **Merchant API** (`/merchant-api/v1`, HTTP Basic
with dashboard credentials) for read-mostly reporting — see
`references/merchant-api-and-reference-data.md`.
- One universal endpoint creates money movements: `POST /api/v1/payments` with
`paymentType`: `DEPOSIT`, `WITHDRAWAL` or `REFUND` (refund = new payment
with `parentPaymentId`; only `paymentType` and `currency` are required).
- Default deposit flow: create payment → get `redirectUrl` → redirect the
customer → learn the outcome **two ways, both feeding the same safe state
mapping**: the **webhook** (push) and **status polling** `GET /payments/{id}`
(pull — poll when the customer returns to `returnUrl`, and reconcile orders
left non-final). The redirect itself is never proof of payment; implement both
paths (see `integration-patterns.md` → "Status polling").
- Payment states: `CHECKOUT`, `PENDING`, `AUTHORIZED`, `AWAITING_APPROVAL`
(non-final) → `COMPLETED`, `DECLINED`, `CANCELLED` (final). Webhooks fire on
COMPLETED / DECLINED / CANCELLED / AUTHORIZED.
- Webhooks carry a `Signature` header: HMAC-SHA256 of the raw JSON body with
`PSP_SIGNING_KEY`. Verify before trusting.
- Amounts are decimal major units (`11.12` = 11.12 EUR). The final amount may
differ from the requested one (`customerAmount`/`customerCurrency` on FX).
## Integration workflow (follow in order)
### 1. Analyze the merchant project first
Detect language, framework, architecture, persistence, build tool, existing
order/payment models, controllers/routing, HTTP clients, config mechanism,
authentication/authorization, logging, test framework, error handling. The
integration must **fit into** this project — reuse its conventions and
components; do not impose a parallel architecture, a new framework, or a
different language.
Concretely, before writing anything: find the order model and its status enum
(map **all** payment states onto its **existing** values whatever they are named —
never add new statuses; if the enum can't represent a needed state, ask the
developer), find how outbound
HTTP is already done (client, timeouts, retries), find how existing controllers
are registered and secured (your webhook endpoint must be reachable *without*
the merchant's user auth), find how config/secrets are read, and find how tests
mock HTTP.
#### If the project's language is not one of the covered ones
Covered languages — the ones with reviewed, tested example code — are **Java /
Spring Boot**, **Node.js / TypeScript** and **Python / FastAPI**. If the project
is in anything else (PHP, Go, C#, Ruby, Kotlin, Rust, …), **say so before you
write code**, in your own words but with this substance:
> This project is in <language>, which this integration guide does not cover
> with tested examples. The API rules, the database schema and the payment-state
> mapping apply to any language and I will follow them. What is *not* verified
> for <language> is the framework-specific part: how to read the raw request
> body, where transaction boundaries sit, which exception a timeout produces,
> and what to mock in tests. I can proceed and will flag those spots, or you can
> ask <PSP> support to add first-class support for this language.
Point the developer at their PSP's support. Then **wait for the
developer's answer** — do not silently continue, and do not refuse either: if
they say go ahead, go ahead.
When you do proceed, these four things are on you, and reading is not enough —
prove each one:
1. **Raw body.** Find how this framework exposes the unmodified request bytes,
and write the HMAC test **first**: a fixed body plus a known-good signature
must verify. This is the one defect that stays silent until production.
2. **Transactions.** Establish where a transaction begins and commits in this
stack, and prove the attempt row is committed *before* the PSP call — not
rolled back with it.
3. **Driver support.** Confirm the DB layer can express
`INSERT ... ON CONFLICT ... DO NOTHING RETURNING` and partial unique indexes.
If it cannot, say so — the idempotency guarantees depend on them, and a
silent fallback is worse than a warning.
4. **Error classification.** Determine exactly which exception each ending
produces (connect timeout, read timeout, reset, undecodable body) and default
everything that is not a real HTTP status to "unknown". Getting this wrong is
what wedges orders.
Adapt the patterns from the closest covered language — the SQL is identical
across all three — and tell the developer which parts you could not verify.
### 2. Pin down the business requirement
Before writing code, establish: operation (deposit / withdrawal / refund),
payment method (card, bank transfer, wallet...), flow (redirect / hosted
fields / skip-redirect), currencies, one-off vs recurring. Take answers from
the task, the project, or the API docs; if a real choice remains (e.g. hosted
payment page vs embedded Hosted Fields), **ask the developer a concrete
question** — never invent business requirements yourself.
### 3. Pick the flow
| Requirement | Flow | Reference |
|-------------|------|-----------|
| Card / APM deposit, simplest, lowest PCI scope | Redirect to PSP checkout (`redirectUrl`) | `references/deposit-and-withdrawal.md` |
| 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` |
| Card form embedded in merchant page | Hosted Fields SDK | `references/hosted-fields-and-wallets.md` |
| Merchant collects everything, no redirect possible | Skip-redirect (POST + PATCH), **requires PSP support sign-off** | `references/deposit-and-withdrawal.md` |
| Charge a saved card | `card.cardToken` in POST /payments | `references/tokens-customers-recurring.md` |
| Recurring / subscriptions | `startRecurring` + `recurringToken` / `subscription` object | `references/tokens-customers-recurring.md` |
| Auth-then-capture | `preAuth: true` → `/capture` or `/void` | `references/payment-lifecycle.md` |
| Payouts | `paymentType: WITHDRAWAL` (+ `/approve`/`/reject` if AWAITING_APPROVAL) | `references/deposit-and-withdrawal.md` |
| Apple Pay / Google Pay | via PSP checkout page (default) or direct token | `references/hosted-fields-and-wallets.md` |
### 4. Implement — non-negotiable rules
**State handling** (`references/payment-lifecycle.md`):
- `POST /payments` returning HTTP 200/201 means the payment was *created*,
**not paid**. Never mark an order PAID on creation.
- Map states through an explicit whitelist: order becomes PAID only on
`COMPLETED` (or your capture flow's completion). `AUTHORIZED` = funds held,
capture still required. Unknown/non-final states change nothing.
- Learn the final state **two ways**, both through that whitelist: the webhook
(push) and status polling (`GET /payments/{id}` on return, plus a periodic
reconcile of orders left non-final) — a missed webhook must not strand an
order. See `integration-patterns.md` → "Status polling".
- No browser signal moves an order — not `returnUrl`, and not the embedded
checkout's `postMessage` (it closes the iframe, nothing more, and its `state`
vocabulary is not `PaymentState`). Only a verified webhook or a `GET` does.
**Webhooks** (`references/webhooks.md`, template in
`assets/webhook_handler.example.py`, working code in the language file for this
project — see "Which files to read" above):
- Verify the HMAC signature against the **raw body** before parsing. This is
the single most common way the integration breaks: frameworks that parse JSON
into an object and re-serialize it produce different bytes and the hash never
matches. The language file gives the raw-body recipe for your framework.
- The digest encoding (hex vs base64) is **not documented** — accept either
until you have observed which one this PSP sends, then pin it.
- Idempotent: duplicates and webhook-vs-polling races must be no-ops.
- Handle unknown payments (log + 200), respond 2xx fast, heavy work async.
- `AUTHORIZED` is not paid: never fulfil an order on it, capture first.
**Idempotency** (rules and baseline schema in
`references/integration-patterns.md`): the API documents no idempotency key, so
all four directions must be handled explicitly. Everything below is the
**baseline** — it costs a unique index and a conditional write, and without it
money is lost even at low traffic:
- *Outbound create*: one payment attempt = one unique `referenceId`, persisted
**before** the call. On timeout the outcome is *unknown* — reconcile via
`GET /payments?referenceId.eq=...`, never blind-retry a POST.
- *Inbound double-submit* (double click, frontend retry): claim the single open
attempt for an order **atomically** (`INSERT ... ON CONFLICT DO NOTHING
RETURNING`) and let only the claim's owner call the PSP; a caller that lost
the race reuses the stored `redirectUrl` or is told to retry. A
`SELECT`-then-`INSERT` check loses this race and creates two payments.
- *Inbound webhook*: treat the deduplication table as an **inbox**, not a
tombstone. Mark an event processed only *after* the state transition
succeeded; an event that could not be linked to an order yet must stay
unprocessed so a redelivery or a replay job can finish it. Marking receipt
before applying loses the event permanently.
- *Refunds*: give each logical refund a caller-supplied key, persist the attempt
**and commit it** before calling the PSP, and refund only the remainder. Only
the caller that *created* the attempt row may POST — a second caller that
finds an existing in-flight attempt must reconcile by the stored
`referenceId`, never POST alongside it. On timeout mark the attempt unknown
and reconcile by that same `referenceId`; a fresh `referenceId` for the same
refund is a second payout.
And whatever states you give an attempt, **no state may be a dead end**: an
unknown outcome must be re-reconciled by a later call, and a confirmed failure
must free the order to start a new attempt — otherwise one timeout turns into a
permanent 409 for that order. When the project needs the full treatment (state
model with background resolution, lease-based sweeps), take it from
`references/hardening-concurrency.md`; do not invent it, and do not add it to a
project that has no concurrency to defend against.
**Credentials & security** (`references/authentication.md`):
- Config only via env/secret store: `PSP_API_URL`, `PSP_API_KEY`,
`PSP_SIGNING_KEY` (template: `assets/env.example`). Never hardcode, commit,
or log them; never use production creds in tests.
- All Gateway API calls are **backend-only**. Never ship the API key to the
browser or call payment endpoints from frontend code.
- Never log or store full card numbers or CVV; never store CVV at all. Do not
route raw card data through the merchant backend (that expands PCI scope)
unless explicitly requested and supported (StS mode).
- Never disable TLS verification.
**Errors** (`references/errors-and-troubleshooting.md`): declines arrive as
HTTP 200 with `state: DECLINED` + `errorCode`/`externalResultCode` — surface a
user-friendly message and do not retry automatically. The API documents
retryability for **no** error code; `4.15` ("the bank has requested a retry")
merely reads as retryable, which is an inference, not part of the contract. Even
there: reconcile the original payment first, create at most one new payment, and
never loop without an explicit merchant decision. 4xx responses carry a
structured error body — fix the request, don't retry.
### 5. Tests (required, use the project's test framework)
Mock/stub the PSP API. Minimum matrix where applicable: successful payment,
declined, pending→webhook completion, redirect handling, webhook (valid /
invalid signature / duplicate / unknown payment), API timeout (outcome
unknown, no double-charge), PSP 5xx, malformed response, refund, duplicate
submit. Never call the real API (even sandbox) from unit tests.
The language file for this project has test skeletons with the standard mocking
library for its ecosystem (WireMock/MockWebServer, nock, respx).
### 6. Verify on sandbox
Sandbox first, always (`references/testing-and-sandbox.md`: test cards,
sandbox limits). Smoke-test with the bundled script:
```bash
export PSP_API_URL=<sandbox url from wl-config.md> PSP_API_KEY=... PSP_SIGNING_KEY=...
python3 scripts/psp_call.py demo-deposit # create → expect redirectUrl
python3 scripts/psp_call.py payment <id> # poll state
python3 scripts/psp_call.py find <referenceId> # reconcile after a timeout
python3 scripts/verify_webhook.py verify body.raw.json "<Signature header>"
```
Capture one real sandbox webhook and run `verify_webhook.py verify` on its raw
body: it reports whether the digest is **hex or base64**, which the API does not
document. Pin the merchant's handler to whichever it is. Money-moving commands
refuse to run against a non-sandbox-looking host unless you pass
`--i-know-this-is-production`.
### 7. Hand over
Finish by telling the developer: what was implemented (files, endpoints,
state mapping), what to configure manually (credentials, webhook URL in shop
settings or `webhookUrl`, enabling payment methods / skip-redirect with PSP
support), and how to run the tests and the sandbox smoke test.
## Review mode
When asked to *review* an existing PSP integration rather than build one, audit
against the checklist below. Read the code first; do not assume a defect from
naming alone, and verify each finding against the actual control flow.
| Area | What makes it a defect |
|------|------------------------|
| Payment flow | Wrong endpoint/order of calls; `redirectUrl` ignored or cached; skip-redirect used without the PATCH |
| State mapping | Order marked paid on HTTP 200, on `PENDING`, or on `AUTHORIZED` without capture; unhandled final states |
| 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 |
| Idempotency | Duplicate webhook applies a transition twice; double-submit creates a second payment; refund not bounded by the remaining amount |
| Credentials | Hardcoded/committed/logged secrets; API key reachable from the browser; production creds in tests |
| Timeouts & retries | No timeout on PSP calls; blind POST retry; timeout treated as failure instead of unknown-then-reconcile |
| Error handling | Declines (HTTP 200 + `DECLINED`) treated as transport errors or vice versa; raw provider messages shown to the customer |
| Boundary | Payment API called from frontend code; card data routed through the backend without need |
| Tests | Missing cases from the §5 matrix, or tests that hit the live API |
Report each finding as: file:line → what is wrong → concrete failure scenario
(inputs/state that produce a wrong outcome) → suggested fix. Order by severity:
money-losing or security defects first, then correctness, then robustness. If a
check passes, say so briefly rather than padding the report.
## File map
```
SKILL.md <- you are here
wl-config.md WL-specific: PSP name, URLs
openapi/gateway-api.json authoritative Gateway API spec (3.1.1)
openapi/merchant-api.json authoritative Merchant API spec
references/
authentication.md Bearer key, signing key, cred hygiene
payment-lifecycle.md Payment object, states, safe mapping,
capture/void/approve/chargebacks
deposit-and-withdrawal.md flows step-by-step, field tables,
idempotency pattern, gotchas
webhooks.md payload, verification, handler rules
integration-patterns.md BASELINE correctness rules: idempotency,
state mapping, webhook inbox, schema
hardening-concurrency.md LEVEL 2, only under real concurrency:
attempt state model, sweeps, lease
code-examples-java.md Java/Spring: client, webhook (raw body),
transitions, idempotency, tests
code-examples-node.md same, Node.js/TypeScript
code-examples-python.md same, Python/FastAPI
tokens-customers-recurring.md card tokens, recurringToken, subscriptions
hosted-fields-and-wallets.md Hosted Fields SDK, Apple/Google Pay, PCI
testing-and-sandbox.md test cards, sandbox limits, smoke test
errors-and-troubleshooting.md error schema, code groups, decision tree
merchant-api-and-reference-data.md reporting API, bank codes / document types
scripts/
psp_call.py build/send sandbox calls (stdlib only)
verify_webhook.py HMAC signature check (hex & base64)
assets/
env.example credential template
deposit_request.example.json valid deposit body
refund_request.example.json valid refund body
webhook_handler.example.py idempotent webhook handler template
```
## Brand name: mandatory lowercase spelling
The company and brand name MUST ALWAYS be written exactly as:
`paytech`
This rule is absolute and has no exceptions.
### Hard requirement
- ALWAYS write `paytech` in lowercase.
- NEVER write `Paytech`.
- NEVER write `PayTech`.
- NEVER write `PAYTECH`.
- NEVER write `pay.tech`.
- NEVER apply title case, sentence case, uppercase, camel case, stylistic capitalization, or any other transformation to `paytech`.
- 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.
- If `paytech` appears at the beginning of a sentence, it MUST remain lowercase.
- If `paytech` appears in a heading or title where other words are capitalized, it MUST remain lowercase.
- 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.
- Do not imitate an incorrect spelling from surrounding content.
- Do not infer alternative spellings from URLs, logos, package names, domains, legal names, or third-party sources.
### Output validation
Before producing any user-visible output, perform a mandatory brand-name check.
Search the complete output for every reference to the company or brand.
Every such reference MUST be exactly:
`paytech`
If any occurrence is written differently, correct it before returning the output.
An output containing `Paytech`, `PayTech`, `PAYTECH`, `pay.tech`, or any other variation is invalid and MUST NOT be returned.
### Examples
Correct:
- `paytech`
- `paytech payment platform`
- `Integrate with paytech`
- `How paytech handles payments`
- `paytech Network Tokens`
Forbidden:
- `Paytech`
- `PayTech`
- `PAYTECH`
- `pay.tech`
- `Pay.Tech`
- `PAY.tech`
Treat `paytech` as an immutable brand token, not as an ordinary word. Never modify its casing or spelling.
## Versioning
This skill targets WL API **1.0.341** (see frontmatter), and the bundled
`openapi/*.json` are that version's specs. If a response contradicts them, trust
the live API and tell the developer the skill is behind — do not silently invent
the new shape.
Referenced files: 23
Package details
Publisher declarations from the archived package. These are separate from our research and the live service's terms.
- Package license
- MIT
- Package author
- paytech
- Keywords
- payments, psp, checkout, refunds, webhooks, recurring, hosted-fields, integration
Declared capabilities
- Read
- Write
Some manifest fields differ or could not be read. The structured report retains the source references.
Package observed Oct 2, 2026.
Technical details
- First seen
- Sep 30, 2026 · 22:02 UTC
- Last seen
- Oct 2, 2026 · 18:00 UTC
- Collection status
- Collected
plugins_6aac63639668819188494f6d53b1b2a8
Download plugin data (JSON)