Access on a member's behalf
How a third-party app registers, asks a member for access, and reads what the member allowed.
What this is for
A service connection belongs to a company and is made by its manager. This guide is for the other kind. That is an app that a member chooses, such as an AI assistant or a reporting tool, that acts for the member.
The app registers itself and sends the member to Norra. On Norra's consent page, the member chooses what the app gets and signs the consent with BankID. The app then trades a code for tokens. Norra speaks OAuth 2.1 with PKCE for this, and every step below is a plain HTTP request.
The examples use made-up values. Norra's own addresses are real.
Find the endpoints
The discovery document lists every endpoint, the grant types and the permissions a connection may hold:
curl https://app.norra-co.com/.well-known/oauth-authorization-serverThe issuer is https://app.norra-co.com. The endpoints are /oauth/register, /oauth/authorize, /oauth/token and /oauth/revoke on it.
1. Register the app
Registration is open and needs no account. Send your app's name and the addresses Norra may send the member back to:
curl https://app.norra-co.com/oauth/register \
--request POST \
--header "Content-Type: application/json" \
--data '{
"client_name": "Example Assistant",
"redirect_uris": ["https://assistant.example/callback"]
}'The answer is 201 with the client id. Keep it. The examples below read it from NORRA_CLIENT_ID. There is no secret: a registered app is a public client and proves itself with PKCE on each exchange.
{
"client_id": "4f6c2a8e-…",
"client_id_issued_at": 1790000000,
"client_name": "Example Assistant",
"redirect_uris": ["https://assistant.example/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}What registration takes:
client_nameis required, at most 100 characters, with no control or formatting characters. The consent page shows it to the member.redirect_urisholds one to tenhttpsaddresses, with no fragment and no user name or password in them. A host written in non-ASCII letters is refused. Register an internationalised name in its punycode form.grant_typesmay nameauthorization_codeandrefresh_token,response_typesmay namecode, andtoken_endpoint_auth_methodmay benone. Leave them out and you get those values.jwks_uri,jwks,request_uris,sector_identifier_uriandsoftware_statementare refused. Norra does not fetch anything a client points to, so it takes metadata only in the request itself.
Other fields are ignored. Registration is limited to three per address and then one every twenty seconds, ten per network, and 120 an hour for the whole deployment. Past a limit the answer is 429. Wait for the number of seconds in Retry-After.
An app that nobody has connected may be removed 30 days after it registered or was last used. Register again if client_id stops being accepted.
2. Send the member to authorize
Make a PKCE pair for each authorization. The verifier stays with your app. The challenge goes in the link.
CODE_VERIFIER=$(openssl rand -base64 48 | tr -d '\n=' | tr '+/' '-_')
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" \
| openssl dgst -sha256 -binary | openssl base64 | tr -d '\n=' | tr '+/' '-_')
STATE=$(openssl rand -hex 16)Then open this in the member's browser, with your own values:
https://app.norra-co.com/oauth/authorize?response_type=code&client_id=$NORRA_CLIENT_ID&redirect_uri=https%3A%2F%2Fassistant.example%2Fcallback&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256&state=$STATE&scope=company.read%20metrics.readThe parameters:
| Parameter | Required | What it is |
|---|---|---|
response_type | Yes | code. |
client_id | Yes | The id registration answered with. |
redirect_uri | Yes | One of the addresses you registered, exactly. Required even when you registered one. |
code_challenge | Yes | The S256 challenge: 43 characters of base64url without padding. |
code_challenge_method | Yes | S256. plain is refused. |
state | No | Your own value. It comes back unchanged. Use it to tie the answer to the request. |
scope | No | Permissions, separated by spaces. Leave it out and the page offers every permission an app may hold. |
companies | No | Organisation numbers you suggest, separated by spaces or commas, at most 100. The member still picks. |
resource | No | Another Norra server the token is for, such as the mcp server. May appear more than once. |
request and request_uri are refused. Send the parameters themselves.
The member signs in with BankID if they have not already, and then sees the consent page.
What the member sees
The consent page shows:
- Your app's name, and the host it will send the member back to.
- What the app may see. Each permission that only reads starts ticked.
- Changes the app may ask for. These start unticked, and give an app nothing today (see What a token can do).
- Which companies. This is one company, the companies the member ticks, or every company the member has access to. The last includes companies the member gets later. Companies you suggested start ticked. The member can name only companies they have access to.
- Another Norra server the access also opens, when you asked for one with
resource. The member cannot untick it. - The date the access ends.
The member can untick anything. The member then signs the consent with BankID. The app gets access when the member has signed, and not before. The connection holds what the member signed. That is at most what you asked for, and at most what the member can do.
If the member already gave your app access, the page shows what the app has and only what the new request adds. The member signs the addition with BankID. Adding to the access does not move its end date. If the request adds nothing, the member continues without a signature, and you get a new code for the same connection.
The answer at your redirect
When the member signs the consent, Norra sends the browser to your redirect with a code:
https://assistant.example/callback?code=nac_…&state=$STATE&iss=https%3A%2F%2Fapp.norra-co.comCheck that state is yours and that iss is https://app.norra-co.com. The code works once and for five minutes.
When the member denies, or the request is wrong, the redirect carries error and error_description instead of code, with state and iss as before:
error | Why |
|---|---|
access_denied | The member did not allow access. |
invalid_request | A parameter is missing, malformed or given twice, PKCE is missing or not S256, or companies is wrong. |
unsupported_response_type | response_type is not code. |
invalid_scope | scope names a permission an app may not hold, or one that does not exist. |
invalid_target | resource names a server this issuer does not issue tokens for. |
An unknown client_id, or a redirect_uri you did not register, is not sent to any redirect. The member sees an error on Norra's page instead, and your app hears nothing.
3. Trade the code for tokens
Send the code, the same redirect address, and the verifier. Name your app with client_id in the body, and send no secret:
curl https://app.norra-co.com/oauth/token \
--request POST \
--data grant_type=authorization_code \
--data-urlencode "client_id=$NORRA_CLIENT_ID" \
--data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=https://assistant.example/callback" \
--data-urlencode "code_verifier=$CODE_VERIFIER"The body is application/x-www-form-urlencoded, which curl --data sends. code, redirect_uri and code_verifier are required. If you asked for a resource at authorize, name the same one here.
{
"access_token": "nat_…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "nrt_…",
"scope": "company.read metrics.read"
}scope lists what the member allowed. It can be less than you asked for.
4. Call Norra
Send the access token in the Authorization header, not in a URL. A token acts for one member on many companies, so name the company with orgnr on each call:
curl "https://app.norra-co.com/v1/samples?orgnr=5590000000&source_id=7a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" \
--header "Authorization: Bearer $NORRA_TOKEN"Norra records each call it answers: the connection, the company, the path and the outcome.
5. Refresh
An access token lasts an hour. Before it runs out, trade the refresh token for a new pair:
curl https://app.norra-co.com/oauth/token \
--request POST \
--data grant_type=refresh_token \
--data-urlencode "client_id=$NORRA_CLIENT_ID" \
--data-urlencode "refresh_token=$NORRA_REFRESH_TOKEN"The answer has the same shape as the code exchange, with a new refresh token. Store it and throw the old one away: a refresh token works once.
Refreshing does not move the chain's end. The last refresh token stops when the connection ends.
6. Revoke
When the member signs out of your app, revoke the token you hold:
curl https://app.norra-co.com/oauth/revoke \
--request POST \
--data-urlencode "client_id=$NORRA_CLIENT_ID" \
--data-urlencode "token=$NORRA_REFRESH_TOKEN"Send either the access token or the refresh token. Norra revokes every token from the same code. The answer is 200 with no body, also when the token is unknown or already revoked. A token issued to another app is refused with 400 and unauthorized_client.
Revoking a token does not end the connection. The member ends it under Connections on their profile page in Norra, and every token stops on its next call.
Lifetimes
| What | How long |
|---|---|
| Authorization code | Five minutes, one use. |
| Access token | One hour, or less when the connection ends sooner. expires_in says which. |
| Refresh token | Until it is used once, or the connection ends. |
| Connection | 90 days from the member's consent. Then the member gives access again. |
| Unused registered app | May be removed 30 days after it registered or was last used. |
Norra looks up each token on every call. A revoked connection or token stops at once.
What a token can do
A token reads. It can read what the member allowed, on the companies the member chose. It can read at most what the member can read at the time of the call. When the member loses access to a company, the token loses it on the next call.
It cannot do everything a member can:
- It does not push data.
sources.ingestbelongs to service connections, and an app cannot ask for it. - It cannot change anything today. A call that changes something is refused with
403andpermission_denied. Later an app will be able to propose a change for the member to accept. - It is not a BankID sign-in. A call that needs one is refused.
- It cannot hold
ledger.read.nin,ledger.read.terms,self.write,auth.login,connections.manageor any staff permission.
Errors from the OAuth endpoints
Registration, token and revocation errors are JSON in the OAuth shape:
{
"error": "invalid_grant",
"error_description": "the code or refresh token is not valid"
}Branch on the status and on error. Log error_description and do not parse it.
| Status | error | What to do |
|---|---|---|
400 | invalid_client_metadata | Registration: fix the field the description names. |
400 | invalid_redirect_uri | Registration: a redirect address is not one Norra takes. |
400 | invalid_request | A parameter is missing, given twice, or the body is not a form. |
400 | invalid_grant | The code or refresh token is wrong, used, expired, or its connection ended. Send the member through authorize again. |
400 | invalid_target | The resource is not one the member's consent named. Send the member through authorize with it. |
400 | unsupported_grant_type | Use authorization_code or refresh_token. |
400 | unauthorized_client | Revocation: the token was issued to another app. |
401 | invalid_client | The client_id is missing or not an id, or a secret was sent. Send client_id in the body and no secret. |
429 | temporarily_unavailable | Too many requests from your address. Wait Retry-After seconds. |
503 | temporarily_unavailable | Norra issues no tokens right now. Try again later. |
500 | server_error | Try again later. |
Calls to Norra with the token answer in the shape the errors guide describes. A missing, revoked or expired token gets 401. A permission the token does not hold gets 403 with permission_denied, and a WWW-Authenticate header with error="insufficient_scope" names it. Ask the member for it through authorize: the consent page then shows only the addition.