Your own backend
You already have a product, a server, and users. You do not want a desktop shell or a marketplace listing; you want metered model access with your own users attributed individually. That is what an app principal is: a server-side identity keyed by an Ed25519 keypair you generate and hold, whose signature your backend uses to mint short-lived tokens for individual end users. Those users’ clients can then talk to the AI proxy directly. You may instead keep inference on your backend when your application needs server-side processing.
The shape is deliberately the same as a GitHub App: you hold a private key, a customer organization installs your app and grants it capabilities, and every call is attributed to the installation.
The pieces
Section titled “The pieces”| Thing | Who holds it | What it is |
|---|---|---|
| App | you | a slug registered under an organization, with a display name |
| Signing key | you only | an Ed25519 keypair. WAMP stores the public half and a kid derived from it; the private half never leaves your infrastructure |
| Installation | a customer organization | the grant of capabilities to your app in that organization. Identified by a UUID your backend keeps |
| End-user token | minted per request-burst by your backend | a short-lived JWT signed by your key, naming one of your users |
1 · Register the app and its key
Section titled “1 · Register the app and its key”Use Account Center → Apps in the browser: pick an organization, choose a slug and a name, then generate a keypair and register the public half.
POST /api/apps { orgId, slug, name }POST /api/apps/:slug/keys { publicKeyPem } → { kid, ... }DELETE /api/apps/:slug/keys/:kidorgId is a required UUID on create, and a required query parameter when
listing your apps — an app is always scoped to one organization. publicKeyPem
must be an SPKI PEM (-----BEGIN PUBLIC KEY-----). The returned kid is the
RFC 7638 JWK thumbprint of the key, which means you can compute it yourself and
do not have to store what the server told you.
Generate the keypair in a trusted environment and register only its public
half. Account Center can generate it locally in the browser. crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']) is the whole of it.
2 · Get an installation
Section titled “2 · Get an installation”Your app is registered, but registration alone authorizes nothing. An
organization has to install it, and the installation has to carry the AI
capability — a valid signature on a token is not sufficient, because the
proxy independently checks that the installation holds wamp.ai.invoke on the
wamp-ai resource. An organization admin performs that grant.
For your own organization, install the app from the same Apps surface where you registered it. For a customer’s organization, the flow avoids asking them for a UUID: your backend opens an installation intent naming the capabilities you want, the customer’s admin reviews and authorizes it against their organization, and you poll for the result.
POST /api/apps/installation-intents { assertion, resourceAudience, capabilityIds }POST /api/apps/installation-intents/:id/status { assertion }GET /api/apps/installation-intents/:id (the admin's review screen)POST /api/apps/installation-intents/:id/authorize { orgId } (the admin authorizes)The assertion is a short-lived JWT signed by your app key — your credential on
the intent routes, which take no human session. What you keep from the finished
intent is the installation id; every token you mint afterwards names it.
3 · Mint an end-user token
Section titled “3 · Mint an end-user token”This is the only cryptography you have to perform, and it is one JWT. Sign it with your app’s private key on every request-burst for a signed-in user of your product:
| Claim | Value |
|---|---|
header alg |
EdDSA |
header kid |
your key’s thumbprint |
iss |
your app slug — the app is the issuer, not the platform |
sub |
your own opaque id for the user. Matches ^(anon:)?[A-Za-z0-9._:-]{1,128}$, so hash it. Never PII, never a WAMP user id |
aud |
wamp-proxy |
installation_id |
the installation UUID from step 2 |
iat / exp |
issued-at and expiry |
Signature verification is strict: EdDSA only, the kid must resolve to a
non-revoked key of an app whose slug equals iss, the audience must be exactly
wamp-proxy, a suspended app fails closed, and clock skew is tolerated to 30
seconds. Anything issued by the platform itself is rejected on this path — the
issuer being your slug is what keeps end-user tokens out of the human-session
code path.
The verifier requires integral iat and exp values with a positive lifetime
of at most one hour. The reference client defaults to that maximum and rejects
invalid ttlSeconds before signing. Treat the token as a bearer credential that
reaches a browser and pick the shortest TTL your refresh story can carry.
import { createHash, createPrivateKey } from 'node:crypto';import { SignJWT } from 'jose';
const key = createPrivateKey(process.env.WAMP_APP_KEY); // PKCS#8 PEM
// Call only after authenticating this user in your own application.export async function tokenFor(userId) { const now = Math.floor(Date.now() / 1000); const subject = createHash('sha256').update(String(userId)).digest('hex'); return new SignJWT({ installation_id: process.env.WAMP_INSTALLATION_ID }) .setProtectedHeader({ alg: 'EdDSA', kid: process.env.WAMP_APP_KID }) .setIssuer('your-app-slug') .setSubject(subject) .setAudience('wamp-proxy') .setIssuedAt(now) .setExpirationTime(now + 900) .sign(key);}4 · Call the proxy with the OpenAI-compatible HTTP API
Section titled “4 · Call the proxy with the OpenAI-compatible HTTP API”The canonical AI surface is the OpenAI-compatible HTTP API under /v1. Send
the end-user token as a bearer credential on every request. Use
POST /v1/responses for new integrations; POST /v1/chat/completions is
available for clients that require the Chat Completions contract.
First discover the models enabled on the deployment:
curl -sS https://api.vampikez.fun/v1/models \ -H "Authorization: Bearer $END_USER_TOKEN"Then make a Responses request:
curl -sS https://api.vampikez.fun/v1/responses \ -H "Authorization: Bearer $END_USER_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "model": "gpt-5.6-terra", "input": "Summarize this thread in three bullets.", "max_output_tokens": 1024 }'The response is an OpenAI response object. Read generated text from
output_text, usage from usage, and check status before treating it as
complete. stream: true returns named SSE events; a successful stream ends
with response.completed, while response.incomplete and response.failed
are terminal outcomes that your UI must distinguish.
If your client requires Chat Completions, send messages to
/v1/chat/completions and read choices[0].message.content. Its stream ends
with data: [DONE], unlike Responses streams.
Tool calls
Section titled “Tool calls”Your product executes its own tools. Validate each returned function name and
arguments, enforce your application’s authorization, and return a
function_call_output for every call. On the next Responses request, replay
the original input, the previous output items, and the tool results, keeping
the tool definitions if another call is allowed. WAMP rejects
previous_response_id and conversation; it does not retain your turn history.
Use the complete tool round-trip example and stream consumer. Those examples check HTTP and terminal response failures rather than returning an error body as if it were an answer.
The model catalog is deployment-specific. GET /v1/models returns available
ids, aliases, context and output limits, tool support, input modalities, and
pricing metadata. Cache that response and select only an advertised model.
What WAMP attributes to whom
Section titled “What WAMP attributes to whom”Admitted inference attempts write usage records, including provider failures.
Authentication and validation rejections are not model calls. For an app principal, a record carries your
appId, the appInstallationId, the organization, the end-user subject, the
model, token counts, cost, and latency — and no WAMP user, because there is not
one involved.
The end-user subject comes from inside your signature, so it cannot be
overridden: a payload user or safety_identifier that disagrees with a signed end-user token
is rejected as an attribution conflict rather than quietly preferred. (A backend
calling with an app-level credential rather than an end-user token does set
user or safety_identifier in the payload, since in that case the app is the only thing
asserting who its user was.)
You read it back per app, filtered by end user and time range:
GET /api/apps/:slug/usage?endUserId=&from=&to=&limit=GET /api/apps/:slug/usage/summary@wamp/app-sdk, and what to do until it ships
Section titled “@wamp/app-sdk, and what to do until it ships”The SDK wraps all of the above. Its real export surface — verified against the source, since one name in circulation is out of date:
| Export | What it is |
|---|---|
generateAppKeypair() |
returns { privateKeyPem, publicKeyPem, kid } |
WampApp |
the app principal. issueUserToken({ endUserId, installationId, ttlSeconds? }), installationToken(installationId, resourceAudience?, opts?), createInstallationIntent(resourceAudience, capabilityIds), getInstallationIntent(id), buildAssertion() |
WampCloud, WampCloudError |
the Cloud session client: sessions, turns, runs, artifacts, publications |
END_USER_TOKEN_AUDIENCE, WAMP_CLOUD_RESOURCE_AUDIENCE |
the two audience strings, wamp-proxy and wamp-cloud |
There is no appToken function; the backend-credential call is
WampApp.installationToken(), whose second argument is the resource audience you
want the token for.
// The shape once it is installable:import { createHash } from 'node:crypto';import { WampApp } from '@wamp/app-sdk';
const app = new WampApp({ slug: 'your-app-slug', privateKeyPem: process.env.WAMP_APP_KEY,});
const token = await app.issueUserToken({ endUserId: createHash('sha256').update('authenticated-user-id').digest('hex'), installationId: process.env.WAMP_INSTALLATION_ID,});The same token can address app documents
Section titled “The same token can address app documents”The end-user token from step 3 also authenticates the App document store at
/a/<slug>/data. mine binds records to that token’s endUserId; shared
binds them to the App. The server never accepts another user’s id from the
request, and revoking the App key stops both model and data access.
WAMP does not mint an anonymous product session or hold your private signing key. Your backend owns user identity and token issuance. See Apps and credentials for the wider identity model.
Beyond model calls
Section titled “Beyond model calls”An installation token issued for the wamp-cloud audience lets your backend
drive durable agent sessions on WAMP’s own infrastructure — a long-running task
started from your CRM, bot, or scheduler, rather than a streaming completion in a
user’s client. That product has its own documentation at
docs.cloud.vampikez.fun.