TPPVALIDATION
Talk to us

One POST. Every PSD2 check.

Behind your API gateway, the certificate in and the verdict out: the chain against 31 EU trusted lists, OCSP or CRL, qualification as ETSI TS 119 615 defines it, the PSD2 roles, and the licence in the EBA register with the passports you asked for. Headers for your gateway, JSON for your logs, milliseconds from a warm engine.

Checked again, several times a day.

A certificate that passed this morning can be revoked by lunch, and a licence withdrawn without the certificate changing at all. Give a webhook URL with the call and the certificate goes on our list: re-validated several times a day, and you hear from us, signed, the moment its status changes.

The key, not just the certificate.

A certificate is public. Behind your gateway you only know that someone sent it. Two ways to know they hold the private key: a single-use challenge they sign, or the message signature already on the request, RFC 9421 or the Berlin Group profile. Both verified against the leaf key, in the same call.

Every answer, an audit record.

An audit id on every answer and a record kept as it was sent. Each record is hashed, signed with your account's key and bound to the one before it, and the chain is anchored every hour with rFC 3161 qualified electronic time-stamps issued by AlfaTrust Certification S.A., a Qualified Trust Service Provider on the EU Trusted Lists: legally recognised in every Member State under eIDAS Article 41, verifiable with the EU DSS validator. Printed as a sheet or exported as an evidence record, it validates with openssl alone, or by anyone at tppv.app/verify: integrity, authenticity and proof of existence. It is what RTS 2018/389 Articles 29 and 34 and PSD2 Article 68 ask a bank to show for an allow or a reject, and what the PSR keeps asking for once it applies.

  • 1POST per certificate
  • 3 msengine, warm
  • 31trusted lists
  • 24/7re-checks, several a day
  • 4routes
  • 1 842calls, 24 h

A1 · One call

Certificate in. Verdict out.

One POST from behind your gateway. The engine builds the chain against the 31 EU trusted lists, asks the issuer's OCSP responder or reads its CRL, decides qualification the way ETSI TS 119 615 defines it, reads the PSD2 roles from the certificate, and looks the licence up in the EBA register with the passports you asked for. The answer comes back as headers for your gateway and JSON for your logs. Ask again and it comes from cache, until you say otherwise.

The call

One POST, the certificate as the body

PEM, a PEM chain or base64 DER, with any content type but JSON. Jurisdictions in cc name the passports the licence must cover, ISO codes, uppercase, comma separated or repeated.

curl -si -X POST "https://api.tppvalidation.com/v6?cc=SE,DE&details=true" \ -H "Authorization: Bearer $TOKEN" \ --data-binary @qwac.pem
The answer

Headers for the gateway, JSON for the logs

Status 200 means valid. One header says what was decided, one says why when it was not. The rest is in the JSON.

HTTP/2 200 X-TPP-Internal: VFD X-TPP-Passports: SE=PSP_AI,PSP_PI;DE=PSP_AI HTTP/2 422 X-TPP-Internal: VLW X-TPP-Reason: withdrawnLicense, 2026-09-30
01

Chain and lists

The chain is built from the certificate, the CAs the trusted lists name, and the issuer's AIA. The first list-anchored certificate is the trust anchor. 31 lists, re-read around the clock, signatures verified against the LOTL.

02

Revocation

OCSP with a nonce first, CRL as the fallback, for every certificate below the anchor. Answers are reused only until their nextUpdate.

03

Qualification

ETSI TS 119 615, as the regulation defines it: the CA/QC services in the chain, their status on the date, the qualifiers, QWAC or QSeal, at issuance and at validation.

04

Register

The organisation identifier from the certificate against the EBA register: licence present, not withdrawn, passports for the jurisdictions you named. Withdrawn comes back with its date.

A2 · The verdict

The status code decides. The headers explain.

The answer comes to your backend from us over TLS, with nothing in between: a header in it is as trustworthy as the body. Your gateway branches on the status code and two headers. Everything else is in the JSON as well, and in headers when a proxy that cannot read a body needs it.

Branch onWhat it carries
Status code200 is the only yes. 422 is a certificate or licence that does not pass. 401 and 403 are about your account, 503 about an upstream we depend on.
X-TPP-InternalThe result code. VFM or VFD on a fresh verdict (minimal or detailed), QFM or QFD from cache. On a refusal: MPP, VML, VLW, VPM, VFA, VCT, SIG or POP.
X-TPP-AuditThe id of this answer's own audit record, on every authenticated answer: fresh, cached or refused. Paste it on your account page and the verdict is traced to its sources; the record is kept from the moment the answer went out.
X-TPP-ReasonWhy, when it is not 200: missingLicense, withdrawnLicense with its date, missingPassports, the engine's sub-indication, an EBA upstream code, or the signature or possession reason.

