LIMEDocs

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:

AudiencePurposeWho starts itDelivered toLifetimeDistinct claims
mcpCalling tools on an MCP serverThe agent (mints the passport)The MCP server300 secondsheader typ: at+jwt (not a claim); client_id mirrors sub and is required; never carries user_id, passport_version, or request_id
lime-site-loginAn agent signing in to your siteThe site backend (creates the request); the agent approvesThe 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-bindingProving ownership when bindingThe site backend (creates the request)The site backend (code exchange)60 secondsno 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:

ItemLifetime
Login request120 seconds
Binding request600 seconds
One-time binding_code90 seconds
JWKS response cache60 seconds (Cache-Control: max-age=60)
Clock-skew toleranceMCP 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:

StatusMeaning and transition
PENDINGCreated; moves to APPROVED or EXPIRED
APPROVEDApproved; moves to DELIVERING
DELIVERINGThe passport is being delivered; moves to DELIVERED
DELIVEREDTerminal; the site received the passport
EXPIREDTerminal; the request passed its deadline before approval
CANCELLEDReserved 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:

AudienceRequired claims
mcpiss aud sub iat exp jti domain client_id
lime-site-loginiss aud sub user_id passport_version request_id jti iat exp
lime-bindingiss 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:

FieldMeaning
request_idthe login request id being read
statusthe login request status
pow_challengea base64url token over 32 random bytes; typically 43 URL-safe characters (bound 1–256)
pow_difficulty1–32; default 15
expires_atISO-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:

python
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.

CodeHTTPWhen
SITE_LOGIN_REQUEST_NOT_FOUND404No login request matches the id, or it was already cleaned up
SITE_LOGIN_REQUEST_EXPIRED409The login request passed its expiry before approval
SITE_LOGIN_CONFLICT409The request was already approved or is in a conflicting state
SITE_LOGIN_EVENT_CHANNEL_UNAVAILABLE503The SSE event channel for this site is unavailable
SITE_LOGIN_INVALID_REQUEST400The login request payload or state is malformed
SITE_LOGIN_CAPABILITY_DENIED403The agent token lacks the site_login:approve capability
SITE_LOGIN_INVALID_TRANSITION409The requested state change is not allowed from the current status
SITE_LOGIN_IDEMPOTENCY_ERROR409Reserved: 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_UNAVAILABLE503Proof-of-Work state is missing or could not be read
SITE_LOGIN_CRYPTO_UNAVAILABLE503The signing service that issues the passport is unavailable
SITE_LOGIN_INTERNAL_ERROR500An unexpected error occurred while serving the login request
SITE_TOKEN_MISSING401A site call with no X-Site-Token
SITE_TOKEN_INVALID401The X-Site-Token is unknown or revoked
SITE_TOKEN_AUTH_UNAVAILABLE503Site-token verification is temporarily unavailable
SITE_PAUSED403The site integration is paused by its owner
SITE_SUSPENDED403The site is suspended and cannot start or read login requests
MISSING_POW_NONCE400The approve request omitted pow_nonce
INVALID_POW400The submitted pow_nonce does not meet the challenge difficulty
BINDING_REQUEST_NOT_FOUND404No binding request matches the binding_id
BINDING_REQUEST_EXPIRED410The binding request passed its expiry before completion
BINDING_ALREADY_COMPLETED409The binding was already completed or exchanged
BINDING_INVALID_REQUEST400The binding request payload is malformed
BINDING_INVALID_TRANSITION409The binding state change is not allowed from the current status
BINDING_AGENT_NOT_OWNED403The agent does not belong to the signed-in owner
BINDING_PASSPORT_UNAVAILABLE503The binding passport could not be issued
BINDING_CODE_INVALID400The binding_code is unknown, already used, or expired
BINDING_CODE_SITE_MISMATCH403The binding_code was issued for a different site
BINDING_CODE_STORE_UNAVAILABLE503The one-time code store is unavailable
BINDING_INTERNAL_ERROR500An unexpected error occurred while completing the binding
AGENT_NOT_OWNED403The requested agent is not owned by the authenticated caller
INVALID_AGENT_ID400The agent_id is not a valid identifier
AGENT_PROFILE_NOT_FOUND404No agent profile exists for the agent_id
AGENT_INACTIVE403The agent is inactive and cannot mint a passport
AGENT_USER_SUSPENDED403The agent owner account is suspended
MISSING_AGENT_TOKEN401An agent call with no X-Agent-Token header
INVALID_AGENT_TOKEN401The X-Agent-Token is wrong or carried in Authorization: Bearer
AUTH_UNAVAILABLE503Authentication could not be verified
CORE_PROFILE_READ_FAILED500The Core profile lookup failed
CORE_PROFILE_NOT_FOUND404No agent profile exists for the authenticated agent (Core profile lookup)
USER_TRUST_UNAVAILABLE503The user-trust lookup for the authenticated agent failed
CORE_INTERNAL_ERROR500The Core service returned an internal error
JWKS_UNAVAILABLE503JWKS is unreachable and no cached key verifies the passport
invalid_request400The OAuth token request is malformed or omits a required field (RFC 6749)
invalid_client401The OAuth client credentials are invalid (RFC 6749); on POST /oauth/token a missing or wrong X-Agent-Token returns this
invalid_grant400The OAuth grant is invalid, expired or already used (RFC 6749)
unsupported_grant_type400The OAuth grant type is not supported (RFC 6749)
unsupported_response_type400 / 501The 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_denied403A LIME capability error at the token endpoint: the agent lacks the capability required to issue this token
OAUTH_CODE_STORE_UNAVAILABLE503The authorization code store is unavailable
OAUTH_TOKEN_ISSUANCE_FAILED503Issuing the OAuth token failed
OAUTH_JTI_REPLAY_STORE_UNAVAILABLE503The token replay store is unavailable
OAUTH_CRYPTO_UNAVAILABLE503The signing service that issues OAuth tokens is unavailable
RATE_LIMIT_UNAVAILABLE503The rate limiter could not decide
REQUEST_VALIDATION_ERROR422The request body or parameters failed validation
RATE_LIMIT_EXCEEDED429Too many requests from this client
HTTP_500500The 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:

