PaySaxas XS2A
PSD2 access to account — Berlin Group NextGenPSD2 v1.3.15

← All documentation

Consents

A consent is the customer’s recorded permission for you to reach specific accounts for specific data, for a limited time.

What we accept

Dedicated accounts only. List the IBANs in the access object:

{
  "access": {
    "accounts":     [{"iban": "FI2112345600000785"}],
    "balances":     [{"iban": "FI2112345600000785"}],
    "transactions": [{"iban": "FI2112345600000785"}]
  },
  "recurringIndicator": true,
  "validUntil": "2026-12-31",
  "frequencyPerDay": 4
}

availableAccounts, availableAccountsWithBalance and allPsd2 are refused with SERVICE_INVALID rather than silently narrowed — you must never believe you received a broader grant than you did.

Which lists you fill decides the scope: accounts alone grants account details; balances or transactions additionally grants balance and transaction data.

combinedServiceIndicator is not offered

Consents and payments are separate resources here, so there is no combined AIS+PIS session. Sending combinedServiceIndicator: true is refused with SERVICE_INVALID rather than quietly granting you an ordinary consent you would then use as though it covered a payment. Omit the field or send false, and create the consent and the payment separately.

Read the grant back — it may be narrower than you asked for

GET /v1/consents/{consentId} returns what was granted, not what was requested, and the two can differ: the customer may authorise fewer accounts than you named, and we cap validUntil silently.

{
  "consentId": "PSXS-CONSENT-…",
  "consentStatus": "valid",
  "validUntil": "2026-12-28",
  "recurringIndicator": true,
  "frequencyPerDay": 4,
  "access": {
    "accounts":     [{"iban": "FI21…"}],
    "balances":     [{"iban": "FI21…"}],
    "transactions": [{"iban": "FI21…"}]
  },
  "_links": { "self": {"href": "…"}, "status": {"href": "…"} }
}

An omitted or empty list means that data type was not granted. If access carries accounts but no balances, balance reads on that consent will be refused — check here rather than discovering it at the first CONSENT_INVALID. An IBAN you asked for that is absent from accounts does not belong to the customer who authenticated; we do not say which, because that would answer a question about a third party’s accounts.

A confirmation-of-funds consent carries account instead of access — it is not a data-access grant (see funds confirmations).

Validity: the 180-day ceiling

Recurring consents last at most 180 days from the customer’s authentication (RTS Art. 10a as amended by (EU) 2022/2360). Ask for less and we honour it; ask for more and we cap it silently — read validUntil back from the consent rather than assuming your request was taken literally.

The clock starts when the customer authenticates, not when you asked.

Frequency: the four-per-day cap

Unattended requests are limited to four per consent, per data type, per day (RTS Art. 36(5)) — so four account reads, four balance reads and four transaction reads, counted separately.

Attended requests (those carrying PSU-IP-Address) are not capped and do not consume the unattended budget.

Exceeding it returns ACCESS_EXCEEDED (429). The counter resets daily.

What the cap applies to

Only reads of the customer’s data, and the funds-confirmation service:

Operation Capped?
GET /v1/accounts, …/{id}, …/balances, …/transactions Yes — one budget per data type
POST /v1/funds-confirmations Yes — a CBPII polling solvency is exactly what Art. 36(5) governs
Consent create / read / status / delete, and the authorisation reads No
Everything under /v1/payments No

An uncapped operation returns no 429 and carries no X-RateLimit-* headers. That is not an invitation to poll them: they are uncapped because they are administrative rather than a window onto the customer’s data, and abuse is handled as a registration matter rather than by a counter.

Knowing what is left before you spend it

Every consent-gated response carries the remaining budget, so you never have to burn a request to discover you are out:

X-RateLimit-Limit:     4
X-RateLimit-Remaining: 3
X-RateLimit-Reset:     1785283200

Prefer these headers to a local counter. Yours cannot see calls made by your other processes or hosts, and it resets when you deploy; ours is the number the gate actually enforces.

frequencyPerDay and this cap are the same number

They are not two budgets. frequencyPerDay is the per-data-type unattended allowance described above — four account reads and four balance reads and four transaction reads, not four calls in total.

This is why a request for frequencyPerDay: 10 comes back as 4 rather than as an error, and why it says 4 both before and after the customer authorises.

One-off consents (recurringIndicator: false)

A one-off consent is for a single errand — read the data once and be done. It behaves differently from a recurring one in three ways, and all three matter for how you build against it:

  Recurring One-off
