Tokens and keys

The token endpoint, the key you get back, how to present it, and how to revoke it.

Partnerdokumentasjonen publiseres på engelsk.

#The token request

POST {issuer}/oauth/token with Content-Type: application/x-www-form-urlencoded:

grant_type=authorization_code
&code=…
&code_verifier=…
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback

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

json

{
  "access_token": "ebk_…",
  "token_type": "Bearer",
  "expires_in": 31536000,
  "scope": "accounts.read",
  "apiKey": "ebk_…",
  "apiBaseUrl": "https://open-banking.io",
  "user": "wl:…:connect:[email protected]"
}

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" }:

StatuserrorMeaning
400invalid_requesta parameter is missing, both client-authentication methods were used, or the content type is neither form nor JSON
400unsupported_grant_typegrant_type is not authorization_code
401invalid_clientunknown client or wrong secret; WWW-Authenticate: Basic realm="oauth" when Basic was used
400invalid_grantthe 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:

Authorization: Bearer ebk_…
X-Api-Key: ebk_…

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:

js

import { OpenBankingClient } from '@open-banking-io/client';

const client = OpenBankingClient.fromTokenResponse(token, privateKey);

#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:

json

{
  "sub": "wl:…:connect:[email protected]",
  "email": "[email protected]",
  "partner_id": "…",
  "client_id": "obc_…",
  "scope": "accounts.read",
  "expires_at": "2027-08-26T00:00:00+00:00"
}