{"openapi":"3.1.0","info":{"title":"Control plane","version":"0.0.0","description":"Multi-tenant control plane: sign up, log in, and manage the API keys that authenticate gateway traffic. Every endpoint is callable by an agent; the only step a human must perform is reading a six-digit code out of their email."},"servers":[{"url":"https://megaloop.dev","description":"Production"}],"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"An API key minted at POST /v1/keys."},"bearerAuth":{"type":"http","scheme":"bearer","description":"A session token from POST /v1/auth/verify, or an API key. Routes that list this scheme alone require a real session: an API key belongs to a tenant rather than a person, and they refuse it with 401."}},"schemas":{"Error":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}},"paths":{"/health":{"get":{"operationId":"getHealth","summary":"Liveness","description":"Reports that the process is up. Performs no dependency checks.","tags":["meta"],"security":[],"responses":{"200":{"description":"The service is up","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"status":{"type":"string","const":"ok"},"version":{"type":"string"}},"required":["status","version"],"additionalProperties":false}}}}}}},"/ready":{"get":{"operationId":"getReady","summary":"Readiness","description":"Reports whether the service can reach its database. Returns 503 when it cannot. Carries no tenant data of any kind.","tags":["meta"],"security":[],"responses":{"200":{"description":"The service is ready","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"status":{"type":"string","const":"ok"},"version":{"type":"string"}},"required":["status","version"],"additionalProperties":false}}}},"503":{"description":"A dependency is unreachable","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"status":{"type":"string","const":"ok"},"version":{"type":"string"}},"required":["status","version"],"additionalProperties":false}}}}}}},"/v1/auth/code":{"post":{"operationId":"requestLoginCode","summary":"Request a login code","description":"Emails a six-digit code to the address. Signup and login are one flow: the account is created on first successful verification. The response is identical whether or not the address already has an account.\n\nRate limited per address. Once the budget for an address is spent the call answers 429 until the window rolls; a client that retries on a failed send will reach it, and so will a person who clicks 'send another code' repeatedly.","tags":["auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["email"]}}}},"security":[],"responses":{"202":{"description":"A code has been sent if the address can receive one","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"status":{"type":"string","const":"accepted"}},"required":["status"],"additionalProperties":false}}}},"400":{"description":"The body is not a valid request","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"429":{"description":"The per-address budget for login codes is spent; try later","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/auth/verify":{"post":{"operationId":"verifyLoginCode","summary":"Exchange a login code for a session","description":"Consumes the code and returns a session token. The token is shown once. Every failure — wrong, expired, already used, too many attempts, unknown address — returns the same 401.","tags":["auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"code":{"type":"string","pattern":"^\\d{6}$"}},"required":["email","code"]}}}},"security":[],"responses":{"200":{"description":"A session token","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"token":{"type":"string"},"expires_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"tenant_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"user_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},"required":["token","expires_at","tenant_id","user_id"],"additionalProperties":false}}}},"400":{"description":"The body is not a valid request","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"The code was not accepted","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/session":{"get":{"operationId":"getSession","summary":"Identify the caller","description":"Returns the tenant the presented credential belongs to. `user_id` is null when the credential is an API key, which belongs to a tenant rather than a person. `theme_preference` is the account's light/dark choice — the same value across every device the account signs in from — or null when it has none and the client should follow the browser.","tags":["session"],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"The caller's identity","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"tenant_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"user_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]},"theme_preference":{"anyOf":[{"type":"string","enum":["light","dark"]},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["tenant_id","user_id","theme_preference","email"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"operationId":"updateSession","summary":"Update the caller's account preferences","description":"Sets the account-level UI theme so a signed-in user's light/dark choice follows them across devices instead of living in one browser. The tenant is resolved from the presented credential, never from the body (D8). Send `theme_preference` as `\"light\"` or `\"dark\"` to pin a theme, or `null` to clear it and follow the browser's own preference. Returns the updated identity — the same shape as GET.","tags":["session"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"theme_preference":{"anyOf":[{"type":"string","enum":["light","dark"]},{"type":"null"}]}},"required":["theme_preference"]}}}},"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"The updated identity","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"tenant_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"user_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]},"theme_preference":{"anyOf":[{"type":"string","enum":["light","dark"]},{"type":"null"}]},"email":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["tenant_id","user_id","theme_preference","email"],"additionalProperties":false}}}},"400":{"description":"The request body was not valid","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"operationId":"revokeSession","summary":"Log out","description":"Revokes the session token used to make this call, immediately. Calling it with an API key is accepted and does nothing: keys are revoked through /v1/keys.","tags":["session"],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"204":{"description":"The session is revoked, or was already"},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Revocation could not be performed and did NOT happen — the session is still live. Retry until this returns 204.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/session/email":{"post":{"operationId":"requestEmailChange","summary":"Send a confirmation code to a new sign-in address","description":"Mails a six-digit code to the address you want to move to. Always answers 202, whether or not that address already has an account — confirm the code to find out. Nothing changes until POST /v1/session/email/confirm succeeds.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["email"]}}}},"security":[{"bearerAuth":[]}],"responses":{"202":{"description":"Code sent, if the address can receive one."},"400":{"description":"Malformed address.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No session was presented (an API key cannot move the address)","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"429":{"description":"Too many codes for that address.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/session/email/confirm":{"post":{"operationId":"confirmEmailChange","summary":"Confirm and move the sign-in address","description":"Consumes the code mailed to the new address and moves the account onto it. Answers 409 when the address already belongs to another account — by then the code has proved you can read its mail, so saying so reveals nothing new.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"code":{"type":"string","pattern":"^\\d{6}$"}},"required":["email","code"]}}}},"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The address was moved."},"401":{"description":"Wrong or expired code.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"That address already has an account.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/tenant/delete":{"post":{"operationId":"deleteTenant","summary":"Close this account and delete its data","description":"Irreversible and immediate. Cancels any live subscription, then deletes the account: every linked provider account and its stored credential, every API key, the request history, and all settings. `confirm_email` must match the address you sign in with — it is a confirmation, not an identifier, and the account acted on is always the one your credential resolves to. Security records that carry your address (the audit log, and codes already issued) are not removed; the privacy policy describes them.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"confirm_email":{"type":"string","minLength":1,"maxLength":320}},"required":["confirm_email"]}}}},"security":[{"bearerAuth":[]}],"responses":{"204":{"description":"The account and its data are gone."},"400":{"description":"The confirmation did not match the signed-in address.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No session was presented (an API key cannot close the account)","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"The subscription could not be cancelled; nothing was deleted.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/notifications/preferences":{"get":{"operationId":"getNotificationPreferences","summary":"Read the tenant's notification settings","description":"Which pool events send email, the utilisation at which one is considered worth sending, and the switch that stops all of them. `utilization_threshold` is a FRACTION in [0, 1] — the same unit a limit window reports — not a percentage. A tenant that has never changed anything has no stored row and reads back the defaults: every event on, threshold 0.8. Absent is deliberately not the same as off.","tags":["notifications"],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"The tenant's settings, or the defaults when it has none","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"enabled":{"type":"boolean"},"utilization_threshold":{"type":"number","minimum":0.25,"maximum":0.99},"on_utilization_threshold":{"type":"boolean"},"on_window_nearly_exhausted":{"type":"boolean"},"on_account_paused":{"type":"boolean"},"on_pool_exhausted":{"type":"boolean"},"on_overage":{"type":"boolean"}},"required":["enabled","utilization_threshold","on_utilization_threshold","on_window_nearly_exhausted","on_account_paused","on_pool_exhausted","on_overage"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"operationId":"updateNotificationPreferences","summary":"Change the tenant's notification settings","description":"Which pool events send email, the utilisation at which one is considered worth sending, and the switch that stops all of them. `utilization_threshold` is a FRACTION in [0, 1] — the same unit a limit window reports — not a percentage. A tenant that has never changed anything has no stored row and reads back the defaults: every event on, threshold 0.8. Absent is deliberately not the same as off. Every field is optional and is merged over what is stored, so a client switching one event off need not resend the rest. Returns the settings as they now stand.","tags":["notifications"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"enabled":{"type":"boolean"},"utilization_threshold":{"type":"number","minimum":0.25,"maximum":0.99},"on_utilization_threshold":{"type":"boolean"},"on_window_nearly_exhausted":{"type":"boolean"},"on_account_paused":{"type":"boolean"},"on_pool_exhausted":{"type":"boolean"},"on_overage":{"type":"boolean"}}}}}},"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"The settings as stored","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"enabled":{"type":"boolean"},"utilization_threshold":{"type":"number","minimum":0.25,"maximum":0.99},"on_utilization_threshold":{"type":"boolean"},"on_window_nearly_exhausted":{"type":"boolean"},"on_account_paused":{"type":"boolean"},"on_pool_exhausted":{"type":"boolean"},"on_overage":{"type":"boolean"}},"required":["enabled","utilization_threshold","on_utilization_threshold","on_window_nearly_exhausted","on_account_paused","on_pool_exhausted","on_overage"],"additionalProperties":false}}}},"400":{"description":"The request body was not valid","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/keys":{"post":{"operationId":"createApiKey","summary":"Mint an API key","description":"Creates a key for the calling tenant. The `key` field in this response is the only time the full value is ever available — it is stored as a SHA-256 hash and cannot be recovered. Store it now or mint another.","tags":["keys"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100}},"required":["name"]}}}},"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"201":{"description":"The key, shown once","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"name":{"type":"string"},"key":{"type":"string"},"key_prefix":{"type":"string"},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["id","name","key","key_prefix","created_at"],"additionalProperties":false}}}},"400":{"description":"The body is not a valid request","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"get":{"operationId":"listApiKeys","summary":"List the tenant's API keys","description":"Returns every key belonging to the calling tenant, revoked ones included. The key values are not returned — only the prefix, which identifies without using.","tags":["keys"],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"The tenant's keys","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"name":{"type":"string"},"key_prefix":{"type":"string"},"last_used_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"revoked_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updated_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["id","name","key_prefix","last_used_at","revoked_at","created_at","updated_at"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/keys/{id}":{"delete":{"operationId":"revokeApiKey","summary":"Revoke an API key","description":"Revocation is immediate for management calls. Inference can keep accepting the key for up to 60 seconds afterwards: the gateway caches an accepted key for that long and is a separate process, so treat 60s as the window when rotating a credential you believe is compromised. A key belonging to another tenant and a key that does not exist both return 404 with the same body, so this cannot be used to discover ids.","tags":["keys"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"204":{"description":"The key is revoked"},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"No such key for this tenant","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Revocation could not be performed and did NOT happen — the key still authenticates. Retry until this returns 204.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/usage":{"get":{"operationId":"getUsage","summary":"Per-account, per-day, per-model usage and its list-price cost","description":"A rollup of the request-event trail grouped by account, provider, model and day, over the half-open range `[from, to)`. `from` and `to` are ISO-8601 timestamps; omit them for the last seven days, and the response echoes the window it used. The null `account_id` bucket is traffic that reached no account (all accounts exhausted).\n\nEach bucket carries the request count, the error count (4xx/5xx responses plus drop-out verdicts), four disjoint token sums, and `cost` — what those tokens would have cost at the provider's published list price on the day they were served. That is a counterfactual, never an amount billed.\n\n**Read `metered_requests` before any token figure.** It counts the requests in the bucket that actually reported usage, and it is not the same as `requests`: rows written before usage capture existed carry no counts and never will. A token sum covers only the metered rows, so a bucket where the two differ has a real figure over part of its traffic.\n\n`cost` is null exactly when `cost_absence` says why — `no_tokens` (nothing measured), `unknown_model` (no published rate for this model), or `no_price_list` (this provider has no metered API to compare against). A null cost is never a zero cost, and no figure here is ever estimated.","tags":["usage"],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"The tenant's usage rollup","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"from":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"to":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"data":{"type":"array","items":{"type":"object","properties":{"account_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"provider":{"type":"string"},"model":{"type":"string"},"day":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"requests":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"errors":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"metered_requests":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"input_tokens":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"output_tokens":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"cache_read_tokens":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"cache_write_tokens":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"cost":{"anyOf":[{"type":"object","properties":{"currency":{"type":"string","const":"USD"},"input_usd":{"type":"number"},"output_usd":{"type":"number"},"cache_usd":{"type":"number"},"total_usd":{"type":"number"},"unpriced_tokens":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"basis":{"type":"object","properties":{"input_per_mtok":{"type":"number"},"output_per_mtok":{"type":"number"},"source":{"type":"string"},"as_of":{"type":"string"}},"required":["input_per_mtok","output_per_mtok","source","as_of"],"additionalProperties":false}},"required":["currency","input_usd","output_usd","cache_usd","total_usd","unpriced_tokens","basis"],"additionalProperties":false},{"type":"null"}]},"cost_absence":{"anyOf":[{"type":"string","enum":["no_tokens","unknown_model","no_price_list"]},{"type":"null"}]}},"required":["account_id","provider","model","day","requests","errors","metered_requests","input_tokens","output_tokens","cache_read_tokens","cache_write_tokens","cost","cost_absence"],"additionalProperties":false}}},"required":["from","to","data"],"additionalProperties":false}}}},"400":{"description":"A malformed `from` or `to`","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/requests":{"get":{"operationId":"listRequests","summary":"Recent request events for the calling tenant","description":"The tenant's most recent request events, newest first. Page with `limit` (default 50, capped at 200) and the cursor `before` + `before_id`, taken from the LAST row of the previous page. Pass both: events are ordered by `(created_at, id)`, and a cursor of only `before` skips every row sharing that timestamp — parallel requests land inside one millisecond routinely, so a walk that omits `before_id` loses rows without leaving a visible gap. Each event carries the account, provider, model, path, status code, rotation verdict (`response_class`) and latency. This is the feed that shows an account dropping out in real time: a `pause_account` or `try_another_account` verdict is that moment.\n\nToken counts are four disjoint figures — each token is counted once, so a cost is their plain sum against four rates. A null is 'the upstream did not report it', never zero, and stays null: rows written before usage capture existed have nulls forever. `cost` is what these tokens would have cost at list price on the day they were served, or null with `cost_absence` giving the reason. `resolved_model` is what the upstream said it actually ran when that differs from the model asked for; the cost is computed from whichever of the two is authoritative.","tags":["usage"],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"The tenant's recent request events","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"account_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"provider":{"type":"string"},"model":{"type":"string"},"resolved_model":{"anyOf":[{"type":"string"},{"type":"null"}]},"path":{"type":"string"},"status_code":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"response_class":{"type":"string"},"latency_ms":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"input_tokens":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"output_tokens":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"cache_read_tokens":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"cache_write_tokens":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"cost":{"anyOf":[{"type":"object","properties":{"currency":{"type":"string","const":"USD"},"input_usd":{"type":"number"},"output_usd":{"type":"number"},"cache_usd":{"type":"number"},"total_usd":{"type":"number"},"unpriced_tokens":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"basis":{"type":"object","properties":{"input_per_mtok":{"type":"number"},"output_per_mtok":{"type":"number"},"source":{"type":"string"},"as_of":{"type":"string"}},"required":["input_per_mtok","output_per_mtok","source","as_of"],"additionalProperties":false}},"required":["currency","input_usd","output_usd","cache_usd","total_usd","unpriced_tokens","basis"],"additionalProperties":false},{"type":"null"}]},"cost_absence":{"anyOf":[{"type":"string","enum":["no_tokens","unknown_model","no_price_list"]},{"type":"null"}]},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["id","account_id","provider","model","resolved_model","path","status_code","response_class","latency_ms","input_tokens","output_tokens","cache_read_tokens","cache_write_tokens","cost","cost_absence","created_at"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A malformed `before` or `limit`","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/providers":{"get":{"operationId":"listProviders","summary":"List the providers a tenant can link","description":"The linkable providers, each with the human name to show and the credential archetype its guided flow opens on (`oauth`, `api_key` or `endpoint`). A client renders a button per entry and drives the matching link route from it, so the set of providers is data rather than something the client hardcodes. Two further fields exist so a client never has to branch on WHICH provider it is holding: `quota` says whether this provider meters a subscription quota at all, which is what tells an account's empty `limits` list apart as `no reading yet` (metered) or `no quota` (none); and `paste_prompt` carries the wording for the paste step of the browser flow, because providers end that flow differently — one shows a code on a page, another redirects to a URL that must be copied whole. `snippet` carries runnable examples for calling the pool with an account of this provider — the providers do not share a request shape, so the sample travels as data. Each `body` holds exactly three substitution tokens, `{{BASE_URL}}`, `{{API_KEY}}` and `{{MODEL}}`; replace those three and run the rest verbatim. An empty `models` means this provider's models are the tenant's own and cannot be listed — offer a free-text field rather than an empty picker.","tags":["accounts"],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"The linkable providers","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"display_name":{"type":"string"},"link_method":{"type":"string","enum":["oauth","api_key","endpoint"]},"credential_kinds":{"minItems":1,"type":"array","items":{"type":"string","enum":["oauth","api_key","endpoint"]}},"quota":{"type":"string","enum":["metered","none"]},"paste_prompt":{"anyOf":[{"type":"object","properties":{"label":{"type":"string"},"placeholder":{"type":"string"},"hint":{"type":"string"}},"required":["label","placeholder","hint"],"additionalProperties":false},{"type":"null"}]},"snippet":{"anyOf":[{"type":"object","properties":{"path":{"type":"string"},"default_model":{"type":"string"},"models":{"type":"array","items":{"type":"string"}},"samples":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"install":{"anyOf":[{"type":"string"},{"type":"null"}]},"body":{"type":"string"}},"required":["id","label","install","body"],"additionalProperties":false}}},"required":["path","default_model","models","samples"],"additionalProperties":false},{"type":"null"}]}},"required":["id","display_name","link_method","credential_kinds","quota","paste_prompt","snippet"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/providers/public":{"get":{"operationId":"listPublicProviders","summary":"List supported providers, without a credential","description":"The providers megaloop supports, as a name and an archetype: `subscription` for a plan you pay a provider monthly for, `self_hosted` for one you run yourself. Unauthenticated, because the public site draws this list and has no key to present. A smaller shape than `GET /v1/providers`, which additionally carries the linking instructions a signed-in client needs.","tags":["accounts"],"security":[],"responses":{"200":{"description":"The supported providers","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"display_name":{"type":"string"},"kind":{"type":"string","enum":["subscription","self_hosted"]}},"required":["id","display_name","kind"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}}}}},"/v1/accounts/link/start":{"post":{"operationId":"startAccountLink","summary":"Begin linking an account through a browser flow","description":"For providers offering the `oauth` credential kind. Returns a URL for the human to open and approve; they are then shown a code to paste into `/v1/accounts/link/complete`. This service hosts no callback endpoint, so there is no redirect to attack. The session is bound to the calling tenant and expires in ten minutes. For providers that take a key or a URL instead, use `/v1/accounts/link/direct` — there is no browser step at all.","tags":["accounts"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"provider":{"type":"string","minLength":1,"maxLength":64},"mode":{"anyOf":[{"type":"string","minLength":1,"maxLength":64},{"type":"null"}]},"name":{"type":"string","minLength":1,"maxLength":100}},"required":["provider","name"]}}}},"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"201":{"description":"A link session","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"link_session_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"authorize_url":{"type":"string","format":"uri"},"expires_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["link_session_id","authorize_url","expires_at"],"additionalProperties":false}}}},"400":{"description":"Unknown provider, or one that offers no browser flow","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/accounts/link/complete":{"post":{"operationId":"completeAccountLink","summary":"Exchange the pasted code for a linked account","description":"Requires the same authenticated tenant that started the session — the id alone is never sufficient. A session that is expired, already used, or belongs to another tenant returns 404 with an identical body, so the id reveals nothing. A code the provider rejects is a 400 and does NOT spend the session: a mistyped paste can be retried until the session expires. Linking beyond the plan's account limit is a 402 with code `account_limit_reached`.","tags":["accounts"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"link_session_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"code":{"type":"string","minLength":1,"maxLength":4096}},"required":["link_session_id","code"]}}}},"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"201":{"description":"The linked account","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"account_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"provider":{"type":"string"},"name":{"type":"string"},"mode":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["account_id","provider","name","mode"],"additionalProperties":false}}}},"400":{"description":"The provider rejected the code","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"402":{"description":"The plan's linked-account limit is reached","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"No live link session for this tenant","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"This tenant already has an account with that name for this provider","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/accounts/link/direct":{"post":{"operationId":"linkAccountDirectly","summary":"Link an account from a credential you already hold","description":"One call, no browser step, for the `api_key` and `endpoint` credential kinds. Send `kind: \"api_key\"` with `api_key`, or `kind: \"endpoint\"` with `base_url` and an optional `token`. The provider validates the credential; the value is stored encrypted and is never returned by this or any other endpoint. Neither kind expires or refreshes. Linking beyond the plan's account limit is a 402 with code `account_limit_reached`.","tags":["accounts"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","oneOf":[{"type":"object","properties":{"provider":{"type":"string","minLength":1,"maxLength":64},"name":{"type":"string","minLength":1,"maxLength":100},"kind":{"type":"string","const":"api_key"},"api_key":{"type":"string","minLength":1,"maxLength":4096}},"required":["provider","name","kind","api_key"]},{"type":"object","properties":{"provider":{"type":"string","minLength":1,"maxLength":64},"name":{"type":"string","minLength":1,"maxLength":100},"kind":{"type":"string","const":"endpoint"},"base_url":{"type":"string","maxLength":2048,"format":"uri"},"token":{"type":"string","minLength":1,"maxLength":4096}},"required":["provider","name","kind","base_url"]}]}}}},"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"201":{"description":"The linked account","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"account_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"provider":{"type":"string"},"name":{"type":"string"},"mode":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["account_id","provider","name","mode"],"additionalProperties":false}}}},"400":{"description":"Unknown provider, unsupported kind, or a credential it refused","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"402":{"description":"The plan's linked-account limit is reached","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"This tenant already has an account with that name for this provider","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/accounts":{"get":{"operationId":"listAccounts","summary":"List the tenant's linked accounts","description":"Every account belonging to the calling tenant, paused ones included — an account that stopped working is exactly the one its owner needs to see. Each carries a `health` object (`status`, `reason`, `since`, `cooldown_until`, `last_used`) so a client can tell at a glance which accounts have dropped out of the pool and why, plus `limits`: how much of that account's subscription is spent per window and when each window resets. An empty `limits` means nothing has been observed for that account yet — not that it is unused. No token, key, base URL or verifier is returned.","tags":["accounts"],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"The tenant's accounts","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"provider":{"type":"string"},"name":{"type":"string"},"mode":{"anyOf":[{"type":"string"},{"type":"null"}]},"credential_kind":{"type":"string","enum":["oauth","api_key","endpoint"]},"paused":{"type":"boolean"},"pause_reason":{"anyOf":[{"type":"string"},{"type":"null"}]},"last_used_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"base_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"health":{"type":"object","properties":{"status":{"type":"string","enum":["active","paused","cooling_down","failing"]},"reason":{"anyOf":[{"type":"string"},{"type":"null"}]},"since":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"cooldown_until":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"last_used":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]}},"required":["status","reason","since","cooldown_until","last_used"],"additionalProperties":false},"limits":{"type":"array","items":{"type":"object","properties":{"window":{"type":"string"},"utilization":{"type":"number","minimum":0,"maximum":1},"resets_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"observed_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["window","utilization","resets_at","observed_at"],"additionalProperties":false}}},"required":["id","provider","name","mode","credential_kind","paused","pause_reason","last_used_at","base_url","health","limits"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/accounts/{id}/resume":{"post":{"operationId":"resumeAccount","summary":"Put a paused account back in rotation","description":"Clears the pause and the cooldown overlay, making the account selectable again. An account is paused when an upstream refuses it with an entitlement or org block; that refusal is often transient (a bot-management challenge, a temporary org restriction) and until this route existed there was no way back — a paused account stayed out of rotation permanently.\n\nIdempotent: resuming an account that is not paused succeeds and changes nothing, because recovery is a state to reach rather than an event to fire exactly once. An account belonging to another tenant and one that never existed both return 404 with the same body.","tags":["accounts"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"204":{"description":"The account is selectable again"},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"No such account for this tenant","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/accounts/{id}":{"delete":{"operationId":"unlinkAccount","summary":"Unlink an account","description":"Deletes the row and the encrypted credential with it — the account is removed, not flagged, because a tenant withdrawing a credential they own should not leave it sitting in our database. An account belonging to another tenant and one that never existed both return 404 with the same body.\n\n**Requires a signed-in session, not an API key.** Withdrawing a credential is irreversible and there is no reason an inference key needs to do it, so this is done from the dashboard. A key-authenticated caller gets 401.","tags":["accounts"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"responses":{"204":{"description":"The account is unlinked and its credential deleted"},"401":{"description":"No session was presented (an API key cannot unlink)","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"No such account for this tenant","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/limits":{"get":{"operationId":"getLimits","summary":"Subscription limit windows for every pooled account","description":"How much of each linked account's subscription is spent right now, and when each window rolls over — the pool's headroom, side by side. `utilization` is a fraction in [0, 1] (0.29 is 29% spent) and `resets_at` is an absolute instant. `window` is an opaque label the provider minted (`5h`, `7d`, and whatever else it reports): treat it as data, humanise it for display, and expect labels you have not seen. An account with an empty `limits` has had nothing observed — it has served no request, or its provider advertises no utilization. That is not zero spent, and no figure here is inferred rather than measured. Readings are captured from served traffic, so a window is as fresh as `observed_at` says and no fresher.","tags":["usage"],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"Limit windows per account","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"account_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"provider":{"type":"string"},"name":{"type":"string"},"limits":{"type":"array","items":{"type":"object","properties":{"window":{"type":"string"},"utilization":{"type":"number","minimum":0,"maximum":1},"resets_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"observed_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["window","utilization","resets_at","observed_at"],"additionalProperties":false}}},"required":["account_id","provider","name","limits"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/routing":{"get":{"operationId":"getRouting","summary":"How each provider's pool of accounts is juggled","description":"One entry per provider pool: the strategy in force, whether the tenant chose it, and — the part worth reading — what each available strategy would do with this pool right now. `outcomes[].account_ids` is derived from the gateway's own ranking over your real accounts and readings, so it is what would happen, not a description of what is meant to happen. It is the set of accounts new work CAN head to, and says nothing about how many callers you run or where they sit today: exactly one id means the choice is settled for every caller; several mean which one a caller gets depends on the caller, because selection breaks ties per caller and two accounts a strategy rates equally are both reachable; an empty list means nothing in that pool can serve. `pool.quota_readings` is false when no account in the pool has a live subscription reading — `most_headroom` and `fill_first` then have nothing to rank on and order the pool exactly as the default does, which is worth saying out loud rather than offering a control that does nothing. `configured` false means the pool is on the default and no row exists; `strategy` still reports what selection is doing.","tags":["accounts"],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"Every pool and its strategy","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"provider":{"type":"string"},"strategy":{"type":"string","enum":["sticky_quota","sticky","fill_first","weighted"]},"configured":{"type":"boolean"},"weights":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"number"}},{"type":"null"}]},"updated_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"pool":{"type":"object","properties":{"accounts":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"available":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"quota_readings":{"type":"boolean"}},"required":["accounts","available","quota_readings"],"additionalProperties":false},"outcomes":{"type":"array","items":{"type":"object","properties":{"strategy":{"type":"string","enum":["sticky_quota","sticky","fill_first","weighted"]},"account_ids":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}},"required":["strategy","account_ids"],"additionalProperties":false}}},"required":["provider","strategy","configured","weights","updated_at","pool","outcomes"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/routing/{provider}":{"put":{"operationId":"setPoolStrategy","summary":"Choose how one provider's pool is juggled","description":"Sets the strategy for the pool named in the path. The provider is an id from `GET /v1/providers`; the tenant is resolved from the presented credential and never from the body (D8).\n\nRetired strategy names are still ACCEPTED and mapped to their replacement, so a tenant who set one before it was retired keeps working. They are deliberately absent from the enum above, which lists only what to choose today. The response reports the strategy actually stored, so read it back rather than assuming the value you sent is the one in force.\n\nA CALLER is one api key, on one model, on one CONVERSATION. Clients that identify their session — Claude Code sends one on every request — get a caller per conversation, so parallel agents sharing a key spread across the pool and each keeps its own prompt cache warm. A client that identifies no session falls back to one caller per (key, model), which is what every caller was before.\n\n`sticky` pins each caller to one account. A conversation stays put; a caller with no conversation id is re-pinned hourly, since for those the rotation is the only thing that spreads anything. It is blind to how much quota is left. `spread` sends new work to the account in the emptiest ten-point utilization band and, among accounts sharing a band, to the least recently handed out one — which moves the same caller on nearly every request, and so pays a cold prompt cache nearly every request. `most_headroom` ranks on the same ten-point bands and nothing else; an account with no reading sorts as if half spent. `fill_first` concentrates on the account whose longest-running quota window resets soonest, spending an allowance that would otherwise expire unused, and holds back any account with no reading or an already-spent window. `weighted` scales each account's share of callers by a weight you assign and reads no quota data at all.\n\nThis REPLACES the pool's setting: weights you do not send are cleared. The change reaches the serving path within about a minute — the gateway caches the setting rather than reading it per request. `DELETE` the same path to return to the default. Returns the updated entry, outcomes recomputed.","tags":["accounts"],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"strategy":{"type":"string","enum":["sticky_quota","sticky","fill_first","weighted"]},"weights":{"anyOf":[{"type":"object","propertyNames":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"additionalProperties":{"type":"number","minimum":0,"maximum":100}},{"type":"null"}]}}}}}},"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"The updated pool entry","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"provider":{"type":"string"},"strategy":{"type":"string","enum":["sticky_quota","sticky","fill_first","weighted"]},"configured":{"type":"boolean"},"weights":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"number"}},{"type":"null"}]},"updated_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"pool":{"type":"object","properties":{"accounts":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"available":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"quota_readings":{"type":"boolean"}},"required":["accounts","available","quota_readings"],"additionalProperties":false},"outcomes":{"type":"array","items":{"type":"object","properties":{"strategy":{"type":"string","enum":["sticky_quota","sticky","fill_first","weighted"]},"account_ids":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}},"required":["strategy","account_ids"],"additionalProperties":false}}},"required":["provider","strategy","configured","weights","updated_at","pool","outcomes"],"additionalProperties":false}}}},"400":{"description":"Unknown provider, unknown strategy, or malformed weights","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"operationId":"clearPoolStrategy","summary":"Return one pool to the default strategy","description":"Deletes the tenant's stored choice for this pool, which returns it to the default — today `sticky`, the same per-caller hashing with hourly rotation that selection used before strategies existed. Idempotent: a pool that was already on the default answers 204. The row is deleted rather than rewritten with the default's name, so the pool keeps following the default if the default ever changes.","tags":["accounts"],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"204":{"description":"The pool is on the default, or already was"},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/billing/checkout":{"post":{"operationId":"createCheckoutSession","summary":"Start a subscription checkout for a tier","description":"Opens a hosted monthly checkout for the requested tier and returns its URL; redirect the browser there. `tier` is `starter` ($39/month, up to 5 linked accounts) or `pro` ($99/month, unlimited) — any other value is a 400. The price id behind a tier is fixed by the server: no amount, price or term is accepted from the caller, and there is no annual option. The subscription belongs to the tenant that presented the credential; no tenant field is read from the body.\n\nA tenant that is ALREADY subscribed gets the self-service portal URL instead (`mode` is then `portal`), where they change tier or cancel — a second checkout would open a second subscription and bill them twice. Check `mode` to word the button. Answers 503 when billing is not configured.","tags":["billing"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"tier":{"type":"string","enum":["starter","pro"]}},"required":["tier"]}}}},"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"201":{"description":"A URL to redirect to: a checkout for a new subscriber, the portal for an existing one","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"url":{"type":"string","format":"uri"},"mode":{"type":"string","enum":["checkout","portal"]}},"required":["url","mode"],"additionalProperties":false}}}},"400":{"description":"The tier was missing or not one this product sells","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Billing is not configured","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/billing/portal":{"post":{"operationId":"createBillingPortalSession","summary":"Open the billing management portal","description":"Creates a self-service portal session for the calling tenant's customer and returns its URL. This is where a plan CHANGE happens — Starter to Pro, Pro to Starter, or cancel — and where a payment method is updated; the switch is applied to the one existing subscription rather than opening another. The tenant must already have a customer (i.e. have started checkout at least once). Answers 503 when billing is not configured.","tags":["billing"],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"A portal URL to redirect to","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"url":{"type":"string","format":"uri"},"mode":{"type":"string","enum":["checkout","portal"]}},"required":["url","mode"],"additionalProperties":false}}}},"400":{"description":"The tenant has no billing customer yet","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Billing is not configured","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/billing/status":{"get":{"operationId":"getBillingStatus","summary":"The calling tenant's subscription and entitlement","description":"The whole plan picture for the resolved tenant, so a UI or an agent can render it without re-deriving a single rule. Recorded state — `plan`, `status`, `current_period_end`, `price` — sits beside the decision made from it: `state` (`trialing` | `active` | `past_due` | `expired`), `tier` (`trial` | `starter` | `pro`, null when nothing entitles them), `trial_days_left`, `trial_ends_at` (the deadline itself, since a clamped day count cannot be turned back into one), `account_limit` (null means unlimited) and `account_count`. `state` and `account_limit` come from the same entitlement function the account cap and the gateway use, so a client that reads them can never disagree with enforcement. Reads only the resolved tenant's own rows (D8). Answers 503 when billing is not configured.","tags":["billing"],"security":[{"apiKey":[]},{"bearerAuth":[]}],"responses":{"200":{"description":"The tenant's subscription status and entitlement","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"plan":{"type":"string"},"status":{"anyOf":[{"type":"string"},{"type":"null"}]},"current_period_end":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"price":{"anyOf":[{"type":"string"},{"type":"null"}]},"state":{"type":"string","enum":["trialing","active","past_due","expired"]},"tier":{"anyOf":[{"type":"string","enum":["trial","starter","pro"]},{"type":"null"}]},"trial_days_left":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"trial_ends_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"account_limit":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"account_count":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["plan","status","current_period_end","price","state","tier","trial_days_left","trial_ends_at","account_limit","account_count"],"additionalProperties":false}}}},"401":{"description":"No usable credential was presented","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Billing is not configured","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/billing/webhook":{"post":{"operationId":"handleBillingWebhook","summary":"Receive a payment-provider webhook","description":"Verifies the event signature against the raw body and applies subscription changes to the tenant resolved from the stored customer mapping. The subscription's PRICE is mapped back to a tier and stored beside the status and period end, so an upgrade, a downgrade and a cancellation all arrive here. A bad or missing signature is a 400 that changes nothing. Idempotent: a re-delivered event does not double-apply. Not called by clients — the payment provider calls it.","tags":["billing"],"security":[],"responses":{"200":{"description":"The event was accepted (or ignored) idempotently"},"400":{"description":"The signature did not verify"},"503":{"description":"Billing (or its webhook secret) is not configured"}}}},"/v1/support/tickets":{"post":{"operationId":"createSupportTicket","summary":"Send a message to support","description":"Stores a support request and emails it to the operator, with the sender's address set as the reply-to so a reply reaches them directly. The sender also receives an acknowledgement quoting what they sent.\n\nUnauthenticated, because somebody who cannot sign in is the person most likely to need it. The response is identical whether or not the address has an account.\n\nThe ticket is stored before either email is attempted, and a failed send does not fail the request: the durable record is what is promised, and a message that reached us should not be reported as lost because our own notification bounced.","tags":["support"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"message":{"type":"string","minLength":1,"maxLength":8000}},"required":["name","email","message"]}}}},"security":[],"responses":{"202":{"description":"The message was stored","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"status":{"type":"string","const":"accepted"},"ticket_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},"required":["status","ticket_id"],"additionalProperties":false}}}},"400":{"description":"A field was missing or malformed","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"429":{"description":"Too many messages from this address, or too many in total, within the hour. The endpoint sends mail to the address supplied, so it is bounded the same way login codes are.","content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","account_limit_reached","email_taken","not_found","conflict","rate_limited","internal_error","service_unavailable"]},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/openapi.json":{"get":{"operationId":"getOpenApiDocument","summary":"This API's OpenAPI document","description":"Generated from the server's own route table on every request, so it cannot describe a service other than the one answering.","tags":["meta"],"security":[],"responses":{"200":{"description":"An OpenAPI 3.1 document"}}}}}}