Registering an application in a tenant

Audience: a project about to deploy into a tenant on a shared HAP, and the tenant administrator who registers it for them.

You have built an application. It is about to be deployed at, say, https://your-app.example-hosting.com, and it needs to sign people in through a HAP tenant that already exists and that you do not own. What do you actually have to do?

This page answers that, end to end. It is the missing step between "my code is written" and "people can sign in" — and the one most projects discover by trial.

Two things that surprise almost everyone, so they are first.

  1. Deploying your code registers nothing. The deployment platform ships the application; the OAuth client is a separate act, in HAP, by somebody with authority in the tenant.
  2. You probably cannot do it yourself. Holding an account in the tenant — even being able to sign in to other applications there — grants you nothing here. See Who can register.

Who can register

Exactly two routes exist, and both need authority you are given rather than authority you have by being a user of the tenant.

RouteWhoWhere
Hosted Admin consoleA signed-in tenant administrator…/t/<slug>/hosted/admin/applications/new
Admin APIA holder of an admin key bound to that tenantPOST /v1/applications with X-Admin-Key and X-Tenant-Id

There is no third route. An ordinary account in the tenant cannot register an application, and neither can the application's own credentials (it has none yet).

If registration by the tenant administrator becomes a bottleneck for your project, the fix is application administrator — a per-application designation that lets you edit, disable, rotate and delete your own application without any tenant-wide power. It does not let you create a new one, so it relieves the second registration and every one after it, never the first. See Entitlement administration.

The scope that catches people out. POST /v1/applications is gated on identities:write, not applications:write. A key minted with only the applications scopes is refused at creation and then works perfectly for disable, enable, rotate and delete — which reads as a random failure rather than a missing scope. (GET /v1/applications has the matching quirk: it is gated on identities:read.)

What to supply

For a server-rendered or server-backed web application at https://your-app.example.com:

FieldValueNotes
nameyour project's short namePermanent. Not editable on any surface, ever. It is what audit rows and operator lists show.
client_idoptionalOmit it and one is generated. Supply your own to reuse an identifier you already have — see Choosing your own identifier. Permanent once set.
application_typewebOne of web, spa, native, machine_to_machine, saml_sp.
confidentialtrueThis, not application_type, is what mints a client secret. A browser-only SPA or a native app is public (false) and uses PKCE instead.
redirect_urishttps://your-app.example.com/<callback path>Matched as an exact string. See Redirect URIs.
scopesopenid profile emailAdd offline_access if your library asks for a refresh token. See Scopes.
home_urlhttps://your-app.example.com/Where someone is sent when they choose your application from their menu. Omit it and your application appears on the menu as a row that cannot be opened.
access_modeopenopen = anyone signed in to the tenant may obtain tokens. restricted = only people or groups granted access.
show_in_menuleave unsetAbsent means decide for me: an all-loopback redirect set (a CLI or machine client) is registered off the menu, everything else on it. Correct either way for a hosted web app.
label, description, icon_urloptionalWhat people read on the application menu. label falls back to name.

Minimal create call:

curl -X POST "$HAP_BASE/v1/applications" \
  -H "X-Admin-Key: $ADMIN_KEY" \
  -H "X-Tenant-Id: <tenant-slug-or-uuid>" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "your-app",
        "application_type": "web",
        "confidential": true,
        "redirect_uris": ["https://your-app.example.com/auth/callback"],
        "scopes": ["openid", "profile", "email"],
        "home_url": "https://your-app.example.com/"
      }'

Three fields you cannot set here

  • post_logout_redirect_uris is not on the create call. It exists only on PATCH /v1/applications/{id} and in the console's edit form. If you need RP-initiated logout, register first and patch immediately after.
  • scopes cannot be changed through the API afterwards. PATCH has no scopes field and will not gain one, for the same reason client_id is permanent. The console edit form can change them, so a mistake is recoverable — but plan to get them right at creation.
  • name and client_id are permanent on every surface. Neither the API nor the console will change them. A new name means a new registration.