Also present when it applies, each a field of the JSON too: X-TPP-Entity and X-TPP-Cert (entity type from the register and from the certificate), X-TPP-Passports (SE=PSP_AI,PSP_PI;DE=PSP_AI), X-TPP-Identifier (the hash of the EBA snapshot used), X-TPP-Latest (when it ran), X-TPP-Sig and X-TPP-Pop (valid or invalid, only when asked), X-TPP-Engine and X-TPP-Version, and Server-Timing with auth, parse, cache, engine and persist.

Status codes

Branch on the number

200 is the only yes. 422 is a certificate or licence that does not pass, with the reason in the headers. 401 is your bearer, 503 is an upstream we depend on. The Gatekeeper answers differently on purpose: a proxy only understands 2xx and 403, so there a failed QWAC is 403 in enforce and 200 with the verdict attached in observe. Same engine, same reason codes, two answer languages.

200 valid 401 bearer missing, malformed, expired, or account expired 403 account disabled 400 not a certificate, no PSD2 certificate in the chain, bad jurisdiction codes, or a route that does not exist 422 MPP not a PSD2 certificate · VML no licence · VLW withdrawn VPM no passport for those jurisdictions · VFA validation failed VCT not qualified (test certificate?) · SIG · POP 503 EBA register or engine upstream unavailable · Retry-After: 300
The JSON

The record, flattened

With details=true you get the whole record; without it, three fields that name the certificate and the moment.

{ "serialNumber": "…", "organizationIdentifier": "PSDSE-FINA-556000-0000", "entityName": "…", "entityCountry": "SE", "entityType": "PSP_AI", "ncaData": { "ncaName": "Finansinspektionen", "ncaCountryCode": "SE", … }, "pspPassports": [ { "SE": ["PSP_AI","PSP_PI"] }, { "DE": ["PSP_AI"] } ], "certificateRevocation": "good", "certificateData": { "qualificationAtValidation": "QC for WSA", "notAfter": "…", "ocsp": "…", "trustList": { … }, … }, "validationTime": "2026-10-25T09:14:02Z", "dataHash": "7c1f…", "webhookRegistered": false }

A3 · Endpoints

Four routes. Three parameters that matter.

RouteDoesBody
POST /v6Validates the certificate and, when asked, a message signature or a possession proof.PEM, chain or base64 DER as the body; or JSON with pem, webhook_url, pop, http_sig.
POST /v6/challengeIssues a single-use possession challenge, 120 seconds.Empty, or bind: { client_id, purpose }.
GET /v6/subscriptionsLists the certificates this account is watching, with their webhook URL.None.
DELETE /v6/subscriptions/{issuer}/{serial}Stops watching one certificate. 204, or 404 when nothing was watched.None.
QueryMeaning
cc=SE,DEThe jurisdictions the licence must hold a passport for. Uppercase ISO codes, comma separated or repeated. Codes we do not know are ignored; with none left, 400. 31 codes: the EU 27, Iceland, Liechtenstein, Norway, and GB for the legacy cases.
details=trueThe full record in the JSON, not just the three identifying fields.

Limits: 256 KB request body, 64 KB of PEM. Unknown routes answer 400. CORS preflight is answered for Authorization, Content-Type and X-Webhook-URL.

A4 · Proof of possession

Know the caller has the key.

A certificate is public. Behind your gateway you only know that someone sent it. Two ways to know they hold the private key: a challenge they sign, or the message signature they already put on the request, RFC 9421 or the Berlin Group's older form. Both verify against the leaf key of the certificate you submit, in the same call.

Challenge

A nonce, signed once

Ask for a challenge, hand the TPP the string to sign over your own channel, post certificate and signature together. The nonce lives 120 seconds and is consumed on first use.

POST /v6/challenge { "bind": { "client_id": "tpp-42", "purpose": "onboarding" } } → { "challenge_id": "01JB…", "challenge": "…", "expires_in": 120, "tbs": "tppv-pop-v1|01JB…|<challenge>|tpp-42" } POST /v6?cc=SE (application/json) { "pem": "…", "pop": { "challenge_id": "01JB…", "signature_b64": "…", "client_id": "tpp-42" } } → X-TPP-Pop: valid
Message signature

The signature already on the request

Hand us the signature your gateway received and we verify it against the leaf key: RFC 9421 or the Berlin Group legacy profile, detected when you do not say. Created within five minutes, keyid matching the certificate, digest matching the body when you give it.

POST /v6?cc=SE (application/json) { "pem": "…", "http_sig": { "profile": "rfc9421", "signature": "…", "signature_input": "…", "content_digest": "…", "created": 1792742400, "covered": { "@method": "POST", "@path": "/psd2/v1/payments", "content-digest": "…", "x-request-id": "…" }, "body_sha256_b64": "…" } } → X-TPP-Sig: valid # algorithms: RSA PKCS#1 and PSS with SHA-256/512, ECDSA P-256 and P-384

