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
}
- Every list you send must be empty. If one list names an IBAN, the request is a dedicated-account consent and the empty lists are ignored.
- The lists decide the data, the customer decides the accounts. An empty
balancesortransactionslist asks for balance and transaction data for every chosen account. Account details are always included, so you can always list what you were given. - Until the customer authorises, the lists read back empty. Afterwards,
GET /v1/consents/{consentId}names the accounts they chose. Read it back before you callGET /v1/accounts. - The customer is offered their own euro accounts as their PaySaxas portal shows them. Nothing is ticked for them, and at least one account must be chosen.
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
- Per data type, matching the cap itself. The numbers on a balances response describe the balances budget; they say nothing about transactions.
X-RateLimit-Resetis a Unix timestamp, not a duration — the next reset, which is a calendar boundary rather than a rolling window from first use.- On a
429you also getRetry-After(seconds). Wait it out; retrying sooner cannot succeed. - Attended requests do not consume the budget, so
Remainingis unchanged by them — but it still tells you what an unattended call would find.
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.
- Asking for more than 4 is capped, not refused — the same treatment as an
over-long
validUntil. Your consent is created; it just permits four. - Asking for fewer does not reduce it. We do not enforce a self-limit, so we do not claim one: the consent reports 4 either way. If you want to make fewer calls, make fewer calls.
- The granted consent reports what is enforced, not what you requested — 4 for a recurring consent, 1 for a one-off. Read it back; the number you get is the number that applies.
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:
- You terminate it —
DELETE /v1/consents/{consentId}(orDELETE /v1/consents/confirmation-of-funds/{consentId}), answered204. No notification is sent back; you are the party asking. A consent deleted before the customer authorised it readsrejected, notterminatedByTpp. - 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.
- 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": "…"
}
secretis your HMAC key. It is derived from your registration rather than stored, so if you lose it,GET /v1/revocation-callbackreturns the same value — there is no reset ceremony. While no callback is registered, thatGETanswers404 RESOURCE_UNKNOWN.- The URL must be absolute
httpswith no fragment, or thePUTis refused withFORMAT_ERROR. PUTdeclares state, so a repeat with the same URL changes nothing and a repeat with a different one replaces it.DELETEstops notifications.- Available to any registered provider, whatever your certificate authorises. Managing your own notifications is not an AIS, PIS or CBPII service, so it is not gated on one of those roles.
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:
- Split the header into
tandv1. - Reject if
tis more than 300 seconds from your clock. - Recompute over
"{t}.{body}"using the raw bytes you received — not a re-serialised parse, which will not match. - 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.