What comes back

A client_id, an internal id (a uuid), and — for a confidential client — a client_secret.

The secret is shown once and is never retrievable. Only a hash is stored. If you lose it, nothing can recover it; you rotate and get a new one. Put it straight into your deployment's secret store.

Use the internal uuid as your durable handle for the application — in your own records, in scripts, in anything that has to find it again. It is the id every lifecycle call takes. A client_id is not a good key: it is unique only within one tenant among live registrations, and it becomes available again if the registration is deleted.

If the secret is lost, or leaked

Rotate it. See Managing your applications for the full procedure; the one decision is the overlap window:

  • Overlap 0 (the default) retires the old secret immediately and revokes that application's access tokens and refresh sessions. Your application is signed out until it is redeployed with the new value. This is what you want when a secret has leaked, or when it is lost and nothing is using it.
  • Any other value keeps both secrets valid for that many seconds — up to 2 hours — so you can deploy the new one with no interruption and no revocation. This is what you want when you are replacing a working secret on a schedule.

Redirect URIs

The redirect URI is matched as an exact string: scheme, host, port, path and query, all of it.

https://your-app.example.com/callback and https://your-app.example.com/callback/ are two different URIs. So are the http and https forms. A trailing slash is a refused sign-in that looks like a broken server.

The one exception is loopback. A URI on 127.0.0.1, [::1] or localhost may present any port at sign-in time, because a command-line tool cannot know which port it will get until it binds one. That exception exists for native and CLI clients and never applies to a hosted address.

Changing them later

Edit redirect_uris on the existing registration — through PATCH /v1/applications/{id} or the console's edit form. Do not delete and re-register; that retires the registration for good.

Editing them revokes nothing. No session, token or grant is touched. Only three things cascade a revocation: disable, delete, and rotate-with-no-overlap.

The one thing to get right is ordering: the new URI must be registered before your application starts sending it, or every sign-in fails at the authorize step. During a path or host change, register both, deploy, then remove the old one.

Scopes

The scopes your application requests at sign-in must be a subset of the scopes it is registered with. Ask for one that is not registered and the authorize request is refused with invalid_scope — a message that reads like a platform fault rather than a registration gap.

The practical rule: if your library mentions refresh tokens, register offline_access. That is how OpenID Connect spells "give me a refresh token", and it is the single most common cause of a client that authenticates in testing and fails the first time it tries to stay signed in.

Choosing your own identifier

You may supply your own client_id at registration rather than take a generated one. It must be 8–128 characters of letters, digits, hyphen, period, underscore or tilde, and may not begin hap_, which is reserved for identifiers the platform issues.

Two consequences worth understanding before you use this:

  • A client_id names a client within one tenant. Identifiers are unique per tenant, not globally. The same identifier may name a different application in another tenant, and in another installation.
  • Validate the issuer, not the audience alone. A token's audience is the client_id, which no longer distinguishes a tenant or an installation. The issuer — {hap-url}/t/{tenant} — carries host and tenant in one string, and is what you should check.

Deleting a registration frees its identifier for re-use. For the remaining lifetime of a token issued to the deleted registration, a locally-validating consumer cannot distinguish it from a token issued to a new registration holding the same name. HAP itself still refuses the old tokens; anything validating on its own cannot tell them apart.

The five things projects get wrong

In the order they are usually hit:

  1. Expecting a deploy to register the client. It does not. This page is the separate step.
  2. Reaching for a shared test client. A shared sandbox client is normally registered localhost-only, so it cannot work from a hosted address, and the failure is a redirect-URI refusal that reads as a platform problem.
  3. A redirect URI that differs by one character — a trailing slash, http for https, a port that is present in one place and absent in the other.
  4. Forgetting offline_access and then finding people are signed out sooner than expected.
  5. No home_url. Sign-in works perfectly from your application's own front door, and your application appears on everyone's menu as a row that cannot be opened. It is a one-field fix that nobody thinks to make, because nothing is broken from where they are looking.

After registration