Authentication
How Half Off Dev API keys work: the four places a key can travel, a free way to test one, the 401 errors you may see, and how to keep keys safe.
Half Off Dev has a single kind of credential: an API key tied to your prepaid balance. The same key calls Claude, GPT and Gemini, and you send it in whatever header your SDK or CLI already uses, so there is no auth code to write.
Creating a key#
Keys are made on the API keys page. Each one is ho- plus 40 letters and digits, and it is displayed once. Half Off Dev keeps only a hash, which means a lost key is gone for good: revoke it and issue a new one.
Every sample in these docs expects the key in $HALFOFF_API_KEY:
export HALFOFF_API_KEY="ho-..."Spend caps, model allow-lists and expiry dates are covered in API keys.
Where the key can go#
You don't have to pick a header to suit Half Off Dev: each of the four forms below is accepted on every endpoint, so leave your client's default alone.
| Form | Looks like | Typical sender |
|---|---|---|
| Bearer token | Authorization: Bearer ho-... | OpenAI SDK, Codex CLI, Claude Code via ANTHROPIC_AUTH_TOKEN, OpenAI-compatible apps |
x-api-key header | x-api-key: ho-... | Anthropic SDK |
x-goog-api-key header | x-goog-api-key: ho-... | Google Gen AI SDK, Gemini CLI |
key query parameter | ?key=ho-... | Gemini REST samples |
When a request carries several, Half Off Dev reads them in this order and uses the first it finds: Authorization: Bearer, then x-api-key, then x-goog-api-key, then ?key=.
Headers beat query strings
A URL with a key in it gets copied into proxy logs and shell history. Reach for ?key= only when a client has no way to set a header.
Test a key for free#
Token counting checks the key and the model exactly as a real request would, but nothing is charged. A 200 with an input_tokens number means the key is live and allowed to call that model.
curl https://halfoff.dev/v1/messages/count_tokens \
-H "x-api-key: $HALFOFF_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-5.5",
"messages": [{"role": "user", "content": "Is this key working?"}]
}'Public endpoints#
The model catalog is open, so you can check ids and prices before you create an account. Any key sent to these endpoints is ignored.
| Endpoint | Returns |
|---|---|
GET /v1/models | All models with context size, prices and channel groups |
GET /v1beta/models | The same catalog in Gemini's format |
All model requests need a key, token counting included.
401 errors#
If the key is absent, unknown, revoked or expired, you get a 401, shaped like the errors of the API you were calling.
| Cause | Message |
|---|---|
| The request has no key | Missing API key |
| The key is unknown or revoked, or the account is suspended | Invalid or revoked API key |
| The key's expiry date has passed | This API key has expired |
{"type": "error", "error": {"type": "authentication_error", "message": "Invalid or revoked API key"}}A key that authenticates fine can still be turned away by its own limits: 403 when the model isn't on its allow-list, 402 once its spend cap is used up, 429 at its rate limit. API keys explains those limits and Errors lists every status.
Handling keys safely#
- Server side only. The API answers requests from any origin, so a key shipped inside a web page or mobile app can be lifted and spent by anyone. Call Half Off Dev from your backend.
- Never in the repo. Read keys from environment variables or a secrets manager, and list
.envfiles in.gitignore. - Separate keys per app and per environment. A leaked staging key can't reach production, and Logs tell you which key sent what.
- Give shared keys limits. Keys for CI jobs, scripts or teammates should carry a spend cap, a model allow-list and an expiry date. See API keys.
- Rotate on a schedule, and immediately when someone who had a key leaves. Security has more.