# Your users' budgets

URL: https://bridge.wekoya.tech/docs/end-users
Updated: 2026-11-08

> If your app has its own users, give each one an AI budget instead of letting them all draw from one bucket. Your backend keeps one key and names the user on…

If your app has its own users, give each one an AI budget instead of letting them all draw from one bucket. Your backend keeps one key and names the user on each request. Bridge tracks what each person spends, refuses them once their budget is used up, and resets it each period. Nobody has to step in.

- Create a key under API keys with the End users permission. Only an owner or admin can. Keep it on your server.
- Create each user when they sign up (below), or turn on auto-create on the End users page.
- Send `X-Bridge-End-User: <their id>` with every chat request made for them.

```
import os, requests

BRIDGE = "https://wekoya-api.wekoya.tech/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['WEKOYA_API_KEY']}"}

def on_signup(user):
    # Safe to retry: the same external_id returns the same user, 200 not 201.
    r = requests.post(f"{BRIDGE}/end-users", headers=HEADERS, json={
        "external_id": user.id,      # your own id for them
        "allowance": 5000,           # ₦5,000 of AI per period
        "period": "monthly",
    }, timeout=10)
    r.raise_for_status()
```

```
from openai import OpenAI, APIStatusError

client = OpenAI(base_url="https://wekoya-api.wekoya.tech/v1",
                api_key=os.environ["WEKOYA_API_KEY"])

def ask(user_id, prompt):
    try:
        reply = client.chat.completions.create(
            model="claude-sonnet-5",
            max_tokens=800,
            messages=[{"role": "user", "content": prompt}],
            extra_headers={"X-Bridge-End-User": user_id},
        )
    except APIStatusError as e:
        if e.status_code == 402 and e.body["error"]["type"] == "end_user_budget_exhausted":
            return "You've used this month's AI credit. Top up to keep going."
        raise
    return reply.choices[0].message.content
```
The reply says what that user has left, so you can show it in your app without a second call. `cost` is what this request charged, in naira; `remaining_minor` is in kobo, as a string, and is `null` for a user with no limit.

```
{
  "choices": [{ "message": { "role": "assistant", "content": "Correct: x = 3, y = 2 ..." } }],
  "usage": { "prompt_tokens": 212, "completion_tokens": 318 },
  "wekoya": {
    "cost": 18.4,
    "currency": "NGN",
    "end_user": {
      "id": "e3a1c0de-...",
      "external_id": "user_8412",
      "remaining_minor": "481160",
      "period_end": "2026-11-01T09:12:00.000Z"
    }
  }
}
```
A reply is also shortened to fit what the user has left, so a user with ₦40 remaining gets a shorter answer rather than a refusal sized for a long one. Only when they cannot afford a useful reply is the request refused.

```
# When the user pays you. Idempotency-Key makes a retried call add the credit once:
# use your payment's own reference.
curl https://wekoya-api.wekoya.tech/v1/end-users/user_8412/top-up \
  -H "Authorization: Bearer $WEKOYA_API_KEY" \
  -H "Idempotency-Key: flw_tx_7781230" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 2000 }'

# How much have they used this period, by model and by day?
curl https://wekoya-api.wekoya.tech/v1/end-users/user_8412/usage \
  -H "Authorization: Bearer $WEKOYA_API_KEY"

# Change the budget, suspend, resume, start a new period, or delete.
curl -X PATCH https://wekoya-api.wekoya.tech/v1/end-users/user_8412 \
  -H "Authorization: Bearer $WEKOYA_API_KEY" -H "Content-Type: application/json" \
  -d '{ "allowance": 10000, "allowed_models": ["claude-*"] }'
curl -X POST   .../v1/end-users/user_8412/suspend
curl -X POST   .../v1/end-users/user_8412/resume
curl -X POST   .../v1/end-users/user_8412/reset
curl -X DELETE .../v1/end-users/user_8412
```
Field | What it does |
`external_id` | Required on create. Your id for the user: 1 to 128 printable characters, no spaces. Used in paths and the header. |
`allowance / allowance_minor` | Budget per period, in naira (5000) or in kobo (500000). Send one, not both. null means no limit, but usage is still tracked. |
`period` | "none" (one budget that never resets), "monthly", or "days" with period_days. Periods count from when the user was created. |
`rollover` | What survives a reset. "top_ups" (default): unused top-ups carry, the allowance does not. "none" or "all". |
`allowed_models` | Optional list: an exact id, a prefix such as "claude-*", or "vendor:anthropic". Applies on top of the key's own list. |
`rpm_limit` | Optional requests-per-minute cap for this user. |
`max_output_tokens` | Optional cap on the length of any reply for this user. |
`name, metadata` | Optional, for you. metadata is any JSON object and comes back in webhooks. |
`status` | "active" or "suspended". |

- A budget is a limit on your balance, not money set aside. A request needs room in the user's budget and money in your balance.
- Anything you leave out on create comes from your defaults on the End users page, so a signup handler can send only the id.
- `GET /v1/end-users` lists users 20 at a time (up to 100 with `limit`), with `next_cursor` for the next page and `search`, `status` and `exhausted=true` filters.
- Paths take your id or ours. Deleting a user keeps their past usage on your bill and frees the id, so the same person can sign up again fresh.
- `GET /v1/end-users/settings` and `PATCH` it to set defaults, auto-create, `require_end_user` (refuse any request that names nobody), and `bind_openai_user` (use the OpenAI `user` field as the id).
Never call Bridge from the mobile or browser app itself. The key would be visible to anyone who unpacks it, and it can spend your whole balance. Call from your server, which already knows who the user is.
