Skip to content

Delegated access with OAuth 2.1

Your application runs at other customers’ sites: register, have users consent with PKCE, exchange the oat_ token, renew, revoke.

7 minute readAPIUpdated: 10/25/2026

On this page

Before you start

For
Anyone who builds software that runs on other people’s machines and cannot hide a secret: a command-line tool, a desktop application, an AI assistant.
Prerequisite
the base URL of your environment in SZENERA_API and a browser in which the consenting person is signed in
Where
command line and browser

By the end of this guide, your application lets a person decide instead of asking for a key: It registers itself, sends the person to give consent, exchanges the code with PKCE for an oat_… access token, and renews or revokes it. It never sees the person’s password.

When this approach is the right one

Situation Choice
Your own server talks to Szenera an API key ak_…, which belongs to the organization and is attached to no account
Your application runs at other customers’ sites and should see their inventory this approach: A person there consents, and your application gets a token
An AI assistant should work on your behalf this approach, usually through the MCP service

Delegated access always has two sides: the application that acts and the account on whose behalf it acts. At runtime, the intersection of the two applies. If the person loses a permission, your application loses it at the same moment.

The addresses

Your application has to hard-code exactly one address: the metadata document. Everything else is stated in it, as absolute URLs. Issuer and endpoints will in future be named by the metadata of auth.<your-domain>, if your environment has this host. So do not enter any endpoint by hand, read it from the document you fetch there. The addresses at the API base URL remain reachable until further notice.

Route Purpose
GET /.well-known/oauth-authorization-server metadata document of the authorization server (RFC 8414)
GET /.well-known/oauth-protected-resource metadata document of the protected resource (RFC 9728)
POST /oauth/register registration of a public client (RFC 7591)
GET /oauth/authorize validates the request and leads to the consent page
POST /oauth/token exchange a code or refresh token for a new token
POST /oauth/revoke revoke a token (RFC 7009)
GET /v1/me/connected-apps the connected applications of your own account
DELETE /v1/me/connected-apps/{clientId} take away from an application everything this account gave it

The first six addresses carry no /v1: The standards prescribe their paths literally, and a third-party client looks there and nowhere else. What lies behind the consent lives under /v1 as usual, see Base URL, /v1, and stability.

