Documentation
Till lets you create scoped API keys for AI agents with activation limits plus conservatively reserved token and spend budgets. Give agents scoped credentials instead of distributing upstream keys directly.
New to Till? New accounts are manually onboarded during the controlled beta. Request beta access; existing customers can use the dashboard.
Quick Start
- Obtain beta access Request controlled-beta onboarding. Existing customers should authenticate with their tenant admin key in the dashboard. Save any issued admin key securely; it is a bearer credential.
-
Install the Python SDK
pip install till-ac- or use the REST API directly. - Create a scoped key Set limits and get a disposable key your agent can use as a drop-in replacement.
Python SDK
The Python SDK supports key administration and OpenAI-compatible proxy use. The dashboard and REST examples above are the canonical provider-connection workflow while automatic-routing helpers propagate to client releases.
The SDK provides:
- TillClient - create, list, check, and revoke scoped keys
- patched_openai() - drop-in wrapper that routes OpenAI-compatible requests through Till's proxy
- Key status checking - inspect remaining activations, tokens, and spend before using a key
Full SDK docs and source: pypi.org/project/till-ac
Scoped Keys
A scoped key is a disposable, provider-agnostic proxy key with enforced limits. The format:
When an agent makes a request with a scoped key, Till:
- Extracts the lookup key and finds the key record in the database
- Checks activation and previously recorded token/spend usage
- Selects a configured provider from the route, model, and account default
- Decrypts that provider connection in service memory (AES-256-GCM)
- Forwards the request to the selected upstream provider
- Tracks usage from the response and increments counters
Usage Controls
Every scoped key can have up to three controls. Their enforcement timing differs:
- Activation limit - each proxied HTTP request counts as one activation. 50 activations means 50 API calls, regardless of how many tokens each uses.
- Token budget - reserves a conservative request bound before dispatch, then settles to input + output tokens reported in the upstream response.
- Spend budget - reserves estimated spend before dispatch for 250+ priced model identifiers across 12 providers, then settles to response usage. Spend-limited requests with unknown model pricing are rejected.
You can set any combination. For any token- or spend-budgeted generation request, declare an output cap using max_tokens, max_completion_tokens, max_output_tokens, or Google generationConfig.maxOutputTokens. The conservative request bound must fit the remaining budget. When a request asks the upstream for several generations at once - via OpenAI n or best_of, or Google candidateCount (under generationConfig) - Till multiplies the reserved output bound by that count, because every generation honors the output cap and settles its usage against the budget; lower the generation count or the output cap if the multiplied bound no longer fits the remaining budget. When a Google Gemini request declares an explicit reasoning ceiling with generationConfig.thinkingConfig.thinkingBudget, Till folds that positive token count into the reserved output bound, because Gemini bills thinking tokens at the output rate and does not count them against maxOutputTokens; a dynamic budget (-1) names no finite ceiling and is not reserved. Background-mode requests (background: true) are rejected on token- or spend-budgeted keys because their usage is reported only on retrieval and never settles against the budget; use a key with only an activation limit for background generations.
Providers
Till supports 12 AI providers. All OpenAI-compatible providers use the same proxy path format:
- OpenAI - GPT-4o, o1, o3, GPT-4 Turbo, etc.
- Anthropic - Claude 4, Opus, Sonnet, Haiku
- Google - Gemini 2.5 Pro, Flash, etc.
- OpenRouter - access hundreds of models through one integration
- Mistral - Mistral Large, Medium, Small
- Groq - ultra-fast inference (Llama, Mixtral)
- Together AI - open-source model hosting
- Fireworks AI - fast inference at scale
- Perplexity - search-augmented models
- DeepSeek - DeepSeek-V3, Coder, Reasoner
- xAI - Grok models
- Cohere - Command R, Embed, Rerank
Credential Storage Model
Provider credentials are AES-256-GCM encrypted at rest with application-held key material and authenticated tenant/provider binding. The database stores:
- Encrypted provider credentials and the selected default provider
- A hash of the lookup portion (for finding the key record)
- Activation counter and limit
- Token and dollar usage counters
- Account, allowlist, and key metadata (provider, status, timestamps, and user-provided labels)
Provider credential plaintext and complete scoped tokens are never stored or returned after creation. Database contents alone are insufficient to decrypt provider credentials without the separate application encryption key. This is not a zero-knowledge system: Till decrypts the selected credential while proxying, and compromise of a scoped token, the running service, or both database and encryption key remains in the threat model.
Authentication
All admin API requests require your admin key in the Authorization header:
The proxy endpoint uses the scoped key itself as authorization - your agent just uses it like a normal API key.
API Endpoints
Base URL: https://api.till.ac
Connect a provider
Use GET /admin/providers to list configured connections and DELETE /admin/providers/:provider to remove one. Credentials and ciphertext are never returned.
Create a scoped key
Only max_activations is required after at least one provider connection exists. The key automatically uses current and future account connections. The optional expires_at field sets a time-based expiry (ISO 8601).
List keys
Filter by status (active, exhausted, revoked, expired), provider, or search (searches key IDs and metadata).
Check key status
Update a key
Update a key's limits, metadata, or IP allowlist without resetting counters. All fields are optional. New limits must not be below current usage. Set max_tokens, max_spend_cents, or expires_at to null to remove that ceiling. Raising or removing an exhausted limit, or extending a lapsed expiry, reactivates the key immediately with its counters intact; revoked keys stay revoked.
Clone a key
Creates a fresh automatic key with the same routing mode, limits, IP allowlist, and metadata as the source, but with reset counters. Automatic keys do not require a provider credential. Legacy fixed-provider keys still require upstream_key. If the source key's expiry has already passed, include a future expires_at (or null to remove the expiry) — the clone is rejected otherwise, since inheriting the lapsed expiry would return a key that immediately fails as expired. The new key's metadata includes cloned_from.
Key usage
Returns a structured usage breakdown with remaining and percent for activations, tokens, and spend.
Reactivate a key
Resets counters on an exhausted or expired key. The agent keeps its existing scoped key token. Optionally update limits in the body. If the key's expiry has already passed, include a future expires_at (or null to remove the expiry) — reactivation is rejected otherwise, since keeping the lapsed expiry would return a key that immediately fails as expired.
Revoke a key
Bulk revoke
Revoke up to 100 keys in a single call. Returns per-key status (revoked, not_found, or already_revoked). Useful for rotating upstream keys or decommissioning agent fleets.
Key stats
Returns aggregate counts (total, active, exhausted, revoked, expired), total activations/tokens/spend used, and a legacy/default-provider byProvider grouping. Automatic keys can route to multiple providers, so this grouping is not a per-request audit log.
Date range filtering
Filter key listings by creation date using created_after and created_before query parameters (ISO 8601). Combine with other filters for time-based auditing.
Key introspection (scoped key)
Read-only key introspection — returns valid (the top-level go/no-go boolean: true while the key can serve a request, false once it is revoked, expired, or exhausted, its tenant is inactive or out of monthly quota, or it has no configured provider to route to), key status, usage breakdown with percent per dimension, max_usage_percent, and a healthy boolean. healthy is narrower than valid: it is true only when the key is valid and below 80% on every limit dimension, so a key past 80% reads healthy: false while still being valid: true and fully usable. Branch a pre-flight go/no-go on valid, not healthy — treating healthy: false as a stop would abandon a working key with capacity still remaining. The response also carries bottleneck (which limit dimension will exhaust first), estimated_remaining_requests, exhaustion_eta, the configured limits (including ip when an IP allowlist is set) and ip_whitelist ranges, provider details, and quota info. It also returns warnings — a structured array of human-readable reasons the key is not fully healthy (why valid is false, such as "tenant status is …", "key has been revoked", or the "no provider connections are configured…" pointer, plus any limit dimension at or above 80% used), or null when none apply. This is the structured pre-flight reason the rejection notes below point at when they say GET /till/validate surfaces a stop ahead of time; read it to explain a valid: false result to the operator rather than parsing status_line. Does not consume an activation. Agents should call this for pre-flight checks before dispatching work.
Provider discovery
No auth required. Returns a list of all supported providers with id, name, base_url, auth_style, openai_compatible flag, and models_with_pricing (how many of that provider's models Till has a spend-tracking price for). SDKs and agents can dynamically discover which providers are available; a provider with models_with_pricing: 0 has no priced models yet, so a spend-limited key routed to it would be rejected on the first generation request (spend limits require a known model price) — pre-flight the exact model with GET /till/pricing?provider=<id>.
Pricing discovery
No auth required. Returns pricing data used for spend tracking. Without filters, lists all models with pricing. Filter by provider to see one provider's models. Add model to check pricing for a specific model — returns 404 if unknown. Each entry includes input_per_1k_cents and output_per_1k_cents. SDKs can verify spend tracking accuracy before sending requests.
Proxy Usage
To use a scoped key, keep the same request paths your SDK already uses and point the base URL to Till:
Common passthrough paths:
POST /v1/chat/completions(OpenAI-compatible)POST /v1/messages(Anthropic)POST /v1beta/models/{model}:generateContent(Google)
The proxy transparently forwards requests, tracks usage, and enforces limits. From the agent's perspective, only the base URL and API key change.
Choosing a provider
A provider-agnostic scoped key routes each request to one of your connected providers automatically. Native routes are detected from the path — /v1/messages goes to Anthropic, /v1beta/…:generateContent to Google, and Cohere's /v2/ and native /v1/ routes to Cohere. OpenAI-compatible /v1/ routes are resolved from the request's model, falling back to your default connection when the model alone does not identify a single provider.
When your account has more than one compatible connection and you want a specific one, send the x-till-provider request header with a provider id (case-insensitive):
Till validates the choice before it consumes an activation, so a routing rejection costs nothing. The request is rejected when the header names an unsupported id (list valid ids with GET /till/providers), a provider you have not connected (add it with PUT /admin/providers/:provider or from the dashboard), or a provider that cannot serve that route's request format. A legacy fixed-provider key ignores automatic routing and accepts an x-till-provider value only when it matches its embedded provider.
Response Headers
Every proxied response includes headers your agent can use to track its own key state:
x-till-key-id— the scoped key's unique ID (for admin API lookups)x-till-key-name— the key's name from metadata (if set)x-till-key-status— the key's lifecycle state:activewhile it is serving, orexhausted,expired, orrevokedonce it has stopped. This is the most direct signal that a scoped key has hit the end of its envelope — the same stateGET /till/validatereports. It answers "has this key stopped?" more directly thanx-till-healthy(which only reports whether the busiest limit is under 80% used) and is the first field ofx-till-status-line.x-till-provider— which provider actually served the request (e.g.openai). For an automatic key this is the connection Till selected from your configured providers; use thex-till-providerrequest header above to steer that choice. A legacy fixed-provider key always reports its embedded provider.x-till-routing-mode— whether that provider choice is steerable:automatic(Till selects a compatible connection per request and honors a compatiblex-till-provideroverride) orfixed(the key is locked to its embedded provider and rejects any override that names another). Read this before sending anx-till-providerrequest header, so an agent learns a key is locked from a successful response rather than by spending a request that gets rejected.x-till-remaining-activations— activations left before exhaustionx-till-limit-activations— the key's max activationsx-till-remaining-tokens— tokens left (if token limit set)x-till-remaining-spend-cents— cents left (if spend limit set)x-till-expires-at— ISO 8601 expiry time (if TTL set)x-till-expires-in-seconds— seconds remaining until that expiry (if TTL set), the machine-readable countdown to thex-till-expires-atwall. Pace against this directly to stop before the key expires, rather than parsing the timestamp and diffing it against your own clock; it mirrors theexpires_in_secondsfield ofGET /till/validateand reads0on the terminal403 expiredthat enforces the wall.x-till-usage-percent— highest usage % across all configured limits, including the account's monthly activation quotax-till-healthy—truewhile the busiest limit dimension is under 80% used; flips tofalseonce any dimension crosses the warning threshold, so a single header tells an agent whether the key is comfortably within its envelopex-till-bottleneck— which limit dimension will exhaust the key first (activations,tenant_quota,tokens,spend, orttl)x-till-warning— proactive alert when any limit exceeds 80% usage (e.g. "Approaching limit: activations 85% used")x-till-estimated-remaining— estimated requests left before the first limit (activations, monthly quota, tokens, or spend) is exhausted, projected from this key's observed usagex-till-exhaustion-eta— ISO 8601 estimate of when the key stops working: the earlier of projected capacity exhaustion and any expiry wall, so an agent can pace its remaining work and stop before the key doesx-till-status-line— a compact one-line summary of the key's current state, ready to log or surface without assembling the individual headers: the key status, activation usage, estimated requests remaining, andhealthy/warning(e.g.active · 42/100 activations (42%) · ~58 remaining · healthy). It mirrors thestatus_linefield ofGET /till/validate, so an agent can read the same stop/continue summary from ordinary proxied responses without a separate validation call.x-till-retriable— present only on a budget rejection (403 token_limit_exceeded/403 spend_limit_exceeded); mirrors the body'sretriableflag so an agent pacing on headers can decide without parsing the body.truemeans a corrected or smaller request on this same key may succeed;falsemeans the budget is spent. The terminal403 exhaustedstop carries no such header.x-till-pricing-known—trueif Till has pricing data for the model. A spend-limited request is rejected when pricing is unknown; without a spend limit, this header can befalse.x-till-cost-basis— pricing rates applied in cents/1K tokens (e.g.in=0.25;out=1.0). Only present when pricing is known. Verify cost attribution directly from response headers.x-till-request-tokens— the total tokens (input + output) this one request settled against the key, from the provider-reported usage. Wherex-till-remaining-tokensis the running balance, this is the delta that request subtracted from it, so an agent can attribute cost per call. Present on a completed non-streaming response that performed billable work; absent on a streaming response (headers are sent before the stream's usage is known) and on a retrieval/poll GET (which re-reads an already-settled generation and is never charged again).x-till-request-cost-millicents— this request's settled cost in millicents (thousandths of a cent — e.g.1500is 1.5 cents), computed fromx-till-request-tokensat thex-till-cost-basisrate. Present only when the model's pricing is known, under the same completed-non-streaming condition asx-till-request-tokens. This is the actual per-request figure thex-till-cost-basisrate and the runningx-till-remaining-spend-centsbalance are otherwise left to imply.x-till-request-budget-impact— how much of each configured limit this single request consumed, as a semicolon-joined percentage list (e.g.act=1.0%;tok=0.5%;spend=0.3%):actis always present (one activation as a share of the activation cap),tokappears when a token limit is set and the request settled tokens, andspendappears when a spend limit is set and the request settled cost. It tells an agent, in one header, how many more requests of this size the envelope has room for. Same completed-non-streaming condition as the two headers above.ratelimit-limit— your account's monthly activation quota, in the standard IETF RateLimit convention (same value as your plan's activations/month cap)ratelimit-remaining— monthly activations left on the account before the quota resets; falls toward 0 as the account is used and is0on the 429 that confirms the quota is exhausted, so an agent can pace on the standard convention from ordinary responses rather than waiting for the rejectionratelimit-reset— seconds until the monthly quota resets (the next UTC month boundary); on a 429 this matchesretry-afterretry-after— seconds until monthly quota reset (only on 429 tenant-limit responses)
Upstream Failure Responses
When Till has already dispatched your request to the selected provider but the attempt does not complete, it reports the failure mode as a distinct HTTP status and error code so an agent can choose whether to retry without parsing free text. Both responses carry a JSON error object with code, provider, key_id, and key_name (when set), plus a link header pointing back to this Proxy Usage section.
502 upstream_error— a transport-level failure reaching the provider (for example a connection reset, DNS or TLS failure, or an unreadable provider response). The provider or network is broken, not merely slow; an immediate blind retry is unlikely to help.504 upstream_timeout— the provider did not answer within Till's upstream timeout and the request was aborted. The provider is reachable but slow; back off and retry.
Because both failures happen after the request is dispatched upstream, the activation is counted, not refunded — Till refunds an activation only for a request it rejects locally before dispatch. Token and spend budgets are not charged when the provider reports no usage.
Local Rejection Responses
These are the pre-dispatch rejections the paragraph above refers to: Till stops the request before any provider call, so the activation is refunded and no token or spend budget is charged. Each carries a JSON error object with a machine-readable code (plus key_id and key_name when the key is known) and a link header pointing at the relevant docs section, so an agent can branch on the stop without parsing free text. A budget rejection (token_limit_exceeded / spend_limit_exceeded) also carries a retriable boolean, because the same code covers both a spent envelope and a merely oversized or malformed request. The six groups differ in what the caller should do next.
- Key stopped — terminal for this key.
403 exhausted(a depleting capacity dimension is fully spent — the activation, token, or spend limit, whichever the key hit first; the errormessageandusageobject name which one),403 revoked(key revoked), and403 expired(key TTL elapsed). This is the scoped key working as configured: it stopped at the operating envelope the operator set. Retrying the same key repeats the same rejection, so an agent should stop consuming and surface the stop. Anexhaustedorexpiredkey is recoverable in place: the operator reactivates it (POST /admin/keys/:id/reactivate) to reset usage — optionally raising the limit or extending the expiry — and the agent keeps its existing token, the same recovery each stop's runtimemessagenames. Arevokedkey cannot be reactivated. Any of the three can instead be replaced by cloning the key (POST /admin/keys/:id/clone) for a new token with reset counters and the same config. - Budget rejection — read
retriableto tell a spent envelope from a fixable request.403 token_limit_exceededand403 spend_limit_exceededare returned in two distinct situations that share a code. Whenretriableisfalsethe token or estimated-dollar budget is spent (nothing more fits) — treat it exactly like the terminal stops above. Whenretriableistruethe key still has budget but this one request was rejected for a fixable reason — a missing output cap, an unpriced model, a non-JSON body, a background-mode request, or a conservative bound larger than the remaining budget — so correct or shrink the request (the message names the lever) and retry on the same key rather than abandoning it. The same verdict is mirrored on thex-till-retriableresponse header, so an agent pacing on headers can branch without reading the body. - Account activation window — retry after it rolls over.
429 tenant_limit_exceededmeans the whole tenant's monthly activation cap is reached, not this one key's envelope. Requests resume when the monthly window rolls over or the plan is upgraded; the response also carries the standardRateLimitheaders so an agent can pace its retry. - Account suspended — the account itself, not the key or its envelope.
403 tenant_inactivemeans the whole tenant account is not active, so Till stops the request before dispatch (no activation, token, or spend is charged) even when the scoped key itself is valid and active. This differs from every other stop here in what recovers it. Unliketenant_limit_exceededabove — a monthly activation cap that clears when the window rolls over or the plan is upgraded — no waiting, retry, or pacing resolves an inactive account; and unlike the key stops it is not fixed by reactivating, cloning, or raising the limit on any key. The account owner must resolve whatever deactivated the account before any of its keys resume, so an agent should stop consuming and surface the stop to the operator rather than retry.GET /till/validatesurfaces it ahead of time —valid: falsewith a "tenant status is …" warning — so a pre-flight check can catch a suspended account before a request spends the round trip. - Request origin blocked — terminal for this network location, not the key.
403 ip_not_allowedmeans the caller's IP is outside the IP allowlist the operator scoped onto this key — an enforced envelope dimension like the activation, token, spend, and expiry limits above. The key itself is healthy and still serves requests from an approved address, so retrying from the same blocked IP repeats the rejection: send the request from an allowed address, or have the operator add the caller's IP to the key'sip_whitelist. The message names the blocked IP, and — as with every stop here — the response carrieskey_id/key_nameso the IP-locked key is identifiable without decoding the token. - No routable provider — the key's provider configuration, not its envelope.
409 provider_not_configuredis returned before dispatch (no activation, token, or spend is charged) and, unlike the stops above, the key's own limits are untouched — the request cannot be routed to a provider. Two cases share this code, told apart by thex-till-routing-moderesponse header. For anautomatickey, Till has no connected provider that can serve this request — either no providers are connected to the account, or none is compatible with the request's route andmodel; connect a compatible provider withPUT /admin/providers/:provider(for examplePUT /admin/providers/openai) or from the dashboard and existing tokens resume unchanged, or send amodelone of the connected providers serves. For afixedkey, anx-till-providerrequest header named a provider other than the one the key is locked to; drop the override (or match the key's embedded provider) and retry. When the cause is that no providers are connected at all,GET /till/validatesurfaces it ahead of time —configured_providers: []plus a "no provider connections" warning — so an agent can discover an unroutable automatic key on its pre-flight check rather than by spending a request that gets rejected.
Plans
These are controlled-beta platform prices for new subscriptions. Provider inference is billed separately under your own provider accounts. Eligible existing paid beta subscriptions retain their founding price under the published price-protection policy.
- Free - 3 scoped keys, 1,000 activations/month
- Pro ($59/mo) - 25 keys, 25,000 activations/month
- Scale ($249/mo) - 100 keys, 250,000 activations/month
All plans include all 12 providers; activation, token, estimated-spend, expiry, and IP controls; the dashboard; and the Python SDK. The plans differ by key and monthly activation capacity, not by control type. Subscriptions do not include provider usage, uptime guarantees, compliance certifications, SSO, or dedicated support.
FAQ
What counts as one activation?
One proxied HTTP request equals one activation. A 20-message conversation equals 20 activations (one per API call). A streaming response is still one activation.
Can I set only one type of limit?
Yes. Activation-only, spend-only, all three, or any pair are supported. Token- or spend-budgeted generation requests require a declared output-token cap so Till can reserve conservative capacity before dispatch. Unset dimensions have no per-key limit beyond the plan's monthly activation cap.
How accurate is dollar tracking?
Till's table contains 250+ provider/model identifiers. Spend-limited requests require a matching model price, reserve conservative capacity before dispatch, and settle to provider-reported usage. This remains an operational estimate, not a substitute for the provider invoice: cached tokens, batch discounts, provider price changes, and missing stream usage can differ. Keep provider-side budgets and invoice monitoring enabled.
What happens when a key is exhausted or expires?
Any request using an exhausted or expired key gets a clear error response with the reason (activation limit, token limit, dollar limit reached, or TTL expired). The agent can handle this gracefully. Use the clone endpoint to quickly create a replacement with the same config.
Can I use Till with frameworks like LangChain or LlamaIndex?
Many do. A framework must allow a custom base URL and API key. Till forwards provider paths and common streaming responses, but compatibility is not guaranteed for every SDK feature or provider endpoint. Test the exact combination before production use.