A5 · Re-checks and webhooks

Checked once by you. Then by us, several times a day.

A certificate that passed this morning can be revoked by lunch, and a licence can be withdrawn without the certificate changing at all. Give a webhook URL with the call and the certificate goes on our list: re-validated several times a day, and you hear from us the moment its status changes. Every delivery is signed.

Subscribe

One header on the call

Send the URL in X-Webhook-URL or as webhook_url in a JSON body, never both. https only. Registration happens only on a valid verdict, and the response tells you whether it did. The same URL posted again confirms the subscription; a new URL replaces it.

curl -s -X POST "https://api.tppvalidation.com/v6?cc=SE" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Webhook-URL: https://hooks.bank.example/tpp" \ --data-binary @qwac.pem | jq .webhookRegistered true GET /v6/subscriptions DELETE /v6/subscriptions/{issuer_hash}/{serial} → 204
Deliver

Signed, with an id you can dedupe on

Events: cert.status_changed when a re-check flips a verdict, and webhook.subscription_created, _updated and _deleted for the subscription itself. The signature is an HMAC-SHA256 over the timestamp and the raw body, with the secret we provision for your account.

POST https://hooks.bank.example/tpp X-Webhook-Event-Id: evt_… X-Webhook-Delivery-Id: dlv_… X-Webhook-Signature: t=1792742400,v1=<hex hmac-sha256(secret, "<t>.<body>")> X-TPP-Client-ID: <your client id> User-Agent: tpp-validator-webhooks/5.0 { "event_id": "evt_…", "event_type": "cert.status_changed", "created_at": "2026-10-25T17:31:04Z", "data": { "issuer_hash": "…", "serial": "…", "entity_name": "…", "old_status": "valid", "new_status": "invalid", "reason": "revoked", "checked_at": "…" } }

Delivery waits ten seconds for your 2xx. A Slack incoming-webhook URL gets a formatted message instead of the JSON, and no signature.

A6 · CLI, MCP, Slack and Teams

The same verdict, from a terminal, an agent or a channel.

tppv validate ./qwac.pem --cc SE,DE,NL ✔ PASSED QWAC PSDSE-FINA-556000-0000 PSP_AI 138 ms tppv validate --url https://psd2.bank.example # the server certificate of a host tppv drop # watch a folder, validate what lands tppv integration --gateway nginx # a sideband rule for your proxy tppv status · tppv config · tppv trial
Add to SlackAdd to TeamsThe official Add to buttons, drawn our way. Teams also comes with the Microsoft Marketplace offer.

The CLI, the MCP server, the Slack app and the Teams bot all call the same API with your bearer. Nothing happens in them that you could not do with curl.

A7 · Access

A bearer, minted on your account page.

01

The bearer

A signed token carrying your API key and an expiry. Mint it on your account page, send it as Authorization: Bearer on every call. A new one replaces the old; the full value is shown once.

02

What 401 and 403 mean

401: the bearer is missing, malformed, expired, or the subscription behind it has ended. 403: the account was disabled. Neither is about the certificate.

03

Per environment, unlimited

A subscription covers one environment with as many certificates and calls as it needs. There is no call quota in the API. A trial runs 30 days without a card.

04

Webhook secret

Signed deliveries need a secret on your account. We provision it when you start subscribing; ask and it is there the same day.

A8 · Plain facts

What the API does, exactly.

Cached means cachedA verdict is kept by issuer and serial and served again until a re-check changes it. A cached refusal is a refusal too.
No keys, everOnly the public certificate travels. Nothing in the API asks for a private key, and a possession proof is a signature over our challenge, never the key itself.
The engine is custom builtTailor-made from EU DSS, the trusted lists in our own store. The Commission's reference validator stays available as a shadow to compare against when an audit asks for it.
31 jurisdiction codesThe EU 27, Iceland, Liechtenstein and Norway are the PSD2 area. GB is still accepted for the certificates that carry it.
Re-checks run several times a dayNot continuously: a revocation is seen at the next run, hours later at most. For a verdict at the moment of the request, the Gatekeeper sits beside your proxy.
Every answer leaves a recordFresh, cached or refused, each authenticated answer carries X-TPP-Audit and is kept as sent, with its sources and its time in UTC. Sealed into your account's chain and anchored with rFC 3161 qualified electronic time-stamps issued by AlfaTrust Certification S.A., a Qualified Trust Service Provider on the EU Trusted Lists: legally recognised in every Member State under eIDAS Article 41, verifiable with the EU DSS validator. That is the traceability RTS 2018/389 Article 29 asks for, and the evidence Article 68 of PSD2 asks for when access is refused.
Availability is a figure, not a contractThe numbers on the start page are measured, not promised. A service level is part of an agreement, and we write one when you want it.