DocumentURLWhat to read from it
OAuth AS metadataGET /api/v1/modules/oauth/.well-known/oauth-authorization-servertoken_endpoint, jwks_uri, scopes_supported
Public JWKSGET /api/v1/core/.well-known/jwks.jsonThe RSA keys, selected by kid
Public agent profileGET /api/v1/agents/{agent_id}Presentation fields after you verify a passport
Public OpenAPI documenthttps://lime.pics/.well-known/openapi/v1.jsonMachine-readable request and response schemas for the public operations
OAuth protected-resource metadata (RFC 9728)https://lime.pics/.well-known/oauth-protected-resourceresource, 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.

KeyMeaning
Active signing keySigns new passports
Rollover verification key (optional)Published only during a rotation; a verifier accepts it alongside the active key
Any other kidNot 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 exactly kty=RSA, use=sig, alg=RS256, plus kid, n, e with e=AQAB; the set publishes the active key plus optionally one rollover key — 1–2 keys — unique and sorted by kid (the validator applies an internal hard cap of 8 keys, not an expected count). On an unknown kid the verifier refetches, but the forced refetch is rate-limited by LIME_JWKS_MIN_REFRESH_SECONDS (default 60), so "immediately, ignoring the cache TTL" holds only outside that window; the site SDK does force-refetch.
  • RS256 only: reject any other alg, never accept none or a symmetric algorithm.
  • Clock skew: the MCP verifier uses one leeway, LIME_JWT_VERIFY_LEEWAY_SECONDS (default 120), applied to exp, iat and nbf; the site SDK applies its 30-second leeway to nbf only — exp has zero leeway; the LIME Core internal verifier is strict on exp and nbf (no leeway) and tolerates iat up to 300 s ahead of now.
  • Read iss from the issuer field of the OAuth AS metadata (GET /api/v1/modules/oauth/.well-known/oauth-authorization-server) and match it exactly — never hardcode it.
  • expected_domain matching normalizes both sides (lowercase; strip scheme/path; reject userinfo, IP literal, reserved host, and any port) and requires exact equality with the passport domain claim — not a suffix or subdomain match. A port is rejected, not stripped.
  • domain normalization: reject a non-string or empty value; strip a leading https:// or http://; 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.

OperationLimit
Create a login request30/min per IP, 60/min per site
Read a login request's status120/min per IP, 240/min per site
Read a login request (public)100/min per IP
Read the SSE event stream30/min per IP, 60/min per site
Approve a sign-in100 per 60 s per IP
Create a binding request30/min per IP, 60/min per site
Read a binding request (public)100/min
Complete a binding30/min
Register an agent or a site10 per user
Public agent profile100/min per IP
Mint an MCP passport30/min per IP, 60/min per agent token

Request and field bounds:

BoundValueScope
Request body size6 MB (client_max_body_size 6m)edge
SSE event streamno per-frame size cappublic
pow_challenge1–256 characters (base64url token)public
pow_nonce1–64 characters (decimal, ≥1)public
redirect_uri≤2048 characterspublic
binding_code≤256 characterspublic
turnstile_token≤4096 charactersportal
Avatar upload≤5 MiBportal
Account data export≤5 MBportal

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.

PackageClass
lime-agents-sdkLimeAgent
lime-mcp-server-sdkTokenVerifier
lime-sites-sdkLimeSite

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:

ClassConstructorEnvironment
LimeAgentLimeAgent(*, 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
LimeSiteLimeSite(*, 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)
TokenVerifierTokenVerifier(*, 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:

ClassMethods
LimeAgentget_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
LimeSitecreate_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
TokenVerifierverify(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:

CallReturnsRaises
LimeAgent.get_mcp_access_tokenMcpAccessToken(access_token, token_type, expires_in, issued_at)ValueError (bad target), AuthenticationError, RateLimitError, ApiError
LimeAgent.loginApprovalResult(request_id, site_id, status, expires_at, approved_agent_id=None)PowTimeoutError, AuthenticationError, RateLimitError, ApiError
LimeAgent.get_profileAgentProfile(agent_id, owner_id, display_name, avatar_url, description)AuthenticationError, RateLimitError, ApiError
TokenVerifier.verifyTokenValidationResult(is_valid, claims, error)never throws on a bad token — read is_valid and error
LimeSite.verify_passport / verify_binding_passportPassportVerificationResult(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 401 from a service; the verifier and site client cache JWKS and refresh on an unknown kid (LIME_JWKS_CACHE_TTL_SECONDS: 3600 in the verifier, 300 in the site client).
  • verify_passport raises InvalidPassportError instead of returning valid=false — wrap it; the request_id mismatch message is JWT 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.

Next steps#

Was this page helpful?

Lifetimes, codes and addresses · LIME