Tokens and keys
The token endpoint, the key you get back, how to present it, and how to revoke it.
La documentation partenaires est publiée en anglais.
#The token request
POST {issuer}/oauth/token with Content-Type: application/x-www-form-urlencoded:
Authenticate the client either with HTTP Basic (client_secret_basic) or with client_id and client_secret in the body (client_secret_post). Using both is invalid_request. redirect_uri is optional; if you send it, it must equal the one you used on /oauth/authorize.
First-generation clients send { "code", "codeVerifier", "clientId", "clientSecret" } as application/json instead; that shape keeps working and answers the same response.
#The token response
access_token and apiKey are the same key; apiBaseUrl is the issuer; user is the tenant subject the key reads for. The response is Cache-Control: no-store.
The key lives 365 days or until revoked. There is no refresh token: when it expires, send the user through Connect again.
#Errors
RFC 6749 §5.2 bodies, { "error", "error_description" }:
| Status | error | Meaning |
|---|---|---|
| 400 | invalid_request | a parameter is missing, both client-authentication methods were used, or the content type is neither form nor JSON |
| 400 | unsupported_grant_type | grant_type is not authorization_code |
| 401 | invalid_client | unknown client or wrong secret; WWW-Authenticate: Basic realm="oauth" when Basic was used |
| 400 | invalid_grant | the code is unknown, expired, already used, bound to another client or redirect URI, or the verifier does not match |
Every invalid_grant is terminal for that code — it was consumed before the check. Restart the flow.
#Presenting the key
Both are accepted everywhere and behave identically:
The SDKs and the CLI send X-Api-Key; generic OAuth2 client libraries send Bearer. The key is scoped to accounts.read and to the user who granted it.
#Why there is no id_token
Connect delegates access to a user's data; it does not assert who the user is to you. An OpenID id_token would add a signing key for you to manage and tell you nothing the API does not already: GET /oauth/userinfo with the key returns the subject, the email the user signed in with, your partner id, the client id, the scope and the expiry.
#The private key is a credential
privateKey from the relay decrypts everything the API returns for this user's data in your tenant — a user who connects through two partners has two independent keys, and revoking the API key does not rotate it. Treat it exactly like the client secret: no logs, no URLs, no analytics, no third parties — and exclude your callback path from any reverse-proxy, WAF or APM request-body capture, which is where it actually leaks. Store it encrypted at rest, one row per user connection, or hold it in memory only; losing it means the user connects again.
The key lives 365 days; the bank consent behind it lives at most 180 days (many banks: 90). Expect a reconnect_needed well before the key expires and send the user through Connect again — that renews the bank consent and, on the same visit, the key.
Accounts, balances and transactions arrive as envelopes: ephemeral ECDH P-256 → HKDF-SHA256 → AES-256-GCM, decrypted locally with that key. Envelope formats are versioned and readers support every writer version; the SDKs decrypt for you, and the Node client builds straight from the token response and the relayed key:
#Revoking
POST {issuer}/oauth/revoke (RFC 7009), form-encoded token=ebk_… with the same client authentication as the token endpoint. Only a key issued to your client is touched; the answer is 200 whether or not the key existed. Call it when a user disconnects from your side; drop the private key with it.
A key can also end without you: the user revokes it under Connected apps on their open-banking.io developers page (a grant made through you is listed there, under your client's name), or a partner suspension turns it off. Either way the next request answers 401 — drop the bundle and offer to connect again.
#Inspecting a key
GET {issuer}/oauth/userinfo with the key: