Reference
Lifetimes, codes and addresses
Every LIME value in tables: passport and request lifetimes, the public error codes with their statuses, the discovery URLs, key states, and limits. For AI agents: /docs/all.en.md. Version: 2026-10-11
Lifetimes#
A passport's audience sets its purpose, who starts and receives it, its lifetime, and the claims it carries:
| Audience | Purpose | Who starts it | Delivered to | Lifetime | Distinct claims |
|---|---|---|---|---|---|
mcp | Calling tools on an MCP server | The agent (mints the passport) | The MCP server | 300 seconds | header typ: at+jwt (not a claim); client_id mirrors sub and is required; never carries user_id, passport_version, or request_id |
lime-site-login | An agent signing in to your site | The site backend (creates the request); the agent approves | The site backend (SSE stream) | 60 seconds (default; LIME_PASSPORT_TTL_SECONDS, clamped to 30–120 s) | sub is the agent_id, user_id its owner; match request_id against your own |
lime-binding | Proving ownership when binding | The site backend (creates the request) | The site backend (code exchange) | 60 seconds | no request_id; only this passport carries binding_id |
mcp is the default audience, and it is also the only scope the discovery document advertises (scopes_supported: ["mcp"]).
Request and helper lifetimes:
| Item | Lifetime |
|---|---|
| Login request | 120 seconds |
| Binding request | 600 seconds |
One-time binding_code | 90 seconds |
| JWKS response cache | 60 seconds (Cache-Control: max-age=60) |
| Clock-skew tolerance | MCP verifier: one leeway, LIME_JWT_VERIFY_LEEWAY_SECONDS (default 120 s), applied to exp, iat and nbf; site SDK: 30 s on nbf only, exp has zero leeway; LIME Core internal verifier: strict exp and nbf (0), tolerates iat up to 300 s ahead |
Lifetimes are upper bounds. Once LIME stops minting, the passports already issued expire on their own, so there is no call to LIME and no revocation list on the verify path.
Login-request statuses and their transitions:
| Status | Meaning and transition |
|---|---|
PENDING | Created; moves to APPROVED or EXPIRED |
APPROVED | Approved; moves to DELIVERING |
DELIVERING | The passport is being delivered; moves to DELIVERED |
DELIVERED | Terminal; the site received the passport |
EXPIRED | Terminal; the request passed its deadline before approval |
CANCELLED | Reserved terminal state |
approve returns APPROVED or DELIVERED. The happy path is PENDING → APPROVED → DELIVERING → DELIVERED; the timeout path is PENDING → EXPIRED.
Passport claims#
Every passport is an RS256 JWT signed by one Core key set; the aud claim selects the claim set:
| Audience | Required claims |
|---|---|
mcp | iss aud sub iat exp jti domain client_id |
lime-site-login | iss aud sub user_id passport_version request_id jti iat exp |
lime-binding | iss aud sub user_id passport_version binding_id jti iat exp |
Only the mcp audience derives iss from the issuer field of the OAuth AS metadata document (GET /api/v1/modules/oauth/.well-known/oauth-authorization-server); lime-site-login and lime-binding use a fixed Core issuer constant https://lime.pics, not the metadata. A verifier reads issuer from that metadata and requires the mcp passport iss to match it exactly (PyJWT exact match); the site SDK expects iss to equal the origin of its base_url. The mcp issuer default is environment-driven by LIME_OAUTH_ISSUER and must be set in production. The MCP verifier does not enforce the typ header value: it enforces iss, aud, sub, and the domain pin, and it validates iat within the leeway (alongside exp and nbf), so jti is not independently validated; client_id is still required, and the issuer always sets it equal to sub (RFC 9068). typ is a JOSE header (at+jwt), never a claim.
passport_version is "5" and appears only on lime-site-login and lime-binding; an mcp passport never carries it. domain is an mcp-only claim whose value is the normalized lowercase hostname. Read sub for the agent_id and treat jti as the unique passport id. An mcp passport that carries user_id, passport_version, or request_id is rejected (those claims are forbidden on the mcp audience).
Proof of work#
Approving a sign-in is gated by a proof of work (PoW), so the approve call cannot be replayed at will. The public read returns five fields:
| Field | Meaning |
|---|---|
request_id | the login request id being read |
status | the login request status |
pow_challenge | a base64url token over 32 random bytes; typically 43 URL-safe characters (bound 1–256) |
pow_difficulty | 1–32; default 15 |
expires_at | ISO-8601 UTC deadline of the request |
Find a pow_nonce — a decimal string of 1–64 characters, ≥1 (the solver enumerates 1, 2, 3, …) — whose digest has the first pow_difficulty bits zero:
import hashlib
def solve(pow_challenge: str, difficulty: int) -> str:
prefix = b"lime:pow:v1\0" + pow_challenge.encode() + b"\0"
for n in range(1, 1 << 32):
nonce = str(n) # the nonce is sent verbatim, as this decimal text
digest = hashlib.sha256(prefix + nonce.encode()).digest()
if int.from_bytes(digest, "big") >> (256 - difficulty) == 0:
return nonce
raise RuntimeError("no nonce found")The exact preimage is SHA-256( b"lime:pow:v1\0" || pow_challenge_as_received(UTF-8) || 0x00 || pow_nonce(UTF-8) ) and the first pow_difficulty bits of the digest must be zero; hash the challenge string exactly as received, never base64-decode it, and read both pow_challenge and pow_difficulty from the public read (GET /api/v1/auth/requests/{request_id}), not the create response.
Send it back as {"pow_nonce": "<POW_NONCE>"} on the approve call; a wrong or missing nonce returns INVALID_POW or MISSING_POW_NONCE (both 400).
Error codes#
Every code the public API returns, with the HTTP status it arrives with. Branch on the code, not on the message text.
Rows are grouped by flow: site-login and binding codes are returned to the site backend, verification and OAuth codes to the agent or the MCP server, and request codes to any client. NETWORK_ERROR, INVALID_RESPONSE and HTTP_ERROR are produced on the client and never cross the wire. This is the exact public integrator catalogue (59 codes); CAPTCHA, owner-session, registration and credential-rotation codes are portal-facing and deliberately not part of it, so they never cross the public wire. On POST /oauth/token a missing or wrong X-Agent-Token returns RFC invalid_client (401) — never the envelope codes MISSING_AGENT_TOKEN or INVALID_AGENT_TOKEN, which belong to other LIME endpoints.
| Code | HTTP | When |
|---|---|---|
SITE_LOGIN_REQUEST_NOT_FOUND | 404 | No login request matches the id, or it was already cleaned up |
SITE_LOGIN_REQUEST_EXPIRED | 409 | The login request passed its expiry before approval |
SITE_LOGIN_CONFLICT | 409 | The request was already approved or is in a conflicting state |
SITE_LOGIN_EVENT_CHANNEL_UNAVAILABLE | 503 | The SSE event channel for this site is unavailable |
SITE_LOGIN_INVALID_REQUEST | 400 | The login request payload or state is malformed |
SITE_LOGIN_CAPABILITY_DENIED | 403 | The agent token lacks the site_login:approve capability |
SITE_LOGIN_INVALID_TRANSITION | 409 | The requested state change is not allowed from the current status |
SITE_LOGIN_IDEMPOTENCY_ERROR | 409 | Reserved: raised only when a create supplies an internal create_idempotency_key already bound to another request (the portal path). The public Idempotency-Key header is ignored on site-login routes, so this does not surface on public creates |
SITE_LOGIN_POW_UNAVAILABLE | 503 | Proof-of-Work state is missing or could not be read |
SITE_LOGIN_CRYPTO_UNAVAILABLE | 503 | The signing service that issues the passport is unavailable |
SITE_LOGIN_INTERNAL_ERROR | 500 | An unexpected error occurred while serving the login request |
SITE_TOKEN_MISSING | 401 | A site call with no X-Site-Token |
SITE_TOKEN_INVALID | 401 | The X-Site-Token is unknown or revoked |
SITE_TOKEN_AUTH_UNAVAILABLE | 503 | Site-token verification is temporarily unavailable |
SITE_PAUSED | 403 | The site integration is paused by its owner |
SITE_SUSPENDED | 403 | The site is suspended and cannot start or read login requests |
MISSING_POW_NONCE | 400 | The approve request omitted pow_nonce |
INVALID_POW | 400 | The submitted pow_nonce does not meet the challenge difficulty |
BINDING_REQUEST_NOT_FOUND | 404 | No binding request matches the binding_id |
BINDING_REQUEST_EXPIRED | 410 | The binding request passed its expiry before completion |
BINDING_ALREADY_COMPLETED | 409 | The binding was already completed or exchanged |
BINDING_INVALID_REQUEST | 400 | The binding request payload is malformed |
BINDING_INVALID_TRANSITION | 409 | The binding state change is not allowed from the current status |
BINDING_AGENT_NOT_OWNED | 403 | The agent does not belong to the signed-in owner |
BINDING_PASSPORT_UNAVAILABLE | 503 | The binding passport could not be issued |
BINDING_CODE_INVALID | 400 | The binding_code is unknown, already used, or expired |
BINDING_CODE_SITE_MISMATCH | 403 | The binding_code was issued for a different site |
BINDING_CODE_STORE_UNAVAILABLE | 503 | The one-time code store is unavailable |
BINDING_INTERNAL_ERROR | 500 | An unexpected error occurred while completing the binding |
AGENT_NOT_OWNED | 403 | The requested agent is not owned by the authenticated caller |
INVALID_AGENT_ID | 400 | The agent_id is not a valid identifier |
AGENT_PROFILE_NOT_FOUND | 404 | No agent profile exists for the agent_id |
AGENT_INACTIVE | 403 | The agent is inactive and cannot mint a passport |
AGENT_USER_SUSPENDED | 403 | The agent owner account is suspended |
MISSING_AGENT_TOKEN | 401 | An agent call with no X-Agent-Token header |
INVALID_AGENT_TOKEN | 401 | The X-Agent-Token is wrong or carried in Authorization: Bearer |
AUTH_UNAVAILABLE | 503 | Authentication could not be verified |
CORE_PROFILE_READ_FAILED | 500 | The Core profile lookup failed |
CORE_PROFILE_NOT_FOUND | 404 | No agent profile exists for the authenticated agent (Core profile lookup) |
USER_TRUST_UNAVAILABLE | 503 | The user-trust lookup for the authenticated agent failed |
CORE_INTERNAL_ERROR | 500 | The Core service returned an internal error |
JWKS_UNAVAILABLE | 503 | JWKS is unreachable and no cached key verifies the passport |
invalid_request | 400 | The OAuth token request is malformed or omits a required field (RFC 6749) |
invalid_client | 401 | The OAuth client credentials are invalid (RFC 6749); on POST /oauth/token a missing or wrong X-Agent-Token returns this |
invalid_grant | 400 | The OAuth grant is invalid, expired or already used (RFC 6749) |
unsupported_grant_type | 400 | The OAuth grant type is not supported (RFC 6749) |
unsupported_response_type | 400 / 501 | The requested OAuth response type is not supported. RFC 6749 authorization-code/MCP-compat path: 400 (raw {error,error_description}). The /oauth/authorize stub, while the MCP-compat shim is off, returns 501 with the same raw {error,error_description} body |
access_denied | 403 | A LIME capability error at the token endpoint: the agent lacks the capability required to issue this token |
OAUTH_CODE_STORE_UNAVAILABLE | 503 | The authorization code store is unavailable |
OAUTH_TOKEN_ISSUANCE_FAILED | 503 | Issuing the OAuth token failed |
OAUTH_JTI_REPLAY_STORE_UNAVAILABLE | 503 | The token replay store is unavailable |
OAUTH_CRYPTO_UNAVAILABLE | 503 | The signing service that issues OAuth tokens is unavailable |
RATE_LIMIT_UNAVAILABLE | 503 | The rate limiter could not decide |
REQUEST_VALIDATION_ERROR | 422 | The request body or parameters failed validation |
RATE_LIMIT_EXCEEDED | 429 | Too many requests from this client |
HTTP_500 | 500 | The server returned an unexpected 500 response |
HTTP_ERROR | — | The server returned an unexpected HTTP status (client-only) |
INVALID_RESPONSE | — | The response body could not be parsed (client-only) |
NETWORK_ERROR | — | The request failed before a response was received (client-only) |
Discovery#
The documents a client reads instead of hardcoding values:
| Document | URL | What to read from it |
|---|---|---|
| OAuth AS metadata | GET /api/v1/modules/oauth/.well-known/oauth-authorization-server | token_endpoint, jwks_uri, scopes_supported |
| Public JWKS | GET /api/v1/core/.well-known/jwks.json | The RSA keys, selected by kid |
| Public agent profile | GET /api/v1/agents/{agent_id} | Presentation fields after you verify a passport |
| Public OpenAPI document | https://lime.pics/.well-known/openapi/v1.json | Machine-readable request and response schemas for the public operations |
| OAuth protected-resource metadata (RFC 9728) | https://lime.pics/.well-known/oauth-protected-resource | resource, authorization_servers, scopes_supported, bearer_methods_supported |
Read token_endpoint and jwks_uri from the metadata rather than building URLs by hand. The AS metadata registration_endpoint, when advertised, currently returns 501 (not implemented), and /oauth/authorize returns 501 unsupported_response_type when the MCP-compatibility shim is off. These documents are raw top-level JSON, not the {"ok": true, "data": …} envelope of the application endpoints.
The published OpenAPI document declares info.version 1.0.0; the security-audit snapshot is 0.1.0 and is operator-facing. Only /api/v1 is served, and there is no deprecation or sunset mechanism — no other API version is published.
Keys#
The JWKS is one Core key set that verifies every audience — not per-service keys. It holds one active signing key plus, during a rotation, one optional rollover verification key that is simply published while it is configured. There is no active/rollover/retired state machine and no fixed rotation window: a verifier accepts the active key and the rollover key, and any other kid is rejected.
| Key | Meaning |
|---|---|
| Active signing key | Signs new passports |
| Rollover verification key (optional) | Published only during a rotation; a verifier accepts it alongside the active key |
Any other kid | Not published; rejected |
- Canonical URL:
https://lime.pics/api/v1/core/.well-known/jwks.json. - Serve it with
Cache-Control: public, max-age=60, must-revalidate. Each JWK has exactlykty=RSA,use=sig,alg=RS256, pluskid,n,ewithe=AQAB; the set publishes the active key plus optionally one rollover key — 1–2 keys — unique and sorted bykid(the validator applies an internal hard cap of 8 keys, not an expected count). On an unknownkidthe verifier refetches, but the forced refetch is rate-limited byLIME_JWKS_MIN_REFRESH_SECONDS(default60), so "immediately, ignoring the cache TTL" holds only outside that window; the site SDK does force-refetch. - RS256 only: reject any other
alg, never acceptnoneor a symmetric algorithm. - Clock skew: the MCP verifier uses one leeway,
LIME_JWT_VERIFY_LEEWAY_SECONDS(default120), applied toexp,iatandnbf; the site SDK applies its 30-second leeway tonbfonly —exphas zero leeway; the LIME Core internal verifier is strict onexpandnbf(no leeway) and toleratesiatup to 300 s ahead of now. - Read
issfrom theissuerfield of the OAuth AS metadata (GET /api/v1/modules/oauth/.well-known/oauth-authorization-server) and match it exactly — never hardcode it. expected_domainmatching normalizes both sides (lowercase; strip scheme/path; reject userinfo, IP literal, reserved host, and any port) and requires exact equality with the passportdomainclaim — not a suffix or subdomain match. A port is rejected, not stripped.domainnormalization: reject a non-string or empty value; strip a leadinghttps://orhttp://; cut at the first/; lowercase; reject@(userinfo) and bracket/IP literals (any:, IPv4 or IPv6); enforce a DNS-hostname shape. Reserved hosts are rejected:localhost,metadata.google.internal,metadata,kubernetes,kubernetes.default,kubernetes.default.svc, and any name ending in.localhost,.local,.internal,.intranet,.corp,.home, or.lan.
Limits#
Rate limits use a Redis sliding window; the default window is 60 s. A denied request does not consume quota, so only allowed requests fill the window. When Retry-After is present it is an integer number of seconds equal to the whole window: it is sent on the generic per-IP / per-user 429, on ACCOUNT_TEMPORARILY_LOCKED, ROTATION_COOLDOWN_ACTIVE and JOURNAL_EXPORT_RATE_LIMITED (portal-facing), and on 503 CAPTCHA_UNAVAILABLE (value 5); it is absent on the OAuth 429, the portal-visit 429, the data-rights 429 and 503 RATE_LIMIT_UNAVAILABLE. Do not treat it as universal — on any 429, back off exponentially with jitter and cap retries.
| Operation | Limit |
|---|---|
| Create a login request | 30/min per IP, 60/min per site |
| Read a login request's status | 120/min per IP, 240/min per site |
| Read a login request (public) | 100/min per IP |
| Read the SSE event stream | 30/min per IP, 60/min per site |
| Approve a sign-in | 100 per 60 s per IP |
| Create a binding request | 30/min per IP, 60/min per site |
| Read a binding request (public) | 100/min |
| Complete a binding | 30/min |
| Register an agent or a site | 10 per user |
| Public agent profile | 100/min per IP |
| Mint an MCP passport | 30/min per IP, 60/min per agent token |
Request and field bounds:
| Bound | Value | Scope |
|---|---|---|
| Request body size | 6 MB (client_max_body_size 6m) | edge |
| SSE event stream | no per-frame size cap | public |
pow_challenge | 1–256 characters (base64url token) | public |
pow_nonce | 1–64 characters (decimal, ≥1) | public |
redirect_uri | ≤2048 characters | public |
binding_code | ≤256 characters | public |
turnstile_token | ≤4096 characters | portal |
| Avatar upload | ≤5 MiB | portal |
| Account data export | ≤5 MB | portal |
Do not call LIME on every verify request: the signature is checked locally via JWKS. The mcp audience means "mint a short-lived JWT for an external MCP server", not "present Bearer to the LIME APIs" — those need the opaque Agent Token or Site Token.
Public SDK surface#
Official Python packages wrap the calls above, so you rarely call the HTTP API by hand.
| Package | Class |
|---|---|
lime-agents-sdk | LimeAgent |
lime-mcp-server-sdk | TokenVerifier |
lime-sites-sdk | LimeSite |
Constructors are keyword-only. Pass the secret explicitly (agent_token= / site_token=) or let the client read the documented environment variable when the argument is omitted:
| Class | Constructor | Environment |
|---|---|---|
LimeAgent | LimeAgent(*, agent_token=None, base_url=None, timeout=30.0, max_retries=3, pow_timeout=10.0, mcp_token_refresh_skew=30.0, mcp_read_timeout=300.0, serialize_mcp_per_url=True, http_client=None) | LIME_AGENT_TOKEN, LIME_API_BASE |
LimeSite | LimeSite(*, site_token=None, base_url=None, timeout=30.0, max_retries=3, sse_backoff_base=0.5, http_client=None) | LIME_SITE_TOKEN, LIME_API_BASE, LIME_JWKS_CACHE_TTL_SECONDS (default 300) |
TokenVerifier | TokenVerifier(*, expected_domain=None, base_url=None, audience=None, cache_ttl=None, leeway_seconds=None, min_refresh_seconds=None, allowed_algorithms=None, config=None, cache=None) | LIME_EXPECTED_DOMAIN, LIME_BASE_URL (origin only), LIME_OAUTH_AUDIENCE (default mcp), LIME_JWKS_CACHE_TTL_SECONDS (default 3600), LIME_JWT_VERIFY_LEEWAY_SECONDS (default 120), LIME_JWKS_MIN_REFRESH_SECONDS (default 60), LIME_VERIFY_USER_AGENT (default curl/8.5.0) |
The agent and site clients take the API root including `/api/v1` (LIME_API_BASE); the verifier takes the origin only (LIME_BASE_URL). TokenVerifier.expected_domain is required — a missing pin raises ValueError.
Method signatures:
| Class | Methods |
|---|---|
LimeAgent | get_mcp_access_token(target, *, force_refresh=False) -> McpAccessToken; login(request_id) -> ApprovalResult; list_tools(target) -> list[Tool]; call_tool(target, name, arguments=None) -> CallToolResult; get_profile() -> AgentProfile; mcp_session(target); list_resources(target); read_resource(target, uri); list_resource_templates(target); list_prompts(target); get_prompt(target, name, arguments=None); set_logging_level(target, level); send_ping(target); get_server_capabilities(target); aclose(); async context manager |
LimeSite | create_login_request() -> LoginRequestResult; create_binding_request(*, redirect_uri: str) -> BindingRequestResult; verify_passport(jwt, *, expected_request_id=None) -> PassportVerificationResult; verify_binding_passport(jwt) -> PassportVerificationResult; on_login(handler); aclose(); async context manager |
TokenVerifier | verify(token) -> TokenValidationResult; verify_async(token) -> TokenValidationResult; warmup(*, raise_on_failure=False) -> bool; refresh_cache() -> None; invalidate_cache() -> None; close(); sync context manager |
get_mcp_access_token takes a DNS target, not a domain; a non-DNS target raises ValueError. verify is synchronous and never raises on a bad token; verify_async runs it in a worker thread. warmup returns False and logs on failure unless raise_on_failure=True; refresh_cache force-refetches and is rate-limited by LIME_JWKS_MIN_REFRESH_SECONDS.
Returned values and failures:
| Call | Returns | Raises |
|---|---|---|
LimeAgent.get_mcp_access_token | McpAccessToken(access_token, token_type, expires_in, issued_at) | ValueError (bad target), AuthenticationError, RateLimitError, ApiError |
LimeAgent.login | ApprovalResult(request_id, site_id, status, expires_at, approved_agent_id=None) | PowTimeoutError, AuthenticationError, RateLimitError, ApiError |
LimeAgent.get_profile | AgentProfile(agent_id, owner_id, display_name, avatar_url, description) | AuthenticationError, RateLimitError, ApiError |
TokenVerifier.verify | TokenValidationResult(is_valid, claims, error) | never throws on a bad token — read is_valid and error |
LimeSite.verify_passport / verify_binding_passport | PassportVerificationResult(valid, claims) | InvalidPassportError on any signature/audience/expiry/request_id mismatch |
TokenValidationResult carries is_valid: bool, claims (dict | None) and error (str | None); .agent_id is claims["sub"] when valid, .domain is the domain claim, and .valid_claims returns the claims only when valid. Datetime wire fields (expires_at, created_at, approved_at, delivered_at) are ISO-8601 UTC with a +00:00 offset (no Z), and null when unset.
Exceptions carry .message, .code, .http_status and .detail (base LimeError(message, *, code, http_status, detail); ApiError(code, message, *, http_status, detail)). The agent hierarchy is LimeError → AuthenticationError (AgentInactiveError, AgentUserSuspendedError), PowTimeoutError, RateLimitError, ApiError (OAuthCapabilityError, AuthUnavailableError), McpAuthenticationError. The site hierarchy is LimeError → AuthenticationError, RequestExpiredError (defined; expiry is delivered as on_login(..., passport=None) instead), InvalidPassportError, RateLimitError, ApiError (SiteAuthUnavailableError). RateLimitError has no retry_after attribute, and the Retry-After header is not captured.
- Install:
pip install lime-agents-sdk lime-mcp-server-sdk lime-sites-sdk(see each package for the full reference and version floors). - The agent client caches the passport per domain, re-mints 30 s before expiry, and re-mints once on a
401from a service; the verifier and site client cache JWKS and refresh on an unknownkid(LIME_JWKS_CACHE_TTL_SECONDS:3600in the verifier,300in the site client). verify_passportraisesInvalidPassportErrorinstead of returningvalid=false— wrap it; therequest_idmismatch message isJWT request_id claim does not match expected request.- The site SDK accepts a login passport with a TTL up to 120 seconds (
_LOGIN_MAX_TTL_SECONDS); the default expiry is still 60 seconds.
Portal operations#
Creating an agent or a site and minting or rotating their Agent Token / Site Token are portal-only (the LIME portal browser UI) — no public API mints an Agent Token or a Site Token. The portal shows each secret exactly once; store it, then use it as X-Agent-Token / X-Site-Token. There is no public sandbox or staging host for the LIME API: https://lime.pics is the only public origin of the LIME API/issuer, and any staging tier is operator-only; the public demo MCP resource server at https://demo.lime.pics/mcp is a separate host, not the LIME API origin.