Connections and permissions
How a connection signs in, what it may do, how long it lasts, and how to replace one without a gap.
What a connection is
A service connection is how another system calls Norra for a company. It belongs to one company and is bound to one of its sources, so a call names neither.
A person with the connections.manage permission makes it on the company's Developer page. A new connection is a signed change. It waits in To sign until the person signs it with BankID. Norra makes the secret when the person signs, and To sign shows it once, in the sign result. Norra keeps only a hash of it.
The Developer page lists the company's connections: name, source, what each may do, expiry, and when and from which network it was last used. The list does not show the secret. Rotating and revoking a connection need connections.manage, like making one. A rotation is a signed change in To sign, like a new connection. A revocation needs a BankID sign-in and no signature, and it has effect at once.
Client credentials first
A connection signs in one of two ways. Pick Client id and secret unless the sender cannot ask for a token. It is the choice the Developer page starts on.
With client credentials, the connection has a client id and a secret that starts ncs_. Your system trades them for an access token at the token endpoint, and sends only the token with each call. The token lasts an hour. The secret goes to the token endpoint and nowhere else.
curl https://app.norra-co.com/oauth/token \
--request POST \
--user "$NORRA_CLIENT_ID:$NORRA_CLIENT_SECRET" \
--data grant_type=client_credentialsThe answer carries the token and how many seconds it lasts:
{
"access_token": "nat_…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "sources.ingest"
}Send the token in the Authorization header:
curl https://app.norra-co.com/v1/samples \
--header "Authorization: Bearer $NORRA_TOKEN"Keep the token and use it until about a minute before expires_in runs out, then ask for a new one. If a call gets 401 while the token should still work, ask for a new token once and send the call again. A second 401 means the connection itself is refused: stop, and check it on the Developer page.
The id and the secret go in HTTP Basic, form-encoded as the OAuth standard says. A secret in the request body is refused.
Static keys for devices
A device or a script that cannot ask for a token uses a Static key instead. The key starts nsk_ and is sent as the bearer itself, on every call:
curl https://app.norra-co.com/v1/samples \
--header "Authorization: Bearer $NORRA_KEY"A static key travels with every request for as long as it lives, so it is the second choice. Everything else on this page applies to both kinds.
Apps in a browser
An app that acts for a person, such as an MCP client, registers itself at /oauth/register. It then signs the person in with the authorization code flow and PKCE. It is a public client: it has no secret, and its PKCE verifier proves the code is its own. This works from a page in a browser too. Registration, the token and revocation endpoints, the discovery document at /.well-known/oauth-authorization-server and the MCP server answer any origin, and none of them uses cookies. Keep the tokens in memory, not in storage other scripts on the page can read.
What it may do
You choose what a connection may do when you make it. Each choice is a set of permissions.
| Choice | Permissions | Push deliveries | Read samples |
|---|---|---|---|
| Read | metrics.read | No | Yes |
| Write | sources.ingest | Yes | No |
| Read and write | metrics.read, sources.ingest | Yes | Yes |
Each operation in the reference names the permission it needs. A connection without it gets 403 with the code permission_denied, and a WWW-Authenticate header with error="insufficient_scope" that names the permission.
The permissions are fixed when the connection is made. For different ones, make a new connection and revoke the old one.
Give each system the least it needs. A billing system that sends numbers gets Write. A dashboard that reads them gets Read.
Sandbox connections
A sandbox connection is stored and parsed like any other, and what it sends is marked test, so it reaches no report. The receipt and every sample it produced carry "sandbox": true. Use one while you build the integration, then make a live one for production.
The API reference can send requests. From there, only a sandbox connection or the demo connection works. A production connection is refused with 403 and the code docs_origin_needs_sandbox.
The demo connection
The reference fills in a demo connection for the demo deployment. It reads fixture samples, cannot push, and is shared by every reader, so it has a tighter rate limit than your own. The samples come from a month of daily deliveries to a made-up source. They are parsed the way your own deliveries are, so each sample names the delivery it came from.
Expiry
A connection expires a year after it is made, unless you choose fewer days when you make it. The Developer page shows each connection's expiry and when it was last used. A request with an expired connection gets 401, and so does a request for a token.
Rotation
Rotate a connection before it expires, or as soon as you think its secret may have leaked. Rotation makes a new connection with a new secret, and a new client id for client credentials, and everything else the same.
- On the Developer page, choose how many days the old credential keeps working, up to 30. That is the overlap.
- Select Rotate on the connection. The rotation waits in To sign.
- Sign the rotation with BankID. The overlap starts when you sign.
- Copy the new secret from the sign result. To sign shows it once.
- Copy the new client id from the Developer page, if the connection has one.
- Deploy them. Both connections work during the overlap.
- Check that the old connection's last use stops moving. After the overlap it stops working.