# Your own backend Use WAMP as the AI supplier for a product you already run — register an app principal, mint short-lived per-end-user tokens from your own server, and get usage attributed per person. Source: https://docs.vampikez.fun/ship/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 | 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 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/:kid ``` `orgId` 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 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 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. ```js 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 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: ```bash curl -sS https://api.vampikez.fun/v1/models \ -H "Authorization: Bearer $END_USER_TOKEN" ``` Then make a Responses request: ```bash 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 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](/reference/ai-proxy/#continue-after-a-tool-call) and [stream consumer](/reference/ai-proxy/#stream-with-standard-sse). 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 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 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. :::caution `@wamp/app-sdk` is **not published to npm**. `npm install @wamp/app-sdk` fails as of the [availability check](/reference/packages/). Until it lands, integrate against the HTTP contract on this page — the only part the SDK does that is not a plain `fetch` is minting the JWT, which is the ten lines shown above. Nothing in the flow requires the package. ::: ```js // The shape once it is installable: 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 The end-user token from step 3 also authenticates the App document store at `/a//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](/identity/apps-and-credentials/) for the wider identity model. ## 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](https://docs.cloud.vampikez.fun).