# Browser and mobile apps

URL: https://bridge.wekoya.tech/docs/guest-keys
Updated: 2026-11-08

> Your API key must stay on your server. To let a web page or a phone app call Bridge directly (for chat, voice or pictures without a round trip through your…

Your API key must stay on your server. To let a web page or a phone app call Bridge directly (for chat, voice or pictures without a round trip through your server), give it a guest key: a key your server makes for one user, that lasts minutes, can only do what you allow, and stops at the amount you set.

- Your app asks your server Your signed-in user's app asks your own backend for a key.
- Your server asks Bridge With your real key, it calls POST /v1/guest-keys.
- Bridge returns a guest key Short-lived, limited to what you allowed, capped in naira.
- The app calls Bridge Directly, with the guest key, like any API key.

#### Make a guest key
On your server, with your API key:

```
curl https://wekoya-api.wekoya.tech/v1/guest-keys \
  -H "Authorization: Bearer $WEKOYA_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "expires_in": 900,
    "max_spend": 50,
    "uses": ["chat", "speech"],
    "end_user": "user_123",
    "origins": ["https://app.example.com"]
  }'
```

```
{
  "object": "guest_key",
  "id": "8f1c2d7e-…",
  "key": "wk_guest_…",
  "expires_at": "2026-10-10T12:15:00.000Z",
  "max_spend": 50,
  "currency": "NGN",
  "uses": ["chat", "speech"],
  "end_user": "user_123",
  "origins": ["https://app.example.com"]
}
```
The guest key is shown once. Give it to the app; don't store it. When it runs out or expires, the app asks your server for a new one.

#### Use it in the app
A route on your server hands keys to your signed-in users:

```
// On your server: a route your signed-in users call to get a guest key.
app.post("/api/bridge-key", requireSignedIn, async (req, res) => {
  const response = await fetch("https://wekoya-api.wekoya.tech/v1/guest-keys", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.WEKOYA_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ expires_in: 900, max_spend: 50, uses: ["chat"], end_user: req.user.id }),
  })
  const guest = await response.json()
  if (!response.ok) return res.status(502).json({ error: guest.error?.message ?? "Could not get a key" })
  res.json({ key: guest.key, expires_at: guest.expires_at })
})
```
The app then uses the guest key exactly like an API key:

```
// In the browser: use the guest key like an API key.
import OpenAI from "openai"

const { key } = await fetch("/api/bridge-key", { method: "POST" }).then((r) => r.json())

const client = new OpenAI({
  baseURL: "https://wekoya-api.wekoya.tech/v1",
  apiKey: key,
  dangerouslyAllowBrowser: true, // safe here: the key is short-lived and limited
})

const reply = await client.chat.completions.create({
  model: "gemini-3.8-flash",
  messages: [{ role: "user", content: "Suggest a name for my bakery" }],
})
```
For live audio, a browser WebSocket cannot send a key, so the app gets a one-time ticket first (allow `live` in `uses`):

```
// Live audio from a browser. A WebSocket cannot send headers, so get a one-time ticket first.
const ticket = await fetch("https://wekoya-api.wekoya.tech/v1/audio/live/tickets", {
  method: "POST",
  headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
  body: JSON.stringify({ model: "nova-3" }),
}).then((r) => r.json())

const socket = new WebSocket(ticket.url) // the ticket works once, within a minute
```

#### What you can set

- `max_spend` (required): the most it can spend, in your account's currency. Then it stops with `key_limit_exceeded`.
- `expires_in`: seconds it lasts, from 60 to 86,400 (a day). Default 900 (15 minutes).
- `uses`: what it may call: `chat`, `speech`, `transcription`, `live`, `realtime`, `images`, `embeddings`, `decisions`, `music`, `search`, `videos`. Default `chat`. Everything else (keys, billing, batches, pipelines) is refused.
- `models`: which models, written like a [key's model list](https://bridge.wekoya.tech/docs/restrict-models). Never wider than the key that makes it.
- `end_user`: spend from [one of your end users'](https://bridge.wekoya.tech/docs/end-users) budgets. The app cannot choose a different one.
- `origins`: only from these websites, such as `https://app.example.com`. Leave it out for phone apps, which send no origin.
- `max_requests`: stop after this many requests. `label`: a name you will recognise.

#### Stopping one early
When a user signs out, revoke their key. Revoking the API key that made guest keys stops all of them, and stops any live audio they started.

```
curl -X POST https://wekoya-api.wekoya.tech/v1/guest-keys/$GUEST_KEY_ID/revoke \
  -H "Authorization: Bearer $WEKOYA_API_KEY"
```
Make guest keys with a key that has no spending limit of its own (each guest key has its own cap), and keep that key on your server. `GET /v1/guest-keys` lists the ones still working, with what each has spent.
