[{"method":"GET","path":"/health","group":"Service","summary":"Liveness and readiness","notes":"Touches the database. A process that answers while its only datastore is unreachable will pass a health check straight through an outage.","auth":"public","body":null,"query":[],"captures":[]},{"method":"GET","path":"/v1/meta","group":"Service","summary":"Published contact details and the age policy","notes":"App Store Guideline 1.2 requires developer contact information to exist and be findable. Served rather than hardcoded, so the app, the web client and a reviewer all read the same value. Also carries the operator portal's address, which lives in the web app rather than here.","auth":"public","body":null,"query":[],"captures":[]},{"method":"GET","path":"/","group":"Service","summary":"The API's front door","notes":"A page, not an endpoint: what this host is, and where the console and the explorer live. It was a 404, and a host that serves an API and nothing at its root is a host somebody has to be told the URLs for.","auth":"public","body":null,"query":[],"captures":[]},{"method":"GET","path":"/api","group":"Service","summary":"This explorer","notes":"Renders the catalogue below and calls the API with it. Responses feed a variable store, because an API is a sequence and the id you need in step three came out of step two.","auth":"public","body":null,"query":[],"captures":[]},{"method":"GET","path":"/v1/meta/routes","group":"Service","summary":"This catalogue, as JSON","notes":"Hand-written, because the useful half of an API reference is the reasoning rather than the shape of the body — and none of that can be derived from a handler signature. A test parses the router's own source and fails the build if a route is registered without an entry here, so it cannot quietly go stale.","auth":"public","body":null,"query":[],"captures":[]},{"method":"POST","path":"/v1/auth/signup","group":"Auth","summary":"Create an account","notes":"The 18+ gate lives here and is terminal: a refused date is recorded against the device and the address hash, and a blocked device gets the same answer whatever it sends next — telling it apart would tell it another date might work. No phone number is asked for, ever.","auth":"public","body":"{\n  \"email\": \"rob@example.com\",\n  \"password\": \"correct horse battery staple\",\n  \"displayName\": \"Rob Martinez\",\n  \"dob\": \"1991-04-07\",\n  \"deviceId\": \"explorer\"\n}","query":[],"captures":[{"name":"token","pointer":"/token"},{"name":"userId","pointer":"/user/id"}]},{"method":"POST","path":"/v1/auth/login","group":"Auth","summary":"Sign in","notes":"A wrong password and an unknown address return an identical body, and an unknown address is still verified against a decoy hash so it costs the same — otherwise response time is an account-enumeration oracle. A banned or suspended account is told, because the alternative is retyping a correct password until you contact support.","auth":"public","body":"{\n  \"email\": \"rob@example.com\",\n  \"password\": \"correct horse battery staple\"\n}","query":[],"captures":[{"name":"token","pointer":"/token"},{"name":"userId","pointer":"/user/id"}]},{"method":"POST","path":"/v1/auth/logout","group":"Auth","summary":"End this session","notes":"Revokes the row behind the token. Tokens are opaque rows rather than signed claims precisely so this — and a ban — can take effect now.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"POST","path":"/v1/auth/verify-email","group":"Auth","summary":"Redeem a confirmation link","notes":"Unauthenticated on purpose: the link may be opened in a browser that has never signed in. The token is the credential, and it is single-use, so a mail scanner following it after the reader does not activate anything twice. In development the link is written to the log — there is no mail transport.","auth":"public","body":"{\n  \"token\": \"stk_…\"\n}","query":[],"captures":[]},{"method":"POST","path":"/v1/auth/resend-verification","group":"Auth","summary":"Send the confirmation link again","notes":"Issuing a new link invalidates the last, so a resend cannot leave two working links in two different inboxes.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"GET","path":"/v1/me","group":"Profile","summary":"The private view of your own account","notes":"The public profile plus email, status, and whether the account may act yet. The date of birth is never returned: it is collected once for the gate, and every surface sees only an age bucket.","auth":"bearer","body":null,"query":[],"captures":[{"name":"userId","pointer":"/id"}]},{"method":"PATCH","path":"/v1/me","group":"Profile","summary":"Update your profile","notes":"Absent means leave it; an explicit null clears it — which a single optional cannot express. A rating and its provenance travel together, and the only provenance anything may write is `self` until the DUPR partnership is signed. Every text field goes through the content filter.","auth":"bearer","body":"{\n  \"displayName\": \"Rob Martinez\",\n  \"bio\": \"Drills Tue/Thu\",\n  \"gender\": \"male\",\n  \"rating\": 4.312\n}","query":[],"captures":[]},{"method":"DELETE","path":"/v1/me","group":"Profile","summary":"Delete your account, immediately","notes":"Apple rejects any app supporting account creation without in-app deletion. Credentials, sessions, messages and device tokens are hard-deleted; the row stays as a tombstone with every personal field nulled, so sessions other players attended still resolve. A hash of the address is retained so a banned account cannot delete itself and re-register. Asks for the password again.","auth":"bearer","body":"{\n  \"password\": \"correct horse battery staple\"\n}","query":[],"captures":[]},{"method":"GET","path":"/v1/players/{id}","group":"Profile","summary":"Someone's public profile","notes":"Answers 404 for blocked, deleted, and never-existed alike. A 403 would confirm the account is there and announce that a block is in place.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"PUT","path":"/v1/players/{id}/block","group":"Profile","summary":"Block someone","notes":"Required by App Store Guideline 1.2 alongside reporting and filtering. Symmetric: it hides the profile in both directions, declines anything outstanding between the two, and closes an existing chat room without deleting it — the history is what a report is made of.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"DELETE","path":"/v1/players/{id}/block","group":"Profile","summary":"Unblock","notes":"Restores visibility both ways. It does not revive a declined chat request.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"GET","path":"/v1/me/blocks","group":"Profile","summary":"Everyone you have blocked","notes":"Yours only. There is no way to learn who has blocked you.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"GET","path":"/v1/courts","group":"Sessions","summary":"The Austin court list","notes":"Courts are places, not user content: nothing creates one, and the launch market is a single city seeded by a migration.","auth":"bearer","body":null,"query":[],"captures":[{"name":"courtId","pointer":"/0/id"}]},{"method":"POST","path":"/v1/sessions","group":"Sessions","summary":"Host a session","notes":"The host takes position 0 — a host who is not on the court is an organiser, which V1 does not have. Capacity follows the format unless you say otherwise: two for singles, four otherwise. Send an `Idempotency-Key` header and a retry replays the first answer instead of hosting a second game. A confirmed email is the whole barrier.","auth":"active","body":"{\n  \"courtId\": \"c_zilker\",\n  \"kind\": \"rated\",\n  \"format\": \"doubles\",\n  \"startsAt\": 0,\n  \"endsAt\": 0,\n  \"minRating\": 4.0,\n  \"maxRating\": 4.5,\n  \"notes\": \"Bring a spare ball\"\n}","query":[],"captures":[{"name":"sessionId","pointer":"/id"}]},{"method":"GET","path":"/v1/sessions","group":"Sessions","summary":"Browse — the home screen","notes":"Sorted by start time, never by an algorithm, and time-anchored rather than an infinite feed. The rating band is matched by overlap, not containment: a 4.0–4.5 session belongs in a 3.5–4.25 browse. Give both ends of a band or neither.","auth":"bearer","body":null,"query":[{"name":"from","example":"","about":"UTC epoch seconds. Defaults to now."},{"name":"to","example":"","about":"Upper bound on start time."},{"name":"kinds","example":"rated,friendly","about":"Comma-separated."},{"name":"duprOnly","example":"true","about":"Only sessions that count."},{"name":"minRating","example":"3.5","about":"Band low end."},{"name":"maxRating","example":"4.25","about":"Band high end."},{"name":"openOnly","example":"true","about":"Only ones with a spot."},{"name":"area","example":"Zilker","about":"Court neighbourhood."},{"name":"courtId","example":"","about":"One court."},{"name":"limit","example":"60","about":"Page size, max 200."}],"captures":[{"name":"sessionId","pointer":"/0/id"}]},{"method":"GET","path":"/v1/sessions/{id}","group":"Sessions","summary":"One session","notes":"`state` is derived, not merely stored: a session inside the two-hour lock window reads as locked whether or not the maintenance job has caught up. `viewer` carries where you stand, including whether you are on the waitlist — which the court lineup cannot express.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"PATCH","path":"/v1/sessions/{id}","group":"Sessions","summary":"Edit a session you host","notes":"Time and court stay editable — the details a host actually has to correct. The band does not, once a second person has planned their evening around it: they agreed to a session, not to whatever it becomes.","auth":"bearer","body":"{\n  \"startsAt\": 0,\n  \"endsAt\": 0,\n  \"courtId\": \"c_pharr\"\n}","query":[],"captures":[]},{"method":"POST","path":"/v1/sessions/{id}/cancel","group":"Sessions","summary":"Cancel a session you host","notes":"Terminal. A host cancels rather than leaves — a host quietly dropping out leaves a roster with no organiser and nobody told.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"POST","path":"/v1/sessions/{id}/join","group":"Sessions","summary":"Take a spot","notes":"The capacity check and the insert happen inside one `BEGIN IMMEDIATE` transaction, so the last spot goes to exactly one of several simultaneous joins. A full session you could have played offers the waitlist; one you could never have played says why instead. Eligibility is checked here and never again — a rating that moves later leaves you in. A confirmed email is the whole barrier.","auth":"active","body":null,"query":[],"captures":[]},{"method":"DELETE","path":"/v1/sessions/{id}/join","group":"Sessions","summary":"Drop out","notes":"Promotes whoever is first in line, in the same transaction, and gives back the session's chat room. The messages stay: the people still going are still coordinating around them.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"DELETE","path":"/v1/sessions/{id}/waitlist","group":"Sessions","summary":"Stop waiting","notes":"Leaves the queue without touching the roster.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"GET","path":"/v1/me/sessions","group":"Sessions","summary":"Your commitments","notes":"Not a booking screen — these are promises to other people, which is a different thing from a reserved court. Reaches back one review window as well as forward, so last night's game is still here carrying its review action item.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"GET","path":"/v1/sessions/{id}/review","group":"Post-session","summary":"The review screen","notes":"Only after the session has ended, only within a week, and only for people who were on the court — everyone else gets a 404. Returns the three badges that exist, so nothing negative can appear.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"POST","path":"/v1/sessions/{id}/review","group":"Post-session","summary":"Submit attendance and badges","notes":"\"Everyone showed\" is the assumption, not something to assert, so an empty body is a valid review. Absence needs a consensus — at least half the other players — because one report is noise. Resubmitting inside the window replaces, so taking a badge back takes it back.","auth":"active","body":"{\n  \"absentPlayerIds\": [],\n  \"badges\": {}\n}","query":[],"captures":[]},{"method":"POST","path":"/v1/reports","group":"Post-session","summary":"Report someone","notes":"Private in both directions: the response says only that it was received, and the person reported is told nothing. `detail` is deliberately not content-filtered — quoting the abuse is the point, and the filter would strip the evidence. `suspected_minor` suspends immediately; everything else waits for a human, and line calls are recorded but never adjudicated.","auth":"active","body":"{\n  \"reportedId\": \"{{userId}}\",\n  \"category\": \"sandbagging\",\n  \"detail\": \"Playing well below their listed rating.\"\n}","query":[],"captures":[]},{"method":"POST","path":"/v1/chat/requests","group":"Chat","summary":"Ask to chat","notes":"Anyone may ask; only the recipient may open the room. The request carries no free text — attach a message and it is a cold DM through a different endpoint. Answers 202 for a new ask, a duplicate, one already declined, and a pair who can already talk: a requester who could tell those apart would know they had been declined. Capped at 20 outstanding.","auth":"active","body":"{\n  \"playerId\": \"{{userId}}\"\n}","query":[],"captures":[]},{"method":"GET","path":"/v1/chat/requests","group":"Chat","summary":"Requests waiting on you","notes":"There is deliberately no companion endpoint for requests you sent: the only fact such a list could report is whether somebody declined.","auth":"bearer","body":null,"query":[],"captures":[{"name":"requestId","pointer":"/0/id"}]},{"method":"POST","path":"/v1/chat/requests/{id}/accept","group":"Chat","summary":"Accept, opening the room","notes":"One room per pair however it was asked — if each had asked the other, one accept resolves both.","auth":"active","body":null,"query":[],"captures":[{"name":"roomId","pointer":"/id"}]},{"method":"POST","path":"/v1/chat/requests/{id}/decline","group":"Chat","summary":"Decline","notes":"Silent and permanent. The row is kept so the same person cannot ask again, and nothing is pushed to them — a rejected stranger handed a signal is a rejected stranger with something to act on.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"GET","path":"/v1/chat/rooms","group":"Chat","summary":"Your conversations","notes":"Unread counts and the latest message, most recently active first. No title or subtitle: the client composes those from the participants, so there is one place to change wording and no server-authored prose about a named person.","auth":"bearer","body":null,"query":[],"captures":[{"name":"roomId","pointer":"/0/id"}]},{"method":"GET","path":"/v1/chat/rooms/{id}/messages","group":"Chat","summary":"History, newest first","notes":"The cursor is a message id, not an offset and not a timestamp: ids are UUIDv7 so they already sort by creation, one column does the work of a compound cursor, and a page cannot shift under a reader.","auth":"bearer","body":null,"query":[{"name":"before","example":"","about":"A message id from a previous page."},{"name":"limit","example":"50","about":"Page size, max 100."}],"captures":[]},{"method":"POST","path":"/v1/chat/rooms/{id}/messages","group":"Chat","summary":"Say something","notes":"Sending is REST; receiving is the WebSocket. One write path, so the content filter, the block check and the status codes exist once instead of being reimplemented as error frames. Links are refused; phone numbers are not — two people who agreed to talk swapping numbers is ordinary.","auth":"active","body":"{\n  \"body\": \"6:30 at Zilker?\"\n}","query":[],"captures":[]},{"method":"POST","path":"/v1/chat/rooms/{id}/read","group":"Chat","summary":"Move the read cursor","notes":"Forward only — a stale client replaying an old cursor must not resurrect unread counts. Omit the id to mark everything read.","auth":"bearer","body":"{\n  \"throughMessageId\": null\n}","query":[],"captures":[]},{"method":"POST","path":"/v1/chat/rooms/{id}/mute","group":"Chat","summary":"Mute a room","notes":"Silences a room without leaving it, which matters because leaving is not always available: a session's room is left by leaving the session, and a direct room is exited by blocking. Mute is the answer for a conversation you want to keep but not hear from.","auth":"bearer","body":"{\n  \"muted\": true\n}","query":[],"captures":[]},{"method":"DELETE","path":"/v1/chat/messages/{id}","group":"Chat","summary":"Take back a message you sent","notes":"Hidden, not deleted. The row and its body stay so moderation can read what a report is about — a delete that erases the evidence is a delete that rewards sending it.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"POST","path":"/v1/chat/ws-ticket","group":"Chat","summary":"A ticket for opening the socket","notes":"A browser cannot set an Authorization header on a WebSocket handshake, and a session token in a query string is written into every access log along the way. A ticket lives sixty seconds and burns on first use, so what ends up in the URL is worthless by the time anyone reads it.","auth":"bearer","body":null,"query":[],"captures":[{"name":"wsTicket","pointer":"/ticket"}]},{"method":"GET","path":"/v1/chat/ws","group":"Chat","summary":"The live socket","notes":"Not callable from this explorer — it is an upgrade, not a request. Connect with `?ticket=…`, or an Authorization header from a native client. Receive only: `ready`, `message`, `chatRequest`, `roomOpened`, `pong`, and `resync` when a connection has fallen too far behind to be handed contiguous history. The console's chat lab drives two of these at once.","auth":"bearer","body":null,"query":[{"name":"ticket","example":"{{wsTicket}}","about":"From /v1/chat/ws-ticket."}],"captures":[]},{"method":"GET","path":"/v1/admin/stats","group":"Operator","summary":"The numbers that mean something","notes":"Sessions that filled and happened, and review completion rate. Downloads, signups and MAU are deliberately absent: a thousand signups with two sessions a week is failure, and a dashboard reporting it as success is worse than none.","auth":"moderator","body":null,"query":[],"captures":[]},{"method":"GET","path":"/v1/admin/reports","group":"Operator","summary":"The open moderation queue","notes":"Ordered by how many distinct people have reported an account — counting reporters rather than reports is what stops one person filing twelve times from looking like twelve people.","auth":"moderator","body":null,"query":[],"captures":[{"name":"reportId","pointer":"/0/id"}]},{"method":"POST","path":"/v1/admin/reports/{id}/resolve","group":"Operator","summary":"Close a report","notes":"Records what was done. Resolving twice is a 404.","auth":"moderator","body":"{\n  \"action\": \"warned\"\n}","query":[],"captures":[]},{"method":"GET","path":"/v1/admin/users","group":"Operator","summary":"Look someone up","notes":"The only user search in the API, and it is moderator-only — a member cannot enumerate accounts.","auth":"moderator","body":null,"query":[{"name":"q","example":"rob","about":"Name, email, or id."}],"captures":[]},{"method":"POST","path":"/v1/admin/users/{id}/status","group":"Operator","summary":"Suspend, ban, or reinstate","notes":"Ends every live session for that account on the next request. Refuses to act on yourself, refuses `deleted` — that is the account holder's to do, in the app — and a moderator cannot act on another moderator.","auth":"moderator","body":"{\n  \"status\": \"suspended\"\n}","query":[],"captures":[]},{"method":"POST","path":"/v1/admin/test-users","group":"Operator","summary":"Mint a ready-to-play account","notes":"Confirmed, phone-verified, and signed in. It hands back a live session token, which is an account-takeover button wearing a helpful hat — so it refuses outright in production. It exists because there is no testing a two-person conversation with one account.","auth":"admin","body":"{\n  \"displayName\": \"Ada Test\"\n}","query":[],"captures":[{"name":"testToken","pointer":"/token"},{"name":"testUserId","pointer":"/id"}]},{"method":"POST","path":"/v1/groups","group":"Crews","summary":"Start a crew","notes":"A crew is the recurring half of the product — \"Tuesday 4.0 at Zilker\" — and deliberately not a club (a facility) or a skill band (already a filter on the session browse, needing no membership). The creator becomes its organizer and its founder, and the standing chat room opens with it rather than on the first message: a room that appears when somebody speaks has no empty state to speak into. `cadence.startTime` is wall clock in the court's own zone, never an epoch, so a 6:30pm Tuesday crew stays 6:30pm across a DST transition.","auth":"active","body":"{\n  \"name\": \"Tuesday 4.0\",\n  \"homeCourtId\": \"{{courtId}}\",\n  \"cadence\": { \"weekday\": 2, \"startTime\": \"18:30\" },\n  \"minRating\": 4.0,\n  \"maxRating\": 4.5,\n  \"visibility\": \"request\",\n  \"description\": \"Competitive doubles, rain or shine.\"\n}","query":[],"captures":[{"name":"groupId","pointer":"/id"}]},{"method":"GET","path":"/v1/groups","group":"Crews","summary":"Browse crews","notes":"`invite` crews are excluded in SQL rather than filtered afterwards. A listing that shows a crew nobody can ask to join has still told a stranger who plays together on Tuesdays — so they are not counted, not paged around, and not left as a gap in the list. Rosters come back only for crews the viewer is in; everybody else gets the shape and a member count.","auth":"bearer","body":null,"query":[{"name":"courtId","example":"{{courtId}}","about":"Only crews at this court."},{"name":"limit","example":"50","about":"1-100, default 50."}],"captures":[]},{"method":"GET","path":"/v1/me/groups","group":"Crews","summary":"My crews, including asks still pending","notes":"Separate from the browse for the same reason /v1/me/sessions is separate from /v1/sessions: \"what is out there\" and \"what I am part of\" are two questions, and one screen answering both is a screen with a mode switch on it.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"GET","path":"/v1/groups/{id}","group":"Crews","summary":"One crew","notes":"A roster is not public: a non-member gets the court, the cadence, the band and a member count, and no names. `pendingRequests` is organizer-only and carries no note — free text from a stranger to a person is exactly the cold-DM channel the chat request design closes. `roomId` is present only for members.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"PATCH","path":"/v1/groups/{id}","group":"Crews","summary":"Edit a crew","notes":"Organizers only. Visibility is editable on purpose — a crew that fills up and closes itself to requests is the normal case, and making that a new crew would strand the chat history. A band sent one end at a time is validated against the end already stored, so a new minimum cannot cross an existing maximum.","auth":"bearer","body":"{\n  \"visibility\": \"invite\"\n}","query":[],"captures":[]},{"method":"DELETE","path":"/v1/groups/{id}","group":"Crews","summary":"Disband a crew","notes":"The founder, not any organizer. An organizer can be added by another organizer, so a crew any organizer could delete is a crew one bad promotion away from gone. Members, the standing room and its messages all cascade — a chat that outlives its crew is a room nobody can leave.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"POST","path":"/v1/groups/{id}/join","group":"Crews","summary":"Join, or ask to","notes":"`open` admits immediately, `request` records a pending row and tells the organizers, `invite` answers 404 — a 403 would confirm a crew the viewer was never shown. A stated band is enforced on open crews too: a crew that says 4.0-4.5 and lets a 3.0 walk in has not stated anything, and the organizer finds out at the court. The request carries no message field.","auth":"active","body":null,"query":[],"captures":[]},{"method":"DELETE","path":"/v1/groups/{id}/join","group":"Crews","summary":"Leave, or withdraw an ask","notes":"Refused with 409 when it would leave the crew with no organizer: hand it over first. Silently promoting the longest-standing member would make somebody an organizer of a crew without asking them. Leaving takes the standing room with it; the messages stay, because they are what a report is made of.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"POST","path":"/v1/groups/{id}/requests/{userId}","group":"Crews","summary":"Approve a pending request","notes":"Organizers only. Writes the membership row and the standing room's membership inside one transaction — a member who is in the crew but not in its chat is a bug that surfaces days later as \"why can't I see the messages\".","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"DELETE","path":"/v1/groups/{id}/members/{userId}","group":"Crews","summary":"Decline a request, or remove a member","notes":"One endpoint, because they are one write: the membership row goes and the room access goes with it. Declining is silent, the same rule a declined chat request follows. The founder cannot be removed by anybody — a crew whose organizers can vote out the person who made it is a crew that can be taken, and there is no support desk to take it back.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"PUT","path":"/v1/groups/{id}/members/{userId}/role","group":"Crews","summary":"Promote or demote an organizer","notes":"Exactly two roles: a third (\"moderator of a crew\") is a permission system, and a crew of eight people does not need one. Promoting is open to any organizer; demoting the founder is open to nobody, the founder included.","auth":"bearer","body":"{\n  \"role\": \"organizer\"\n}","query":[],"captures":[]},{"method":"GET","path":"/v1/partners","group":"Partners","summary":"The partner board","notes":"The other question the product exists for: not \"is there a game tonight\" but \"who will keep playing with me\". Postings whose stated band excludes the viewer are withheld rather than shown and refused later — browsing shows you the people who would play you. Every `player` is rendered from the live profile, so somebody who links DUPR and moves from 3.9 to 4.2 is a 4.2 here immediately. Blocks apply in both directions.","auth":"bearer","body":null,"query":[{"name":"intent","example":"doubles","about":"doubles | drill | both. `both` matches everybody."},{"name":"limit","example":"50","about":"1-100, default 50."}],"captures":[]},{"method":"GET","path":"/v1/me/partner-posting","group":"Partners","summary":"My posting","notes":"404 when there has never been one. A posting that has been taken down or has passed its 60-day life still comes back here, with `active: false` — the one person who needs to know a notice has gone quiet is the person who wrote it.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"PUT","path":"/v1/me/partner-posting","group":"Partners","summary":"Post, or replace what is posted","notes":"A full replace rather than a patch: a posting is one short statement, and a partial update invites the half-edited state where the band moved and the note still describes the old one. Posting again is also how a taken-down or expired notice comes back, so there is no separate reactivate. The note is screened for contact details like every other profile surface.","auth":"active","body":"{\n  \"intent\": \"both\",\n  \"minRating\": 3.75,\n  \"maxRating\": 4.5,\n  \"genderPref\": \"any\",\n  \"availability\": \"Weeknights after 6, Sat mornings\",\n  \"note\": \"Working on third-shot drops.\"\n}","query":[],"captures":[]},{"method":"DELETE","path":"/v1/me/partner-posting","group":"Partners","summary":"Take my posting down","notes":"A flag, not a delete. The row survives so the next \"I'm looking again\" is one tap on a form that still remembers the band and the availability, rather than a blank one.","auth":"bearer","body":null,"query":[],"captures":[]},{"method":"GET","path":"/v1/activity","group":"Activity","summary":"What happened while you were away","notes":"The app used to say nothing: not that somebody took the last spot in your game, not that you were promoted off a waitlist, not that a host cancelled the thing you had planned your evening around — exactly the events in the gap between joining and playing, which is the interval both launch metrics measure. Items are structured rather than pre-rendered prose, so the client owns the wording and no server text names a person. The unread count ships with the page because every client that draws this list also draws a badge, and two requests can disagree on screen. `before` is an item id, not a timestamp.","auth":"bearer","body":null,"query":[{"name":"before","example":"","about":"An item id from a previous page."},{"name":"limit","example":"30","about":"1-50, default 30."}],"captures":[]},{"method":"POST","path":"/v1/activity/read","group":"Activity","summary":"Mark items read","notes":"An empty body marks everything, which is what opening the screen does. Read is not deleted: the item stays, because the feed is also how you get back to the session you were told about yesterday. Returns the refreshed feed, so a client never has to follow this with a GET.","auth":"bearer","body":"{\n  \"ids\": []\n}","query":[],"captures":[]}]