Steps

  1. Fetch the metadata document. It names the endpoints, the supported methods, and under scopes_supported every permission that delegated access can carry.

    curl -sS "$SZENERA_API/.well-known/oauth-authorization-server" | jq '{issuer, authorization_endpoint, token_endpoint}'
    
  2. Register your client, once, without authentication and without a secret. Only public clients exist: token_endpoint_auth_method is always none.

    curl -sS -X POST "$SZENERA_API/oauth/register" \
      -H "Content-Type: application/json" \
      -d '{ "client_name": "Warenwirtschaft-Brücke", "redirect_uris": ["http://127.0.0.1:8765/callback"] }'
    
    { "client_id": "oac_01KQRWJRC0OAUTHCLIENT00001", "token_endpoint_auth_method": "none", "redirect_uris": ["http://127.0.0.1:8765/callback"] }
    

    Registration is limited to 10 calls per hour and sender address. Store the identifier and do not register anew on every start.

  3. Generate a code verifier and its code challenge. The method is exclusively S256, and there is no plain.

    VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')
    CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -binary -sha256 | openssl base64 | tr '+/' '-_' | tr -d '=')
    
  4. Send the person to the browser. They land on the consent page of the portal and see the name of your application, the redirect target, and the requested permissions, bundled into areas with one sentence and one toggle each.

    open "$SZENERA_API/oauth/authorize?response_type=code&client_id=oac_01KQRWJRC0OAUTHCLIENT00001&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=scene.read%20playlist.read&state=$(openssl rand -hex 16)&code_challenge=$CHALLENGE&code_challenge_method=S256"
    

    When the person returns, check state against the value you sent before you redeem the code.

  5. Exchange the code. The body is form-encoded, not JSON.

    curl -sS -X POST "$SZENERA_API/oauth/token" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d grant_type=authorization_code \
      -d code=ocd_01KQRWJRC0OAUTHCODE0000001 \
      -d redirect_uri=http://127.0.0.1:8765/callback \
      -d client_id=oac_01KQRWJRC0OAUTHCLIENT00001 \
      -d code_verifier=$VERIFIER
    
    { "access_token": "oat_…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "ort_…", "scope": "playlist.read scene.read" }
    
  6. Work with it like with any other Bearer token:

    curl -sS "$SZENERA_API/v1/me" -H "Authorization: Bearer oat_…"
    
  7. Renew in time. The refresh token rotates: After the exchange, the old one is used up, and the response carries a new one.

    curl -sS -X POST "$SZENERA_API/oauth/token" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d grant_type=refresh_token -d refresh_token=ort_… -d client_id=oac_01KQRWJRC0OAUTHCLIENT00001
    
  8. Give back what you no longer need. Revocation always answers 200, even for an unknown value.

    curl -sS -X POST "$SZENERA_API/oauth/revoke" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d token=ort_… -d client_id=oac_01KQRWJRC0OAUTHCLIENT00001
    

Options

Lifetimes

Item Valid
Access token oat_… one hour
Refresh token ort_… thirty days, rotates on every exchange
Authorization code ocd_… two minutes, exactly one redemption
open consent request ten minutes

Permissions

There is no catalog of its own: A scope is a permission key, the same one that an API key may carry as well: scene.read, playlist.write, and so on. The complete list appears as scopes_supported in both metadata documents. GET /v1/permission-catalog also tells you, through the field appliesTo, which permissions are released for machines.

Two cuts apply at consent: A permission that machines never carry cannot be granted, and neither can a permission that the consenting person does not hold themselves. Both are listed with their reason in their area. Ask only for what you need: A request that is too broad makes the decision harder and is rejected instead of being silently trimmed.

The consent page does not show the permissions as a list but bundled by the category of the permission catalog: “Scenes”, “Playlists & schedules”, “Media”, “Displays & locations”, “Reports”. Each area carries one sentence about what your application can do in it, one toggle, and the individual permissions in an expandable list. All requested areas are switched on at the start. The same bundling appears later in the Connected applications card, collapsed into a tally (“5 areas, 18 permissions”).

Expect a subset. The person may switch individual areas off, and POST /v1/oauth/authorizations/{id}/approve accepts any subset of what was requested. The access token then carries only the granted permissions, and the token response names them under scope:

{ "access_token": "oat_…", "scope": "scene.read", "expires_in": 3600 }

Compare scope with what you asked for instead of assuming it. The platform answers what is missing with 403 at call time, not at the exchange. If you depend on an area, tell the person in plain language and send them through the round trip a second time. There is deliberately no way to extend an existing consent afterward.

Limits

GET /oauth/authorize, POST /oauth/token, and POST /oauth/revoke are limited to 60 calls per minute and sender address, the two metadata documents to 120, and registration to 10 per hour. The realtime channel stays closed to delegated access, and you fetch events through Webhooks.

The platform operator can block a client. Then no new round trip begins, and a running renewal fails. Existing access tokens run out instead of being revoked. Likewise, an organization can switch delegated access off entirely: The consent then fails with OAuthAuthorization.DelegationDisabled, at the point where the person decides rather than at the exchange.

Check the result

  • GET /v1/me with the token answers 200 and names the account and the organization on whose behalf you act.
  • The token response names under scope the permissions that were actually granted. They can be fewer than those requested.
  • The person sees your application in the portal under Settings › My account › Security in the Connected applications card, expandable by the same areas, and can revoke it there with one click.
  • After a revocation, the same call answers 401.

If something goes wrong

The endpoints of the standard answer in the format of the standard ({ "error": …, "error_description": … }) and not in the error format of this API. The routes under /v1 answer as usual.

Symptom Meaning Solution
400 invalid_grant on the exchange The code verifier does not match the challenge, or the code has expired. Start the round trip again. The verifier and the challenge must come from the same run.
400 invalid_grant, and all tokens are gone A code or a refresh token was redeemed a second time. The whole token family expires: Two holders of the same secret cannot be told apart. Discard the old token, have the person consent again, and never run two processes on the same token.
400 without a redirect redirect_uri is not in the list from the registration. This very address is what could not be verified. Enter the address at registration and send it word for word at the exchange.
410 OAuthAuthorization.Expired when opening the consent page The request is older than ten minutes. Explicitly not “not found”. Send the person there again.
422 naming permission keys A scope was confirmed that the person cannot hold or that is not intended for applications. Remove the named key from the request.
401 during operation The access token is older than one hour, or the person has revoked it. Renew once. If that fails too, have the person consent again.

The OAuthClient.* and OAuthAuthorization.* codes are listed in full, with meaning, trigger, and status, under Error codes: accounts and sessions. The two credential codes Auth.OAuthTokenRevoked and Auth.OAuthClientBlocked are listed under Error codes: possible on any route.