{"openapi":"3.1.0","info":{"title":"fairly","description":"fairly product API: native auth gateway, device sync, invites and server-side reads.","license":{"name":"UNLICENSED","identifier":"UNLICENSED"},"version":"0.1.37"},"paths":{"/j/{token}":{"get":{"tags":["crate::invite_page"],"summary":"The invite page: a static dark HTML page with a button that opens the app and a link to get it.\nNo JavaScript.","operationId":"invite_page","parameters":[{"name":"token","in":"path","description":"The invite token","required":true,"schema":{"type":"string"}},{"name":"lang","in":"query","description":"`en`, `es`, `fr`, `de` or `it`; overrides `Accept-Language`","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"the invite page","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"unknown token","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/s/{token}":{"get":{"tags":["crate::settle"],"summary":"The guest settle page: a static dark HTML rendering of [`SettleView`], in English or Spanish.\nNo JavaScript.","operationId":"settle_page","parameters":[{"name":"token","in":"path","description":"The settle token","required":true,"schema":{"type":"string"}},{"name":"lang","in":"query","description":"`en` or `es`; overrides `Accept-Language`","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"the guest settle page","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"unknown token","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/s/{token}/paid":{"post":{"tags":["crate::settle"],"summary":"The page's form target: records the payment in the group's ledger as\n`method = \"marked_paid\"` and re-renders.","operationId":"settle_page_paid","parameters":[{"name":"token","in":"path","description":"The settle token","required":true,"schema":{"type":"string"}},{"name":"lang","in":"query","description":"`en` or `es`; overrides `Accept-Language`","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"the page in its paid state","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"unknown token","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/v1/ai/insights/wording":{"post":{"tags":["crate::ai"],"summary":"Phrase deterministic insights as friendly sentences. Numbers, names and dates are never changed.","operationId":"insight_wording_handler","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightWordingRequest"}}},"required":true},"responses":{"200":{"description":"the wordings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightWordingResponse"}}}},"403":{"description":"no access to the group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`ai_unavailable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"fairly_session":[]}]}},"/v1/ai/parse":{"post":{"tags":["crate::ai"],"summary":"Parse a sentence (\"dinner 42 paid by Bea, split with Marta\") into an expense draft. Nothing is\nwritten.","operationId":"parse_handler","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseRequest"}}},"required":true},"responses":{"200":{"description":"the parsed draft","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseResponse"}}}},"403":{"description":"no access to the group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`ai_unavailable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"fairly_session":[]}]}},"/v1/ai/receipt-scan":{"post":{"tags":["crate::ai"],"summary":"Extract a receipt's merchant, amounts and line items with the vision model. Returns a draft\nfor review; nothing is written.","operationId":"receipt_scan_handler","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiptScanRequest"}}},"required":true},"responses":{"200":{"description":"the extracted receipt","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiptScanResponse"}}}},"403":{"description":"no access to the group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"no such receipt object","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`ai_unavailable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"fairly_session":[]}]}},"/v1/ai/status":{"get":{"tags":["crate::ai"],"summary":"Which AI features are available. The device hides what is off and uses its offline paths.","operationId":"ai_status","responses":{"200":{"description":"feature availability","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiStatusResponse"}}}}},"security":[{"fairly_session":[]}]}},"/v1/ai/talk":{"post":{"tags":["crate::ai"],"summary":"Talk to the copilot about the caller's groups. It can read balances, expenses and members and\npropose actions; it never performs them.","operationId":"talk_handler","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TalkRequest"}}},"required":true},"responses":{"200":{"description":"the reply and proposals","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TalkResponse"}}}},"403":{"description":"no access to `group_id`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`ai_unavailable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"fairly_session":[]}]}},"/v1/auth/push_device":{"post":{"tags":["crate::proxy"],"summary":"`POST /v1/auth/push_device`: record a push token (nothing delivers until mailroom exists).","operationId":"push_device","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushDeviceRequest"}}},"required":true},"responses":{"204":{"description":"recorded"}},"security":[{"fairly_session":[]}]}},"/v1/auth/session":{"get":{"tags":["crate::proxy"],"summary":"`GET /v1/auth/session`: who the handle belongs to.","operationId":"session","responses":{"200":{"description":"the current session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionInfo"}}}}},"security":[{"fairly_session":[]}]}},"/v1/auth/sign_out":{"post":{"tags":["crate::proxy"],"summary":"`POST /v1/auth/sign_out`: end the porter session and revoke the handle.","operationId":"sign_out","responses":{"204":{"description":"signed out"}},"security":[{"fairly_session":[]}]}},"/v1/client/environment":{"get":{"summary":"Porter's environment document","responses":{"200":{"description":"porter's DTO, relayed, plus `device`","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v1/client/me":{"get":{"summary":"Read the current user","description":"Session-bound porter call over the `fs_` handle. A rotated porter session returns a new `session` object the phone must store.","responses":{"200":{"description":"porter's DTO, relayed","content":{"application/json":{"schema":{"type":"object"}}}}},"security":[{"fairly_session":[]}]},"patch":{"summary":"Update the current user's profile","description":"Session-bound porter call over the `fs_` handle. A rotated porter session returns a new `session` object the phone must store.","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"porter's DTO, relayed","content":{"application/json":{"schema":{"type":"object"}}}}},"security":[{"fairly_session":[]}]}},"/v1/client/me/passwords":{"post":{"summary":"Set or change the password","description":"Session-bound porter call over the `fs_` handle. A rotated porter session returns a new `session` object the phone must store.","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"porter's DTO, relayed","content":{"application/json":{"schema":{"type":"object"}}}}},"security":[{"fairly_session":[]}]}},"/v1/client/me/sessions":{"get":{"summary":"List the current user's sessions","description":"Session-bound porter call over the `fs_` handle. A rotated porter session returns a new `session` object the phone must store.","responses":{"200":{"description":"porter's DTO, relayed","content":{"application/json":{"schema":{"type":"object"}}}}},"security":[{"fairly_session":[]}]}},"/v1/client/me/sessions/revoke_others":{"post":{"summary":"End every other session","description":"Session-bound porter call over the `fs_` handle. A rotated porter session returns a new `session` object the phone must store.","responses":{"200":{"description":"porter's DTO, relayed","content":{"application/json":{"schema":{"type":"object"}}}}},"security":[{"fairly_session":[]}]}},"/v1/client/me/sessions/{id}":{"delete":{"summary":"End one session","description":"Session-bound porter call over the `fs_` handle. A rotated porter session returns a new `session` object the phone must store.","responses":{"200":{"description":"porter's DTO, relayed","content":{"application/json":{"schema":{"type":"object"}}}}},"security":[{"fairly_session":[]}]}},"/v1/client/sign_ins":{"post":{"summary":"Start a sign-in","description":"Proxied porter flow step. Send the `device` from the previous response as `X-Fairly-Device`. On completion the body carries `session` in place of porter's `session_token`.","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"porter's flow DTO plus `device` and, on completion, `session`","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v1/client/sign_ins/{id}":{"get":{"summary":"Read a sign-in attempt","responses":{"200":{"description":"porter's DTO, relayed, plus `device`","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v1/client/sign_ins/{id}/{step}":{"post":{"summary":"Advance a sign-in (identify, prepare_first_factor, attempt_first_factor, prepare_second_factor, attempt_second_factor, reset_password, abandon)","description":"Proxied porter flow step. Send the `device` from the previous response as `X-Fairly-Device`. On completion the body carries `session` in place of porter's `session_token`.","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"porter's flow DTO plus `device` and, on completion, `session`","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v1/client/sign_ups":{"post":{"summary":"Start a sign-up","description":"Proxied porter flow step. Send the `device` from the previous response as `X-Fairly-Device`. On completion the body carries `session` in place of porter's `session_token`.","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"porter's flow DTO plus `device` and, on completion, `session`","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v1/client/sign_ups/{id}":{"get":{"summary":"Read a sign-up attempt","responses":{"200":{"description":"porter's DTO, relayed, plus `device`","content":{"application/json":{"schema":{"type":"object"}}}}}},"post":{"summary":"Update a sign-up attempt","description":"Proxied porter flow step. Send the `device` from the previous response as `X-Fairly-Device`. On completion the body carries `session` in place of porter's `session_token`.","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"porter's flow DTO plus `device` and, on completion, `session`","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v1/client/sign_ups/{id}/{step}":{"post":{"summary":"Advance a sign-up (prepare_verification, attempt_verification, abandon)","description":"Proxied porter flow step. Send the `device` from the previous response as `X-Fairly-Device`. On completion the body carries `session` in place of porter's `session_token`.","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"porter's flow DTO plus `device` and, on completion, `session`","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v1/expenses/{expense_id}/history":{"get":{"tags":["crate::history"],"summary":"The edit history of one expense.","operationId":"expense_history","parameters":[{"name":"expense_id","in":"path","description":"The expense id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"the expense's versions, oldest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityHistoryResponse"}}}},"403":{"description":"no access to the expense's group"},"404":{"description":"no such expense"}},"security":[{"fairly_session":[]}]}},"/v1/groups/{group_id}/balances":{"get":{"tags":["crate::read"],"summary":"Per-member, per-currency balances for a group, computed server-side.","operationId":"balances","parameters":[{"name":"group_id","in":"path","description":"The group id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"per-member nets","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BalancesResponse"}}}}},"security":[{"fairly_session":[]}]}},"/v1/groups/{group_id}/invites":{"post":{"tags":[],"summary":"Mint an invite link for a group the caller belongs to and, when `email` is given, email it:\n`invite.friend` for a Direct group, `invite.group` otherwise. A notifier failure never fails the\nrequest — the link is still returned with `emailed: false`.","operationId":"create_invite","parameters":[{"name":"group_id","in":"path","description":"The group id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInviteRequest"}}},"required":true},"responses":{"200":{"description":"the invite link","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteResponse"}}}}},"security":[{"fairly_session":[]}]}},"/v1/groups/{group_id}/pairwise":{"get":{"tags":["crate::read"],"summary":"Pairwise \"who owes whom\" nets for a group, computed server-side. Zero nets are omitted.","operationId":"pairwise","parameters":[{"name":"group_id","in":"path","description":"The group id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"pairwise nets by currency","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PairwiseResponse"}}}}},"security":[{"fairly_session":[]}]}},"/v1/groups/{group_id}/settle":{"get":{"tags":["crate::read"],"summary":"The settle-up plan for a group, computed server-side from its balances.","operationId":"settle","parameters":[{"name":"group_id","in":"path","description":"The group id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"proposed settle-up payments","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SettleResponse"}}}}},"security":[{"fairly_session":[]}]}},"/v1/groups/{group_id}/settle_links":{"post":{"tags":["crate::settle"],"summary":"Mint a settle link: the viewer (payee) asks `to_member_id` (payer) to settle by link.","operationId":"create_settle_link","parameters":[{"name":"group_id","in":"path","description":"The group id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSettleLinkRequest"}}},"required":true},"responses":{"200":{"description":"the settle link","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SettleLinkResponse"}}}},"403":{"description":"no access to the group"},"404":{"description":"to_member_id is not a live member of the group"},"409":{"description":"the viewer has no member in the group"}},"security":[{"fairly_session":[]}]}},"/v1/invites/{token}":{"get":{"tags":[],"summary":"Preview an invite (no auth): the group name and the member to claim.","operationId":"invite_preview","parameters":[{"name":"token","in":"path","description":"The invite token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"the invite preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitePreview"}}}}}}},"/v1/invites/{token}/accept":{"post":{"tags":[],"summary":"Accept an invite: grant the caller access and link or create their member.","operationId":"accept_invite","parameters":[{"name":"token","in":"path","description":"The invite token","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInviteRequest"}}},"required":true},"responses":{"200":{"description":"the joined group and member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInviteResponse"}}}}},"security":[{"fairly_session":[]}]}},"/v1/receipts/presign":{"post":{"tags":["crate::receipts"],"summary":"Presign an upload of one receipt image for an expense in a group the caller can access.","operationId":"presign","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresignReceiptRequest"}}},"required":true},"responses":{"200":{"description":"the presigned upload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresignReceiptResponse"}}}},"403":{"description":"no access to the group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"content_length exceeds 10 MiB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"bad id, deleted expense, empty upload or unsupported content type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: too many presigns this hour or too many unattached uploads","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"receipt storage is not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"fairly_session":[]}]}},"/v1/receipts/{key}":{"get":{"tags":["crate::receipts"],"summary":"Redirect to a short-lived presigned download of a receipt in a group the caller can access.","operationId":"fetch","parameters":[{"name":"key","in":"path","description":"The receipt key, `g/{group_id}/e/{expense_id}/{file}` (slashes unescaped)","required":true,"schema":{"type":"string"}}],"responses":{"302":{"description":"redirect to the presigned GET (`Location`)"},"403":{"description":"no access to the group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"not a receipt key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"receipt storage is not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"fairly_session":[]}]}},"/v1/search":{"get":{"tags":["crate::search"],"summary":"Search the caller's groups: folded text match, or semantic ranking when embeddings are live.","operationId":"search_handler","parameters":[{"name":"q","in":"query","description":"The search text (1..=200 characters).","required":true,"schema":{"type":"string"}},{"name":"group_id","in":"query","description":"Restrict to one group the caller can access.","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","description":"How many results (1..=50, default 20).","required":false,"schema":{"type":"integer","format":"int32","minimum":0}}],"responses":{"200":{"description":"hits from the caller's groups","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}},"403":{"description":"no access to `group_id`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"invalid query","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"fairly_session":[]}]}},"/v1/settle/{token}":{"get":{"tags":["crate::settle"],"summary":"The guest page's data (no auth): the pair's net, the last items and the group's progress.","operationId":"settle_view","parameters":[{"name":"token","in":"path","description":"The settle token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"the guest view (a neutral `expired` view for an expired or revoked link)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SettleView"}}}},"404":{"description":"unknown token"}}}},"/v1/settle/{token}/paid":{"post":{"tags":["crate::settle"],"summary":"Mark a settle link paid (no auth): records the payment and consumes the link.","operationId":"settle_paid","parameters":[{"name":"token","in":"path","description":"The settle token","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SettlePaidRequest"}}},"required":true},"responses":{"200":{"description":"the recorded (or previously recorded) payment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SettlePaidResponse"}}}},"400":{"description":"the link has expired or was revoked"},"404":{"description":"unknown token"},"409":{"description":"nothing is owed"}}}},"/v1/site/events":{"post":{"tags":["site"],"summary":"Store the valid items in one anonymous, cookieless analytics batch.","operationId":"events","requestBody":{"content":{"application/x-ndjson":{"schema":{"$ref":"#/components/schemas/SiteEventRequest"}}},"required":true},"responses":{"202":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteEventsResponse"}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/site/waitlist":{"post":{"tags":["site"],"summary":"Store one waitlist submission. JSON clients receive 201; native forms return to the source page.","operationId":"waitlist","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistRequest"}}},"required":true},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistResponse"}}}},"303":{"description":"URL-encoded form accepted; redirect to the source page"},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/sync/changes":{"get":{"tags":[],"summary":"Return entities changed since the client's cursor, across every group the user can access.","operationId":"changes","parameters":[{"name":"since","in":"query","description":"The cursor from the client's last pull; omitted on a first sync.","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","description":"Maximum entities to return; clamped server-side.","required":false,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"changed entities and the advanced cursor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PullResponse"}}}}},"security":[{"fairly_session":[]}]}},"/v1/sync/mutations":{"post":{"tags":[],"summary":"Apply a batch of device mutations under optimistic concurrency.","operationId":"push","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushRequest"}}},"required":true},"responses":{"200":{"description":"per-mutation outcomes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushResponse"}}}}},"security":[{"fairly_session":[]}]}},"/v1/webhooks/porter":{"post":{"tags":["crate::webhooks"],"summary":"Receive a porter webhook delivery.","operationId":"porter_webhook","requestBody":{"description":"Svix-compatible envelope signed by porter","content":{"application/json":{"schema":{}}},"required":true},"responses":{"200":{"description":"accepted (acted on or ignored)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookAck"}}}},"401":{"description":"bad or missing signature"},"503":{"description":"porter_webhook is not configured"}}}}},"components":{"schemas":{"AcceptInviteRequest":{"type":"object","description":"The invite acceptance body.","required":["display_name"],"properties":{"display_name":{"type":"string","description":"The display name for a newly created member (ignored when an existing member is linked)."},"email":{"type":["string","null"],"description":"The accepting user's email, recorded on the member."}}},"AcceptInviteResponse":{"type":"object","description":"The invite acceptance outcome.","required":["group_id","member_id"],"properties":{"group_id":{"type":"string","description":"The group the user now has access to."},"member_id":{"type":"string","description":"The member entity linked to the user in that group."}}},"AiFeatures":{"type":"object","description":"Which AI features this server can serve right now.","required":["receipt_scan","parse","insight_wording","talk","semantic_search"],"properties":{"insight_wording":{"type":"boolean","description":"`POST /v1/ai/insights/wording`."},"parse":{"type":"boolean","description":"`POST /v1/ai/parse`."},"receipt_scan":{"type":"boolean","description":"`POST /v1/ai/receipt-scan` (needs the model and receipt storage)."},"semantic_search":{"type":"boolean","description":"`GET /v1/search` can rank semantically (embeddings work and pgvector is installed)."},"talk":{"type":"boolean","description":"`POST /v1/ai/talk`."}}},"AiStatusResponse":{"type":"object","description":"`GET /v1/ai/status`.","required":["available","features"],"properties":{"available":{"type":"boolean","description":"Whether any model is reachable."},"features":{"$ref":"#/components/schemas/AiFeatures","description":"Per-feature availability."}}},"BalanceView":{"type":"object","description":"One member's net position in a single currency.","required":["member_id","currency","net_minor"],"properties":{"currency":{"type":"string","description":"ISO-4217 currency code."},"member_id":{"type":"string","description":"The member id (canonical UUID string)."},"net_minor":{"type":"integer","format":"int64","description":"Net minor units: positive = the member is owed, negative = the member owes."}}},"BalancesResponse":{"type":"object","description":"The balances response: every member's non-zero net, sorted by `(member_id, currency)`.","required":["balances"],"properties":{"balances":{"type":"array","items":{"$ref":"#/components/schemas/BalanceView"},"description":"Per-member, per-currency nets."}}},"ChangeEnvelope":{"type":"object","description":"One changed entity in a pull response.","required":["kind","entity"],"properties":{"entity":{"type":"object","description":"The full current entity JSON (a soft-deleted entity carries `deleted_at`)."},"kind":{"type":"string","description":"The entity kind."}}},"CreateInviteRequest":{"type":"object","description":"The invite creation body.","properties":{"email":{"type":["string","null"],"description":"Email the invite link to this address (and record it on the claimed member at acceptance)."},"locale":{"type":["string","null"],"description":"The email's language: `en` (default) or `es`.","example":"en"},"member_id":{"type":["string","null"],"description":"The unlinked, non-removed member the invitee should claim, if the inviter picked one."}}},"CreateSettleLinkRequest":{"type":"object","description":"The settle-link creation body.","required":["to_member_id"],"properties":{"to_member_id":{"type":"string","description":"The member who owes the viewer (the link's payer)."}}},"EntityHistoryResponse":{"type":"object","description":"An entity's edit history, oldest first.","required":["kind","entity_id","group_id","versions"],"properties":{"entity_id":{"type":"string","description":"The entity id."},"group_id":{"type":"string","description":"The group the entity belongs to."},"kind":{"type":"string","description":"The sync kind (`expense`)."},"versions":{"type":"array","items":{"$ref":"#/components/schemas/HistoryVersion"},"description":"Every retained version, ascending. Rows older than the retention window are purged."}}},"Error":{"type":"object","description":"The one error shape every surface (HTTP, CLI `--json`, MCP) emits.\n\n`hint` is written for an agent to act on. `code` values are stable and listed per tool.","required":["code","message"],"properties":{"code":{"type":"string","description":"Stable machine-readable code, `snake_case`."},"details":{"description":"Machine-readable structured detail (e.g. a re-verification `level`/`max_age`). Never free\ntext — a surface that needs to act on the failure reads this rather than parsing `message`.\n\nBoxed so the common `None` case keeps [`Error`] small (it is returned in `Result`s\neverywhere); the box is elided from the wire when absent and transparent when present."},"docs_url":{"type":["string","null"],"description":"Link to the relevant documentation."},"hint":{"type":["string","null"],"description":"What to do about it; written for an agent."},"message":{"type":"string","description":"Human-readable message."}}},"HistoryVersion":{"type":"object","description":"One recorded version of an entity.","required":["version","op","actor","at","entity"],"properties":{"actor":{"type":"string","description":"Who wrote it: the pusher's `usr_…`, or `system` / `server` / `guest` for server-authored writes."},"at":{"type":"string","description":"When the write was accepted (RFC 3339, UTC)."},"entity":{"type":"object","description":"The entity JSON exactly as stored at this version."},"op":{"type":"string","description":"`put` for an ordinary write, `delete` for a tombstone (non-null `deleted_at`)."},"version":{"type":"integer","format":"int64","description":"The entity version this write produced."}}},"InsightInput":{"type":"object","description":"One deterministic insight to phrase.","required":["id","message_key","args"],"properties":{"args":{"type":"array","items":{"type":"object"},"description":"The message arguments (`fairly_core::domain::InsightArg`: `{name, value: {type, value}}`)."},"id":{"type":"string","description":"The insight's id; echoed back, never sent to the model."},"message_key":{"type":"string","description":"The Fluent message key the device would otherwise render."}}},"InsightWording":{"type":"object","description":"One worded insight.","required":["id","text"],"properties":{"id":{"type":"string","description":"The insight id from the request."},"text":{"type":"string","description":"The sentence to show."}}},"InsightWordingRequest":{"type":"object","description":"Word insights for a group.","required":["group_id","locale","insights"],"properties":{"group_id":{"type":"string","description":"The group the insights are about."},"insights":{"type":"array","items":{"$ref":"#/components/schemas/InsightInput"},"description":"Up to 20 insights."},"locale":{"type":"string","description":"The language to write in (`en`, `es`)."}}},"InsightWordingResponse":{"type":"object","description":"The worded insights; an insight the model skipped is absent (render the Fluent message).","required":["wordings"],"properties":{"wordings":{"type":"array","items":{"$ref":"#/components/schemas/InsightWording"},"description":"Wordings, in request order."}}},"InvitePreview":{"type":"object","description":"The anonymous invite preview.","required":["group_name","group_kind","expired"],"properties":{"expired":{"type":"boolean","description":"Whether the link has expired."},"group_kind":{"type":"string","description":"The group's kind (`trip`/`home`/`couple`/`other`/`direct`). `direct` is a 1:1 friend\nledger, so the join screen can say \"X wants to split with you\"."},"group_name":{"type":"string","description":"The group's name."},"member_display_name":{"type":["string","null"],"description":"The display name of the member the invitee would claim, if the invite names one."}}},"InviteResponse":{"type":"object","description":"A freshly minted invite link.","required":["token","url","expires_at","emailed"],"properties":{"emailed":{"type":"boolean","description":"Whether the invite email was handed to the notifier. `false` when no `email` was given or the\nnotifier could not take it (the link is still valid and can be shared by hand)."},"expires_at":{"type":"string","description":"When the link expires (RFC 3339)."},"token":{"type":"string","description":"The opaque invite token (the `/j/<token>` path segment)."},"url":{"type":"string","description":"The shareable URL: `<public_origin>/j/<token>`."}}},"MutationEnvelope":{"type":"object","description":"One mutation: the full intended state of an entity plus the version it was based on.","required":["mutation_id","kind","entity_id","op","payload","base_version"],"properties":{"base_version":{"type":"integer","format":"int64","description":"The entity `version` this edit was based on (`0` for a create)."},"entity_id":{"type":"string","description":"The entity's stable id."},"kind":{"type":"string","description":"The entity kind (`group`/`member`/`expense`/`payment`/`reminder`/`recurring`/`comment`/\n`settlement_group`/`notice`)."},"mutation_id":{"type":"string","description":"Client-generated id, unique per mutation; the unit of idempotent replay."},"op":{"type":"string","description":"The mutation op (`create`/`update`/`delete`); advisory only."},"payload":{"type":"object","description":"The full intended entity state. Carries `id` (a group) or `group_id` (anything else)."}}},"PairwiseCurrencyView":{"type":"object","description":"All pairwise nets for one currency.","required":["currency","pairs"],"properties":{"currency":{"type":"string","description":"ISO-4217 currency code."},"pairs":{"type":"array","items":{"$ref":"#/components/schemas/PairwiseNetView"},"description":"Non-zero nets, keyed by canonical pair."}}},"PairwiseNetView":{"type":"object","description":"A net between one canonical member pair.","required":["pair_key","amount"],"properties":{"amount":{"type":"integer","format":"int64","description":"Signed from the `min` member's side: positive means the `max` member owes the `min` member."},"pair_key":{"type":"string","description":"`\"min|max\"`: the two member ids in ascending string order, joined by `|`."}}},"PairwiseResponse":{"type":"object","description":"The pairwise response: who-owes-whom, by currency.","required":["currencies"],"properties":{"currencies":{"type":"array","items":{"$ref":"#/components/schemas/PairwiseCurrencyView"},"description":"One bucket per currency."}}},"ParseRequest":{"type":"object","description":"Parse one line of natural language into an expense draft.","required":["group_id","text","today"],"properties":{"group_id":{"type":"string","description":"The group whose people and currency the text is about."},"text":{"type":"string","description":"What the user typed or dictated (1..=500 characters)."},"today":{"type":"string","description":"The device's today, `YYYY-MM-DD` (for \"yesterday\", \"on Friday\")."}}},"ParseResponse":{"type":"object","description":"The parsed draft.","required":["draft"],"properties":{"draft":{"type":"object","description":"`fairly_core::domain::ParsedExpenseDraft` (camelCase). Names are the roster's display\nnames, resolved on the server; anything unresolved is listed in `ambiguities`."}}},"PresignReceiptRequest":{"type":"object","description":"Ask for a presigned upload URL for one receipt image.","required":["group_id","expense_id","content_type","content_length"],"properties":{"content_length":{"type":"integer","format":"int64","description":"The exact byte length of the upload; at most 10 MiB.","minimum":0},"content_type":{"type":"string","description":"`image/jpeg`, `image/png`, `image/webp` or `image/heic`."},"expense_id":{"type":"string","description":"The expense the receipt is attached to. It need not be synced yet (a receipt scan presigns\nbefore the expense exists), but when it is it must not be deleted."},"group_id":{"type":"string","description":"The group the expense belongs to; the caller must have access to it."}},"additionalProperties":false},"PresignReceiptResponse":{"type":"object","description":"A presigned upload: `PUT` the bytes to `put_url` with every header in `headers`, then record `key`\nas the expense's `receipt_key`.","required":["key","put_url","expires_at","headers"],"properties":{"expires_at":{"type":"string","description":"When `put_url` stops working (RFC 3339)."},"headers":{"type":"object","description":"Headers the upload must send verbatim (they are part of the signature).","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"key":{"type":"string","description":"The object key: `g/{group_id}/e/{expense_id}/{uuidv7}.{ext}`."},"put_url":{"type":"string","description":"The presigned `PUT` URL."}}},"Proposal":{"type":"object","description":"An action the copilot proposes. The server never performs it; the device shows `summary` and,\non confirmation, applies `payload` through its normal write path.","required":["id","kind","summary","payload"],"properties":{"id":{"type":"string","description":"A server-minted id for this proposal (UUID v7)."},"kind":{"$ref":"#/components/schemas/ProposalKind","description":"What it does."},"payload":{"type":"object","description":"The draft, with every id resolved on the server (serde of the `fairly_core::app` draft)."},"summary":{"type":"string","description":"A one-line human summary."}}},"ProposalKind":{"type":"string","description":"What a proposal would do if the person confirms it.","enum":["add_expense","record_payment","create_group","add_person","set_reminder"]},"ProposedPaymentView":{"type":"object","description":"A payment the settle-up plan proposes: `from` should pay `to`.","required":["from","to","amount_minor","currency"],"properties":{"amount_minor":{"type":"integer","format":"int64","description":"How much, in `currency`'s minor units. Always positive."},"currency":{"type":"string","description":"The currency to settle in."},"from":{"type":"string","description":"The member who should pay (canonical UUID string)."},"from_household":{"type":["string","null"],"description":"The household (settlement group id) paying, when `from` is a household's anchor member."},"to":{"type":"string","description":"The member who should be paid (canonical UUID string)."},"to_household":{"type":["string","null"],"description":"The household being paid, when `to` is a household's anchor member."}}},"PullResponse":{"type":"object","description":"The pull response body: changed entities plus the advanced cursor.","required":["changes","cursor","has_more"],"properties":{"changes":{"type":"array","items":{"$ref":"#/components/schemas/ChangeEnvelope"},"description":"The changed entities, oldest change first."},"cursor":{"type":"string","description":"The cursor to pass as `since` on the next pull."},"has_more":{"type":"boolean","description":"Whether more changes remain past `cursor`."}}},"PushDeviceRequest":{"type":"object","description":"The push registration body.","required":["platform","token"],"properties":{"locale":{"type":["string","null"],"description":"The device's language (`en`, `es`); pushes the server sends this user go out in it.\nAnything else is stored as `en`; absent keeps what was recorded before."},"platform":{"type":"string","description":"`apns` or `fcm`."},"token":{"type":"string","description":"The platform push token."}}},"PushRejection":{"type":"object","description":"The reason a single mutation in a batch was refused. The rest of the batch is unaffected: earlier\nand later mutations apply (or conflict) on their own, so a push never half-commits behind an\nerror response.","required":["code","message"],"properties":{"code":{"type":"string","description":"The stable error code (`forbidden`, `validation_failed`, `not_found`, ...)."},"message":{"type":"string","description":"A human-readable description."}}},"PushRequest":{"type":"object","description":"The push request body: a batch of mutations drained from the device outbox.","required":["mutations"],"properties":{"mutations":{"type":"array","items":{"$ref":"#/components/schemas/MutationEnvelope"},"description":"The mutations to apply, in order."}}},"PushResponse":{"type":"object","description":"The push response body: one result per mutation, in request order.","required":["results"],"properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/PushResult"},"description":"Per-mutation outcomes."}}},"PushResult":{"type":"object","description":"The outcome of one pushed mutation.","required":["mutation_id","status"],"properties":{"current":{"type":["object","null"],"description":"The server's authoritative entity state after the attempt, if any."},"current_version":{"type":["integer","null"],"format":"int64","description":"On a rejected mutation, the stored entity's `version` when the entity exists and the pusher\ncan read its group (`current` then carries the stored entity). Absent otherwise."},"error":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PushRejection","description":"Why the mutation was refused, when it was rejected for a reason other than a version conflict\n(no access, a forbidden member link, a malformed payload). Absent on applied mutations and on\nplain version conflicts, whose `current` carries the server's truth instead."}]},"kind":{"type":["string","null"],"description":"The entity kind, so the client can route `current` to the right table on rejection."},"mutation_id":{"type":"string","description":"The `mutation_id` this result is for."},"reason":{"type":["string","null"],"description":"On a rejected mutation, the stable reason code: `conflict` for a stale `base_version`,\notherwise the refusal's error code (`forbidden`, `validation_failed`, ...). Absent on applied\nmutations."},"status":{"type":"string","description":"`\"applied\"` or `\"rejected\"`."}}},"ReceiptScanRequest":{"type":"object","description":"Scan an uploaded receipt image.","required":["group_id","key","currency"],"properties":{"currency":{"type":"string","description":"The currency to assume when the receipt prints none (ISO-4217)."},"group_id":{"type":"string","description":"The group the receipt belongs to."},"key":{"type":"string","description":"The object key returned by `POST /v1/receipts/presign`; must be under `group_id`."}}},"ReceiptScanResponse":{"type":"object","description":"The extracted receipt.","required":["scan","needs_review"],"properties":{"needs_review":{"type":"boolean","description":"`true` ⇒ show the review screen prominently; never auto-submit."},"scan":{"type":"object","description":"The model's extraction (`fairly_core::domain::ReceiptScanResult`, camelCase, decimal-string\namounts exactly as printed)."}}},"SearchMode":{"type":"string","description":"How the results were ranked.","enum":["semantic","text"]},"SearchResponse":{"type":"object","description":"The search response.","required":["mode","results"],"properties":{"mode":{"$ref":"#/components/schemas/SearchMode","description":"Which ranking produced `results`."},"results":{"type":"array","items":{"$ref":"#/components/schemas/SearchResult"},"description":"Hits, best first."}}},"SearchResult":{"type":"object","description":"One search hit.","required":["group_id","entity_kind","entity_id","title","snippet","score"],"properties":{"amount_minor":{"type":["integer","null"],"format":"int64","description":"The amount in minor units, when the entity has one."},"currency":{"type":["string","null"],"description":"The amount's currency."},"date":{"type":["string","null"],"description":"The civil date, `YYYY-MM-DD`."},"entity_id":{"type":"string","description":"The entity id."},"entity_kind":{"type":"string","description":"The entity kind (`expense`)."},"group_id":{"type":"string","description":"The group the entity belongs to."},"score":{"type":"number","format":"double","description":"Relevance, higher first (0..=1)."},"snippet":{"type":"string","description":"A short line of context (payers, category, notes)."},"title":{"type":"string","description":"The headline (an expense's description)."}}},"SessionHandle":{"type":"object","description":"The session object added to a phone-facing body when a handle is minted or rotated.","required":["handle","expires_at"],"properties":{"expires_at":{"type":"string","description":"When the handle expires (RFC 3339)."},"handle":{"type":"string","description":"The opaque bearer (`fs_…`) the phone stores and sends as `Authorization: Bearer`."}}},"SessionInfo":{"type":"object","description":"The current session summary the phone reads on launch.","required":["user_id","display_name","needs_name","expires_at"],"properties":{"display_name":{"type":"string","description":"The display name: porter's `name`, or the email's local part."},"email":{"type":["string","null"],"description":"The primary email, when porter knows one."},"expires_at":{"type":"string","description":"When the handle expires (RFC 3339)."},"name":{"type":["string","null"],"description":"Porter's own `name`, when the user has set one (or a social sign-in supplied it)."},"needs_name":{"type":"boolean","description":"True when porter has no name for the user: the phone should ask for one."},"user_id":{"type":"string","description":"The porter user (`usr_…`)."}}},"SettleBalance":{"type":"object","description":"The pair's net in one currency.","required":["currency","amount_minor","direction"],"properties":{"amount_minor":{"type":"integer","format":"int64","description":"The size of the net, in minor units (always positive)."},"currency":{"type":"string","description":"ISO-4217 code."},"direction":{"type":"string","description":"`payer_owes_payee` (the guest owes the link's sender) or `payee_owes_payer`."}}},"SettleItem":{"type":"object","description":"One item in \"HOW THIS NUMBER HAPPENED\": an expense that creates a debt between the two.","required":["description","amount_minor","currency","occurred_on"],"properties":{"amount_minor":{"type":"integer","format":"int64","description":"The expense total, in `currency`'s minor units."},"currency":{"type":"string","description":"The expense's currency (ISO-4217)."},"description":{"type":"string","description":"The expense's description."},"occurred_on":{"type":"string","description":"The civil date the expense occurred on (`YYYY-MM-DD`)."}}},"SettleLinkResponse":{"type":"object","description":"A freshly minted settle link.","required":["token","url","expires_at"],"properties":{"expires_at":{"type":"string","description":"When the link expires (RFC 3339)."},"token":{"type":"string","description":"The opaque token (the `/s/<token>` path segment)."},"url":{"type":"string","description":"The shareable URL: `<public_origin>/s/<token>`."}}},"SettlePaidPayment":{"type":"object","description":"One payment \"mark paid\" recorded.","required":["payment_id","amount_minor","currency"],"properties":{"amount_minor":{"type":"integer","format":"int64","description":"The amount, in `currency`'s minor units."},"currency":{"type":"string","description":"ISO-4217 code."},"payment_id":{"type":"string","description":"The payment's id."}}},"SettlePaidRequest":{"type":"object","description":"The \"mark paid\" body.","required":["method"],"properties":{"method":{"type":"string","description":"How the guest paid (free text: `bizum`, `cash`, …). Recorded on the payment."}}},"SettlePaidResponse":{"type":"object","description":"The \"mark paid\" outcome.","required":["payment_id","amount_minor","currency","payments","already_paid"],"properties":{"already_paid":{"type":"boolean","description":"`true` when the link had already been consumed and nothing new was recorded."},"amount_minor":{"type":"integer","format":"int64","description":"The first recorded amount, in `currency`'s minor units."},"currency":{"type":"string","description":"The first recorded currency."},"payment_id":{"type":"string","description":"The first recorded payment's id (see `payments` for every currency)."},"payments":{"type":"array","items":{"$ref":"#/components/schemas/SettlePaidPayment"},"description":"One payment per currency the guest owed, sorted by currency code."}}},"SettleResponse":{"type":"object","description":"The settle-up response: a minimal set of proposed payments that brings everyone to zero.","required":["payments"],"properties":{"payments":{"type":"array","items":{"$ref":"#/components/schemas/ProposedPaymentView"},"description":"The proposed payments; recording them is a human action (`docs/fairly-native.md` § 3.3)."}}},"SettleView":{"type":"object","description":"The guest page's data: the pair's current net, the last items between the two, and progress.","required":["group_name","payee_name","payer_name","currency","amount_minor","balances","items","paid_count","total_count","expired","consumed"],"properties":{"amount_minor":{"type":"integer","format":"int64","description":"What the payer owes the payee in `currency`, in minor units: positive, or `0`."},"balances":{"type":"array","items":{"$ref":"#/components/schemas/SettleBalance"},"description":"Every currency with a non-zero net between the two, sorted by code, each with its direction."},"consumed":{"type":"boolean","description":"Whether the link has already recorded its payment."},"currency":{"type":"string","description":"The first currency (by code) in which the payer owes the payee; the group's default when\nnothing is owed. Kept for older readers: `balances` is the full picture."},"expired":{"type":"boolean","description":"Whether the link has expired or been revoked. Such a view carries no ledger data."},"group_name":{"type":"string","description":"The group's name."},"items":{"type":"array","items":{"$ref":"#/components/schemas/SettleItem"},"description":"The last five live expenses that create a debt between the two members, newest first."},"paid_count":{"type":"integer","description":"Live members whose net is zero in every currency.","minimum":0},"payee_name":{"type":"string","description":"The member who is owed (minted the link)."},"payer_name":{"type":"string","description":"The member who owes (opens the link)."},"total_count":{"type":"integer","description":"Live members in the group.","minimum":0}}},"SiteEventRequest":{"type":"object","description":"One cookieless fairly.day analytics event.","required":["name","props","ts","path","locale","referrer","utm"],"properties":{"locale":{"type":"string","description":"Site locale."},"name":{"type":"string","description":"Stable snake-case event name."},"path":{"type":"string","description":"Site path where the event occurred. Stored without query or fragment, with link tokens and\nid-shaped segments replaced by placeholders."},"props":{"type":"object","description":"Event-specific properties."},"referrer":{"type":"string","description":"Referrer sent by the site, or an empty string. Stored as scheme, host and redacted path only."},"ts":{"type":"string","description":"Client event time in RFC 3339 form."},"utm":{"type":"object","description":"UTM parameters captured by the site."}},"additionalProperties":false},"SiteEventsResponse":{"type":"object","description":"The outcome of a bounded analytics batch.","required":["accepted","rejected"],"properties":{"accepted":{"type":"integer","description":"Valid events stored in this request.","minimum":0},"rejected":{"type":"integer","description":"Malformed or invalid events omitted from storage.","minimum":0}}},"TalkMessage":{"type":"object","description":"One message of the conversation so far.","required":["role","content"],"properties":{"content":{"type":"string","description":"What was said."},"role":{"$ref":"#/components/schemas/TalkRole","description":"Who said it."}}},"TalkRequest":{"type":"object","description":"A talk turn.","required":["locale","messages"],"properties":{"group_id":{"type":["string","null"],"description":"Scope the copilot to one group; omit for every group the caller can access."},"locale":{"type":"string","description":"The language to reply in."},"messages":{"type":"array","items":{"$ref":"#/components/schemas/TalkMessage"},"description":"The conversation, oldest first; the last message is the user's."}}},"TalkResponse":{"type":"object","description":"The copilot's answer.","required":["reply","proposals"],"properties":{"proposals":{"type":"array","items":{"$ref":"#/components/schemas/Proposal"},"description":"Actions to confirm; empty when the reply is just an answer."},"reply":{"type":"string","description":"What to show the person."}}},"TalkRole":{"type":"string","description":"Who said a talk message.","enum":["user","assistant"]},"WaitlistRequest":{"type":"object","description":"A fairly.day waitlist submission.","required":["email","locale","location","path"],"properties":{"email":{"type":"string","description":"Email address to notify at launch."},"locale":{"type":"string","description":"Site locale (`en` or `es`)."},"location":{"type":"string","description":"Form placement, such as `hero` or `final`."},"path":{"type":"string","description":"Site path where the form was submitted."}},"additionalProperties":false},"WaitlistResponse":{"type":"object","description":"A successful waitlist submission.","required":["ok"],"properties":{"ok":{"type":"boolean","description":"Always `true` after the row has been stored."}}},"WebhookAck":{"type":"object","description":"The acknowledgement every accepted delivery gets.","required":["ok"],"properties":{"ok":{"type":"boolean","description":"Always `true`."}}}},"securitySchemes":{"fairly_session":{"type":"http","scheme":"bearer","bearerFormat":"fs_ handle"}}}}