frequencyPerDay 4 unattended accesses per data type, per day 1 per data type. We force this value; requesting more does not change it
Lifetime Up to 180 days from authentication Ends shortly after you use it — see below
Supersession A renewal supersedes the older consent Never supersedes; there is nothing standing to replace

“Spent” is not instantaneous, deliberately. A one-off consent usually backs a sequence — accounts, then balances, then transactions — and retiring it the moment the first call landed would break that sequence halfway. So the consent is retired once it has been used and has been idle for one hour. Inside that window you can complete a multi-leg read; after it, the consent is expired and further calls return CONSENT_EXPIRED.

Note what that hour is not: it is not extra allowance. The one-per-data-type cap still applies, so the window lets you finish a read, not repeat one.

Attended requests (carrying PSU-IP-Address) are uncapped here as everywhere — the cap governs unattended access. The idle clock still runs.

Which to ask for. If you will come back — an accounting integration, a dashboard — ask for recurringIndicator: true and renew before validUntil. A one-off is for a one-time check where asking the customer again is acceptable.

Status lifecycle

Status Meaning
received Created, awaiting the customer’s authentication
valid Authorised and usable
expired Past validUntil, or a spent one-off consent (see above)
revokedByPsu Ended by the customer or by PaySaxas
terminatedByTpp Ended by you, or superseded by a newer consent of yours
rejected The customer declined

Note that revokedByPsu covers a compliance-driven revocation as well as a customer-driven one. That is deliberate: we will not tell you which, because the distinction can reveal facts about a customer that we may not disclose.

Renewal and supersession

To renew, create a new consent and send the customer through SCA again. When they authorise it, any earlier recurring consent of yours covering the same accounts moves to terminatedByTpp. A customer never accumulates several live recurring grants to the same provider for the same accounts.

Consents held by other providers are untouched.

Revocation

Three ways a consent ends before its time:

  1. You terminate itDELETE /v1/consents/{consentId}. No notification is sent back; you are the party asking.
  2. The customer revokes it in their PaySaxas portal, under Settings → Connected services. No ceremony is required of them — withdrawing a permission must not be harder than granting it.
  3. PaySaxas revokes it on support or compliance grounds.

In cases 2 and 3 we attempt a callback if you have registered one. The callback is a courtesy, not the control: the consent stops working at the gate immediately, whether or not we reach you. Build for CONSENT_INVALID arriving without warning.

Registering a revocation callback

Self-service — no support ticket:

curl -X PUT https://xs2a.paysaxas.com/v1/revocation-callback \
  --cert tpp-qwac.pem --key tpp-qwac.key \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: 3b7e2c11-9f4d-4c2a-8f1e-6d5a4b3c2e10" \
  -d '{"url": "https://your-app.example/psd2/revocation"}'
{
  "url": "https://your-app.example/psd2/revocation",
  "signatureHeader": "X-PaySaxas-Signature",
  "signatureToleranceSeconds": 300,
  "secret": "…"
}

Until you register one, revocation notifications are marked “not required” and you will discover the change on your next call.

The callback body names the consent and its new status, and carries no reason:

{
  "eventId": "b6f1…",
  "consentId": "PSXS-CONSENT-3f2b...",
  "consentStatus": "revokedByPsu",
  "revokedAt": "2026-08-02T10:15:00Z"
}

Verifying the callback

Do not act on an unverified callback. The URL is registered privately, but an obscure endpoint is not authentication: anyone who learned it and a consentId could otherwise convince you a live consent was dead.

Every callback carries an HMAC-SHA256 signature:

X-PaySaxas-Signature: t=1785000000,v1=6c1f…

v1 is the hex HMAC-SHA256, keyed with your callback secret, over the string "{t}.{raw request body}". To verify:

  1. Split the header into t and v1.
  2. Reject if t is more than 300 seconds from your clock.
  3. Recompute over "{t}.{body}" using the raw bytes you received — not a re-serialised parse, which will not match.
  4. Compare in constant time.

Your secret is issued with your callback URL; ask support to re-issue it. Rotations are announced in advance.

Other properties you can rely on:

   
Transport HTTPS only. We will not send to an http:// URL
Retries Up to 5 attempts with exponential backoff, on connection failure or a non-2xx. Return 2xx to stop them
Replay eventId is stable per revocation and repeated on every retry — dedupe on it. Two revocations can share a revokedAt second, so do not dedupe on that
Ordering Not guaranteed. Treat each event as a statement about one consent, not as a sequence
Source addresses Callbacks originate from our published egress ranges. Ask support for the current list if you allowlist by IP — but allowlisting is not a substitute for checking the signature

If verification fails, drop the message and rely on the gate — a genuine revocation is already in force, so nothing is lost by ignoring a callback you cannot trust.