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

Two consent models: dedicated accounts, where you name the IBANs, and bank-offered, where the customer chooses them on our screen.

Dedicated accounts

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
}

Which lists you fill decides the scope: accounts grants account details; balances or transactions grants balance and transaction data (the two come together). Always fill accounts as well. A consent with only balances or transactions cannot list accounts — GET /v1/accounts answers CONSENT_INVALID — so you would have no resourceId to address the balances and transactions you were granted.

Bank-offered

Send the lists you want empty, and the customer ticks the accounts to share when they authorise:

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

Refused

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. access.statements (IG v1.3.16) is refused the same way: account statements are not offered.

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

Consents last at most 180 days (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. A validUntil date runs to the end of that day.

The ceiling is applied when you create the consent and again when the customer authenticates, and the earlier of the two wins. Because the customer has only 15 minutes to authenticate, in practice the 180 days count from your creation request.

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. A one-off consent allows one of each.

A paged read counts once. Only the first page of a transaction read is counted. A request carrying entryReferenceFrom within 15 minutes of a counted read of the same consent and account continues that read and is not counted; later, it counts as a new access.

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 carries no X-RateLimit-* headers and never refuses on the daily budget. 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.

Every operation, capped or not, also sits behind a per-address flood ceiling set far above any normal provider rate. Over it you get 429 ACCESS_EXCEEDED with a Retry-After of at most a minute and no X-RateLimit-* headers — wait that long and retry unchanged.

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 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 at least one hour. Retirement is done by a daily sweep, so a spent consent may stay usable for longer than that hour — do not rely on it either way. Once retired, 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 (at most 15 minutes)
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, the 15-minute authorisation window lapsed before they finished, or you deleted the consent before they authorised it

A consent past its validUntil reads expired and is refused with CONSENT_EXPIRED from that moment. A spent one-off consent reads expired once the daily sweep retires it (see above); until then it is still valid.

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 any of 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 it — DELETE /v1/consents/{consentId} (or DELETE /v1/consents/confirmation-of-funds/{consentId}), answered 204. No notification is sent back; you are the party asking. A consent deleted before the customer authorised it reads rejected, not terminatedByTpp.
  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:8443/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 comes back with your callback registration, and GET /v1/revocation-callback returns the same value whenever a callback is registered — there is nothing to re-issue and no ticket to raise. If we ever rotate the key behind it, that is 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 increasing backoff, on a connection failure or timeout (5 seconds to connect, 5 to respond). A response we receive is not retried, whatever its status — so answer 2xx once you have the event, and do not use a non-2xx to ask for a redelivery
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.