Authentication
After this page you can choose the right credential for your integration, produce
one that /v1 accepts, refresh it before it expires, and say precisely which
operations your installation is allowed to perform and why a 403 happened.
One header scheme, two credentials
Section titled “One header scheme, two credentials”Every /v1 call carries the same header:
Authorization: Bearer <credential>That is the only accepted scheme, and these properties hold whichever credential you put in it:
- Bearer only. A request to
/v1without anAuthorization: Bearerheader is401 bearer_credential_required, even if it carries a valid browser session cookie./v1is a service API, not a browser backend. - Bound to one installation, therefore one organization. Sending
X-Wamp-Organizationalongside a bearer is400 organization_header_not_allowed— the credential already names exactly one tenant, and letting a header override it would be a tenancy hole. - Not a user credential. Sessions it creates are attributed to
createdBy: { kind: "app_installation" }. It is not safe in a browser, a mobile app, or anywhere a customer can read it. - Authority is live, never a snapshot. Capabilities are re-resolved from the installation record on every request, so revoking the grant stops the credential immediately.
What differs is how you get one, and there are two ways:
| Installation API key | Installation token | |
|---|---|---|
| What it is | An opaque secret, wamp_cloud_live_…, shown exactly once |
A JWT you mint from your own Ed25519 key |
| Who creates it | An administrator of the organization, in WAMP Account | Your backend, per exchange |
| Setup you build | None — one environment variable | Keypair, key registration, assertion signing, token cache |
| Lifetime | Until revoked, or an optional expiry the administrator sets | 600 seconds, then you mint another |
| Organizations per credential | One | One per token; one key registration serves all of them |
| Reach for it when | Your backend serves one organization: an internal integration, a single customer, an on-premise deployment | Your product is installed by many organizations, or policy forbids a long-lived secret at rest |
Start with the API key. For the single-organization case it is the whole
answer, and building the assertion flow first is work you did not need. A
wamp_cloud_… key cannot authenticate as a user or call the AI proxy. A separate
wamp_ai_… key can call inference with the wamp.ai.invoke grant.
The installation API key
Section titled “The installation API key”The low-engineering path, and the one to use unless the table above sends you elsewhere. Everything happens once, in WAMP Account, and it is the administrator of the organization whose work you will run who does it — not you.
Send them these steps. They need the organization.integrations.manage
permission in that organization:
- Open WAMP Account and go to Installed apps.
- On the WAMP Cloud product, choose Integrations, then New integration.
- Name the integration after the backend that will hold the key, and select the capabilities that backend needs — the same ids as in What your installation may do. Select only what it uses; the set can be changed later without a new key.
- Copy the key. It is shown exactly once, in that dialog, and cannot be retrieved afterwards — only replaced.
That flow creates the installation for you: there is no app to register, no keypair, and no installation intent. What you receive is one string:
export WAMP_CLOUD_API_KEY='wamp_cloud_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'curl -sS "$WAMP_API/v1/capabilities" -H "Authorization: Bearer $WAMP_CLOUD_API_KEY"With the Node SDK it is the whole constructor:
import { WampCloud } from '@wamp/app-sdk';
function requiredEnv(name: string): string { const value = process.env[name]; if (!value) throw new Error(`Set ${name}`); return value;}
const cloud = new WampCloud({ baseUrl: requiredEnv('WAMP_API'), apiKey: requiredEnv('WAMP_CLOUD_API_KEY'),});What to know about the key itself:
- Treat the whole string as the secret, prefix included. The prefix names the
resource (
cloud) and the environment (live), and a key that does not match the deployment it is presented to fails as401 invalid_or_expired_credential. - Store it in a secret manager, load it at boot, never log it, never ship it to a client. Unlike a token, this one is worth stealing for as long as it lives.
- At most two keys are active at a time per integration, which is what makes rotation possible: the administrator creates the second, you deploy it, they revoke the first.
- Losing it is recoverable, retrieving it is not. The administrator replaces the key from the same screen; the replacement revokes the old one in the same step.
- There is no
expiresInto watch. Nothing on your side refreshes. A401means the key was revoked, expired, or belongs to another deployment — not that it aged out, so do not build a re-mint loop around it.
The rest of this page is the other credential. If the key is what you are using, skip to What your installation may do.
Use an installation key for AI inference
Section titled “Use an installation key for AI inference”An administrator can create an AI key in WAMP Account: Installed apps → WAMP AI
→ Integrations → New integration. Select wamp.ai.invoke and copy the
wamp_ai_live_… key shown once. Send it to Account’s AI gateway as a bearer;
WAMP_ACCOUNT is the Account origin, which may differ from the Cloud origin.
curl -sS "$WAMP_ACCOUNT/v1/models" \ -H "Authorization: Bearer $WAMP_AI_API_KEY"const account = process.env.WAMP_ACCOUNT;const key = process.env.WAMP_AI_API_KEY;if (!account || !key) throw new Error('Set WAMP_ACCOUNT and WAMP_AI_API_KEY');const response = await fetch(`${account}/v1/models`, { headers: { Authorization: `Bearer ${key}` },});if (!response.ok) throw new Error(`AI gateway returned ${response.status}`);const models = await response.json();This key is for inference only. It cannot call WAMP Cloud’s /v1 resource API
or exchange for /v1/runtime-credentials; revoking it stops the next inference
request. A wamp_cloud_… key cannot call the AI gateway.
The installation token, for a product many organizations install
Section titled “The installation token, for a product many organizations install”Reach for this when one backend serves many customer organizations, or when a long-lived secret at rest is not acceptable. You register one Ed25519 public key against your app, and from then on you can mint a credential for any organization that installs you, without asking anyone to create a key per tenant.
The chain
Section titled “The chain”Ed25519 keypair you generate it; the private half never leaves your process │ ▼ register the public keyyour App (a slug + one or more key ids) │ ▼ a human administrator approves capabilities for their organizationan Installation (a UUID you store per customer) │ ▼ sign a ≤600s assertion, POST it to /auth/app-installation-tokenan installation token (600s, audience wamp-cloud) │ ▼/v1/...Two origins are involved. The token exchange is served by WAMP Account; /v1
is served by WAMP Cloud. In a single-origin deployment they are the same host;
in a split deployment they are not. Keep them as two configuration values from the
start — your operator tells you both. Every call in the four steps below goes
to Account:
export WAMP_ACCOUNT=https://api.vampikez.fun-
Generate a keypair
Section titled “Generate a keypair”One Ed25519 keypair per app, generated once. WAMP only ever sees the public half, so there is no shared secret to leak and nothing to rotate by support ticket.
import { generateKeyPair, exportPKCS8, exportSPKI, exportJWK, calculateJwkThumbprint } from 'jose';const { publicKey, privateKey } = await generateKeyPair('EdDSA', { extractable: true });const privateKeyPem = await exportPKCS8(privateKey); // secret — your key managerconst publicKeyPem = await exportSPKI(publicKey); // register this with WAMPconst kid = await calculateJwkThumbprint(await exportJWK(publicKey));privateKeyPemis PKCS8 PEM. Put it in a secret manager and load it into memory at boot. It must never ship in a distributed client.publicKeyPemis SPKI PEM (-----BEGIN PUBLIC KEY-----). This is what you register.kidis the RFC 7638 JWK thumbprint of the public key — the identical value WAMP derives when you register it, and thekidyour signatures must carry.
Register the public key against your app slug as a signed-in publisher with
organization.applications.manage, through Account or its app-management API. An App assertion cannot register its own key. Rotation is additive: register a second public key, switch thekidyou sign with, then revoke the first. A revoked key stops verifying at the next exchange.The Node SDK ships this as
generateAppKeypair(); see SDK for whether the package is installable for you yet. The snippet above usesjosedirectly and does not depend on it. -
Sign an assertion
Section titled “Sign an assertion”The assertion is a JWT you sign with your private key. It proves your app’s identity to WAMP Account, and it is what authenticates every call in the two steps below.
Part Value Header algEdDSA— Ed25519. Nothing else is acceptedHeader kidYour registered key id (the JWK thumbprint) issYour app slug subYour app slug — the same value audAccount’s configured platform JWT issuer ( JWT_ISSUER), supplied by the operatoriatIssue time, in seconds expExpiry. exp - iatmust be ≤ 600 seconds, or the exchange failsassertion.mjs import { SignJWT, importPKCS8 } from 'jose';const key = await importPKCS8(process.env.WAMP_APP_PRIVATE_KEY, 'EdDSA');export async function assertion() {const now = Math.floor(Date.now() / 1000);return new SignJWT({}).setProtectedHeader({ alg: 'EdDSA', kid: process.env.WAMP_APP_KID }).setIssuer(process.env.WAMP_APP_SLUG).setSubject(process.env.WAMP_APP_SLUG).setAudience(process.env.WAMP_AUD).setIssuedAt(now).setExpirationTime(now + 300).sign(key);}// Running the file prints one assertion, which is what the shell tabs read.if (process.argv[1]?.endsWith('assertion.mjs')) process.stdout.write(await assertion());The payload is otherwise empty, and the file reads four values from the environment:
WAMP_APP_PRIVATE_KEY(the PKCS8 PEM from step 1),WAMP_APP_KID,WAMP_APP_SLUG, andWAMP_AUD— theaudvalue the aside below explains how to obtain. Signing a fresh assertion per exchange costs nothing, so the TypeScript tabs below callassertion()again for every request. To follow the shell tabs, export one:Terminal window export ASSERTION=$(node assertion.mjs)Re-run that when a command answers
401 invalid_assertion— an assertion past itsexpfails exactly like a bad signature. -
Get an installation
Section titled “Get an installation”An installation is one customer organization’s approval of your app for one exact resource server. Nothing exists — and no token can be minted — until a human approves it.
If the administrator installs your app themselves, they hand you the
installationIdand you are done. To drive the approval from your own product, create an installation intent, which is a signed request for a consent URL:Terminal window INTENT=$(curl -sS -X POST "$WAMP_ACCOUNT/api/apps/installation-intents" \-H 'Content-Type: application/json' \-d "$(jq -n --arg a "$ASSERTION" '{assertion: $a,resourceAudience: "wamp-cloud",capabilityIds: ["wamp.cloud.access","wamp.cloud.sessions:create","wamp.cloud.sessions:read","wamp.cloud.turns:submit"]}')")INTENT_ID=$(printf '%s' "$INTENT" | jq -r '.intent.id')import { assertion } from './assertion.mjs';const { intent } = await fetch(`${process.env.WAMP_ACCOUNT}/api/apps/installation-intents`, {method: 'POST',headers: { 'content-type': 'application/json' },body: JSON.stringify({assertion: await assertion(),resourceAudience: 'wamp-cloud',capabilityIds: ['wamp.cloud.access','wamp.cloud.sessions:create','wamp.cloud.sessions:read','wamp.cloud.turns:submit',],}),}).then((response) => response.json());{"success": true,"intent": {"id": "c1e4f9a2-5d3b-4a7e-8f10-2b6c9d4e7a15","status": "pending","expiresAt": "2026-08-11T09:15:00.000Z","authorizeUrl": "https://accounts.example.com/install/c1e4f9a2-5d3b-4a7e-8f10-2b6c9d4e7a15"}}Send the administrator to
authorizeUrl. They choose the organization and approve the exact capability list you asked for. The intent is valid for 15 minutes.Then poll for the result. Polling is a
POSTbecause it is authenticated by the same assertion, and reading an authorized result does not consume it, so a lost response is safe to retry:Terminal window INSTALLATION=$(curl -sS -X POST \"$WAMP_ACCOUNT/api/apps/installation-intents/$INTENT_ID/status" \-H 'Content-Type: application/json' \-d "$(jq -n --arg a "$ASSERTION" '{assertion: $a}')")# Empty until a human has approved; the exchange below needs it.export WAMP_INSTALLATION_ID=$(printf '%s' "$INSTALLATION" \| jq -r '.intent.installationId // empty')const status = await fetch(`${process.env.WAMP_ACCOUNT}/api/apps/installation-intents/${intent.id}/status`,{method: 'POST',headers: { 'content-type': 'application/json' },body: JSON.stringify({ assertion: await assertion() }),}).then((response) => response.json());{"success": true,"intent": {"id": "c1e4f9a2-5d3b-4a7e-8f10-2b6c9d4e7a15","status": "authorized","expiresAt": "2026-08-11T09:15:00.000Z","authorizeUrl": "https://accounts.example.com/install/c1e4f9a2-5d3b-4a7e-8f10-2b6c9d4e7a15","installationId": "8a0f1c6b-92d4-4e73-b5a1-0d3e7f2c9b48"}}statusispending,authorized, orexpired;installationIdappears only onauthorized. Store it against your customer — it is the tenancy key for everything that follows.Every id in
capabilityIdsmust be a capability that an app installation is allowed to hold. Asking for a human-only capability fails the whole intent withinstallation_not_authorized, so request from the list below, and request only what you use. -
Exchange it for a token
Section titled “Exchange it for a token”The assertion travels in a body field named
assertion— not in a header. The body is validated strictly: three required fields, one optional fourth, and nothing else.Terminal window curl -sS -X POST "$WAMP_ACCOUNT/auth/app-installation-token" \-H 'Content-Type: application/json' \-d "$(jq -n --arg a "$ASSERTION" --arg i "$WAMP_INSTALLATION_ID" \'{assertion: $a, installationId: $i, resourceAudience: "wamp-cloud"}')"const { token, expiresIn } = await fetch(`${process.env.WAMP_ACCOUNT}/auth/app-installation-token`,{method: 'POST',headers: { 'content-type': 'application/json' },body: JSON.stringify({assertion: await assertion(),installationId: process.env.WAMP_INSTALLATION_ID,resourceAudience: 'wamp-cloud',}),}).then((response) => response.json());{ "success": true, "token": "eyJhbGciOiJFZERTQSIsImtpZCI6…", "expiresIn": 600 }Field Required Meaning assertionyes The signed JWT from the previous step, 20–4096 characters installationIdyes UUID of the installation this token acts for resourceAudienceyes The resource server the token is for. For the Cloud API it is always wamp-cloudsetupAuthorityIdno UUID. The opaque setup authority an authorized installation intent handed you. Send it only when completing that handshake; Account binds the live authorizing membership into the token itself, so you cannot supply a membership id Failures on this endpoint use Account’s envelope,
{ "success": false, "error": "<code>" }, rather than the/v1error shape:Status errorCause 400invalid_requestA missing, extra, or malformed field. This body also carries an issuesarray naming what failed validation401invalid_assertionSignature, claims, aud, or expiry did not verify401unknown_app_or_keyNo app matches iss, or no live key matcheskid401assertion_too_long_livedexp - iatexceeds 600 seconds, oriat/expis missing403app_suspendedThe app is suspended 403installation_not_foundNot an active installation of this app 403installation_revokedThe installation existed and was revoked. Nothing to retry; the organization must install the app again 403resource_server_not_foundNo active resource server for that resourceAudience403installation_not_authorizedThe organization has not approved this app for that resource server 429rate limited Token issuance is rate limited per IP. Cache tokens; do not mint per request 500exchange_failedThe exchange itself failed unexpectedly. Retry with backoff
Refresh before expiry
Section titled “Refresh before expiry”The token lives 600 seconds and many runs take longer, so refreshing is part of normal operation rather than an error path. None of this applies to an installation API key, which has nothing to refresh.
- Cache per installation. Key the cache by
installationId. A multi-tenant backend holds one token per active customer, never one globally. - Refresh on a margin. Re-mint when less than a minute of life remains,
using the
expiresInfrom the response rather than a hardcoded number. - Re-mint once on
401. If a/v1call returns401 invalid_or_expired_credential, discard the cached token, mint a new one, and retry the request exactly once. Never loop. - Never re-mint on
403.403 insufficient_scopeand403 cloud_access_deniedare authorization outcomes; a fresh token has identical authority. Surface them to whoever can fix the grant. - Never persist a token. Not in a database, not in a log, not in a trace. It is cheaper to mint another one.
What your installation may do
Section titled “What your installation may do”Capabilities are the authorization model, and they are the same set whichever credential you hold: an administrator chooses them when creating an integration, or approves the list your installation intent asked for. Each one gates specific operations.
wamp.cloud.access is the gate on the credential itself: every /v1 request is
checked against it before routing, and a credential without it gets 403 cloud_access_denied on everything. The rest gate individual operations:
| Capability | What it gates |
|---|---|
wamp.cloud.access |
Using WAMP Cloud at all. Checked on every request |
wamp.cloud.sessions:create |
PUT and PATCH a session — and GET /v1/capabilities |
wamp.cloud.sessions:read |
Reading sessions, turns, runs, events, interactions, artifacts, publications, and the repository review |
wamp.cloud.sessions:archive |
DELETE (archive) a session |
wamp.cloud.turns:submit |
PUT a turn, which is how all work is submitted, and POST /v1/transcriptions |
wamp.cloud.runs:cancel |
Requesting cancellation of a run |
wamp.cloud.approvals:respond |
Answering a live approval with Session access; sensitive, grantable to humans and App installations |
wamp.cloud.repositories:read |
Discovering usable repositories (GET /v1/repositories) and the grants behind them |
wamp.cloud.publications:create |
Opening a pull request from a session’s reviewed tree. Sensitive |
wamp.cloud.publications:merge |
Merging a publication at its exact reviewed head. Sensitive |
wamp.cloud.runtime-releases:probe |
Activating or deactivating a bounded runtime-release probe for this installation’s new placements. Sensitive, APP_INSTALLATION only |
When a call needs one you do not hold, the response names it:
{ "error": "insufficient_scope", "requiredScope": "wamp.cloud.turns:submit" }The field is requiredScope, singular, and its value is a capability id — which
makes it the string to put in the message you show an administrator.
The contract declares fourteen capabilities in total. The ten above are the ones
an app installation can be granted. The other four —
wamp.cloud.sessions:manage (opening a session an installed app created, by
taking operator access on it),
wamp.cloud.accounts:manage (enrolling runtime accounts),
wamp.cloud.repositories:manage (connecting organization installations and
changing application grants), and
wamp.cloud.compute:manage (adding, draining and removing the machines the
organization runs its work on) — can only be held by a human signed in to WAMP
Cloud. They are administration surfaces, not part of an integration’s request,
and asking for one in an installation intent fails the intent.
wamp.cloud.publications:create, wamp.cloud.publications:merge, and
wamp.cloud.runtime-releases:probe are marked sensitive. The first two can write
to a customer’s repository; the third is a release-authority override, not a
session input. Expect them to receive more scrutiny during approval, and request
them only if you need them. Most integrations never ask for the probe.
Related
Section titled “Related”- Integrate WAMP Cloud — the exchange plus a complete run, end to end.
- Drive Cloud from a backend — a token cache with
refresh-on-
401, and what else belongs in your database. - Connect a repository — how an administrator creates a grant and how your installation discovers it.
- Authentication operations — the generated reference for the three exchange endpoints.
- Errors — every
/v1error code and whether it is retryable.