{"components":{"responses":{"BookingsDisabled":{"description":"Bookings are off on this API (`BOOKINGS_ENABLED` isn’t `true`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"not found."}}}},"BookingNotFound":{"description":"No such booking for this parent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Booking not found.","data":null}}}},"FamilyRequestLimit":{"description":"The signed-in family account is making requests too quickly. Family sessions are not metered by the data-API read allotment; they share one abuse limit per account across all its sessions. Wait for Retry-After and try again.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","example":12}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message","retryAfterMs","windowMs"],"properties":{"error":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"null"},"retryAfterMs":{"type":"integer","minimum":0,"description":"Milliseconds until another request is allowed."},"windowMs":{"type":"integer","minimum":1,"description":"Length of the limit's window in milliseconds."}}},"example":{"data":null,"error":true,"message":"This account is making requests too quickly. Wait for Retry-After and try again.","retryAfterMs":12000,"windowMs":60000}}}}},"schemas":{"ChildGuardian":{"properties":{"email":{"type":"string"},"joinedAt":{"format":"date-time","type":"string"},"userId":{"format":"uuid","type":"string"}},"required":["userId","email","joinedAt"],"type":"object"},"ChildInvitation":{"properties":{"createdAt":{"format":"date-time","type":"string"},"email":{"format":"email","type":"string"},"expiresAt":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"}},"required":["id","email","createdAt","expiresAt"],"type":"object"},"ChildInvitationPreview":{"allOf":[{"$ref":"#/components/schemas/ChildInvitation"},{"properties":{"childId":{"format":"uuid","type":"string"},"childName":{"type":"string"},"invitedByEmail":{"type":"string"}},"required":["childId","childName","invitedByEmail"],"type":"object"}]},"FamilySocialItemSummary":{"type":"object","required":["key","kind","id","name","organizationId","organizationName","imageUrl","logoUrl","available","savedAt"],"properties":{"key":{"type":"string","description":"activity:\u003cuuid\u003e or organization:\u003cuuid\u003e.","pattern":"^(activity|organization):[0-9a-f-]{36}$"},"kind":{"enum":["activity","organization"]},"id":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Empty only when the catalog record no longer exists."},"organizationId":{"type":"string","description":"The organization, or the record itself for an organization; empty when unknown."},"organizationName":{"type":"string"},"imageUrl":{"type":["string","null"],"description":"The record's first photo: its selected cover, else its first gallery photo. A logo is never a photo, so this is null when the record has no cover or gallery photo, or is unavailable."},"logoUrl":{"type":["string","null"],"description":"The selected logo of the item's organization (organizationId): an organization's own, an activity's organization's. Never a photo; null when it has none or the item is unavailable. Once rendered it is the logo's public, cross-origin rendition (GET /api/media/logos/{file}), else its source URL."},"available":{"type":"boolean","description":"False once the record is archived, removed or hidden from the catalog. Only the owner of a list sees unavailable items."},"savedAt":{"type":["string","null"],"format":"date-time","description":"When the viewer saved it; null when not in Saved or signed out."}},"additionalProperties":true},"FamilySocialCollectionOwner":{"oneOf":[{"type":"object","required":["kind"],"properties":{"kind":{"const":"you"}},"additionalProperties":false},{"type":"object","required":["kind","userId","publicName","publicNameKnown","photoUrl"],"properties":{"kind":{"const":"person"},"userId":{"type":"string","format":"uuid"},"publicName":{"type":["string","null"]},"publicNameKnown":{"type":"boolean","description":"False when the parent has no name to show; publicName is then null and the client writes its own wording."},"photoUrl":{"type":["string","null"],"description":"The photo the parent uploaded (an API path carrying its hash), else their Google picture (an absolute URL); null when there is neither."}},"additionalProperties":false},{"type":"object","required":["kind"],"properties":{"kind":{"const":"schoolsOut"}},"additionalProperties":false},{"type":"object","required":["kind","organizationId","name","logoUrl"],"properties":{"kind":{"const":"organization"},"organizationId":{"type":"string","format":"uuid"},"name":{"type":"string"},"logoUrl":{"type":["string","null"],"description":"The organization's selected logo; null when it has none. Once rendered it is the logo's public, cross-origin rendition (GET /api/media/logos/{file}), else its source URL."}},"additionalProperties":false}]},"FamilySocialCollectionCover":{"type":"object","required":["style","imageUrl","tone"],"properties":{"style":{"enum":["still-life","gradient","taped-print"]},"imageUrl":{"type":"string","description":"An https URL or a path on this API."},"tone":{"type":["string","null"],"enum":["purple","turquoise","orange","blue",null]}},"additionalProperties":false},"FamilySocialCollectionSummary":{"type":"object","required":["id","name","owner","visibility","verified","cover","counts","previewImages","following","hidden","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"owner":{"$ref":"#/components/schemas/FamilySocialCollectionOwner"},"visibility":{"enum":["private","public"]},"verified":{"type":"boolean","description":"Curated by School's Out or an organization."},"cover":{"oneOf":[{"$ref":"#/components/schemas/FamilySocialCollectionCover"},{"type":"null"}]},"counts":{"type":"object","required":["activities","organizations"],"properties":{"activities":{"type":"integer","minimum":0},"organizations":{"type":"integer","minimum":0}},"description":"Available items only.","additionalProperties":false},"previewImages":{"description":"The first photos of the newest available items that have one. A logo is never a preview.","type":"array","maxItems":4,"items":{"type":"string"}},"following":{"type":"boolean","description":"The viewer follows it; always false for the viewer's own."},"hidden":{"type":"boolean","description":"A collection you follow that you can no longer open, because its owner made it private or it was archived. It keeps its id, name and owner so you can see it and unfollow; it reads as private with no cover, zero counts, no preview images and no items, and its updatedAt is when you followed it."},"updatedAt":{"type":"string","format":"date-time"}},"additionalProperties":true},"FamilySocialCollectionDetail":{"allOf":[{"$ref":"#/components/schemas/FamilySocialCollectionSummary"},{"type":"object","required":["items","nextCursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/FamilySocialItemSummary"},"description":"Newest first."},"nextCursor":{"type":["string","null"],"description":"Opaque; pass as cursor for the next page. Null on the last page."}}}]},"FamilySocialFeaturedCollection":{"allOf":[{"$ref":"#/components/schemas/FamilySocialCollectionSummary"},{"type":"object","required":["items"],"properties":{"items":{"type":"array","maxItems":12,"items":{"$ref":"#/components/schemas/FamilySocialItemSummary"},"description":"The first available items, newest first."}}}]},"FamilySocialRestore":{"type":"object","required":["savedAt","collectionIds"],"properties":{"savedAt":{"type":"string","format":"date-time"},"collectionIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"description":"What Undo sends back: savedAt to PUT saved/{itemKey}, collectionIds to PUT saved/{itemKey}/collections.","additionalProperties":false},"FamilySocialSavedItem":{"allOf":[{"$ref":"#/components/schemas/FamilySocialItemSummary"},{"type":"object","required":["collectionIds"],"properties":{"collectionIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"The viewer's own collections that hold the item, by ID; empty when none does."}}}]},"FamilySocialTrackedRecord":{"type":"object","required":["id","targetKind","targetId","created_at","available","alerts","name","city","region","organizationId","organizationName","activityId","activityName","startDate","endDate","startTime","endTime","scheduleConfigurations"],"properties":{"id":{"type":"string","format":"uuid"},"targetKind":{"enum":["organization","activity","session"]},"targetId":{"type":"string","format":"uuid"},"created_at":{"type":"string","description":"When the account started following."},"available":{"type":"boolean"},"alerts":{"type":"array","items":{"enum":["registration_opens","spot_opens","dates_posted","updates"]}},"name":{"type":["string","null"]},"city":{"type":["string","null"]},"region":{"type":["string","null"]},"organizationId":{"type":["string","null"]},"organizationName":{"type":["string","null"]},"activityId":{"type":["string","null"]},"activityName":{"type":["string","null"]},"startDate":{"type":["string","null"]},"endDate":{"type":["string","null"]},"startTime":{"type":["string","null"]},"endTime":{"type":["string","null"]},"scheduleConfigurations":{"type":["array","null"],"items":{}}},"additionalProperties":false},"FamilySocialPrivacy":{"type":"object","required":["sharePlans","childProfilesVisible","dataSharing"],"properties":{"sharePlans":{"type":"boolean","description":"Friends see what the family is going to."},"childProfilesVisible":{"type":"boolean","description":"Children's first names show on shared plans."},"dataSharing":{"type":"boolean","description":"Aggregate demand signals may use the family's activity."}},"additionalProperties":false},"FamilySocialProfile":{"type":"object","required":["publicName","publicNameKnown","photoUrl","privacy","version"],"properties":{"publicName":{"type":["string","null"],"description":"First name and last initial of the account's name (its legacy profile first and last name), else the preferred name, else the username; null when the account has none."},"publicNameKnown":{"type":"boolean","description":"False when the parent has no name to show; publicName is then null and the client writes its own wording."},"photoUrl":{"type":["string","null"],"description":"The photo the parent uploaded (an API path carrying its hash), else their Google picture (an absolute URL); null when there is neither."},"privacy":{"$ref":"#/components/schemas/FamilySocialPrivacy"},"version":{"type":"integer","minimum":0,"description":"0 until the first write."}},"additionalProperties":false},"FamilySocialRelation":{"enum":["none","requested","incoming","friends","blocked"]},"FamilySocialFriend":{"type":"object","required":["userId","publicName","publicNameKnown","photoUrl","relation","since"],"properties":{"userId":{"type":"string","format":"uuid"},"publicName":{"type":["string","null"]},"publicNameKnown":{"type":"boolean","description":"False when the parent has no name to show; publicName is then null and the client writes its own wording."},"photoUrl":{"type":["string","null"],"description":"The photo the parent uploaded (an API path carrying its hash), else their Google picture (an absolute URL); null when there is neither."},"relation":{"$ref":"#/components/schemas/FamilySocialRelation"},"since":{"type":"string","format":"date-time"}},"additionalProperties":false},"FamilySocialGoing":{"type":"object","required":["childFirstName","item","sessionId","startsOn"],"properties":{"childFirstName":{"type":["string","null"],"description":"Null unless the friend shows children's names."},"item":{"$ref":"#/components/schemas/FamilySocialItemSummary"},"sessionId":{"type":["string","null"],"format":"uuid"},"startsOn":{"type":["string","null"],"format":"date"}},"additionalProperties":false},"FamilySocialPerson":{"type":"object","required":["userId","publicName","publicNameKnown","photoUrl","relation","reported","collections","going"],"properties":{"userId":{"type":"string","format":"uuid"},"publicName":{"type":["string","null"]},"publicNameKnown":{"type":"boolean","description":"False when the parent has no name to show; publicName is then null and the client writes its own wording."},"photoUrl":{"type":["string","null"],"description":"The photo the parent uploaded (an API path carrying its hash), else their Google picture (an absolute URL); null when there is neither."},"relation":{"$ref":"#/components/schemas/FamilySocialRelation"},"reported":{"type":"boolean","description":"You have reported this parent."},"collections":{"type":"array","items":{"$ref":"#/components/schemas/FamilySocialCollectionSummary"},"description":"Their public collections."},"going":{"type":["array","null"],"items":{"$ref":"#/components/schemas/FamilySocialGoing"},"description":"Null unless you are friends and they share plans."}},"additionalProperties":false},"FamilySocialFriendsGoing":{"type":"object","required":["organizationId","activityId","sessionId","friends"],"properties":{"organizationId":{"type":"string","format":"uuid","description":"The business of the sign-ups; the asked ID when asked by organizationIds."},"activityId":{"type":["string","null"],"format":"uuid","description":"Set when asked by activityIds, and by sessionIds for a session that belongs to an activity; null when asked by organizationIds."},"sessionId":{"type":["string","null"],"format":"uuid","description":"Set when asked by sessionIds."},"friends":{"type":"array","items":{"type":"object","required":["userId","publicName","publicNameKnown","photoUrl","childFirstName"],"properties":{"userId":{"type":"string","format":"uuid"},"publicName":{"type":["string","null"]},"publicNameKnown":{"type":"boolean","description":"False when the parent has no name to show; publicName is then null and the client writes its own wording."},"photoUrl":{"type":["string","null"],"description":"The photo the parent uploaded (an API path carrying its hash), else their Google picture (an absolute URL); null when there is neither."},"childFirstName":{"type":["string","null"]}},"additionalProperties":false}}},"additionalProperties":false,"description":"The sharing friends who booked one asked record (a booked signal: friends who booked here, not the feed's still-running friend stories). A sign-up counts while its session is upcoming or ongoing, and for 365 days after its last day (the viewer's household dates), so a business's who-booked line covers past and upcoming bookings."},"FamilyApprovalSource":{"additionalProperties":false,"description":"The authenticated viewer is always the approving actor. Optional source {type:\"manual\"} or {type:\"assistant\", conversationId, turnId} names one of the viewer's own completed Assistant turns that proposed the change; the write itself is the explicit approval.","properties":{"conversationId":{"format":"uuid","type":"string"},"turnId":{"format":"uuid","type":"string"},"type":{"enum":["manual","assistant"]}},"required":["type"],"type":"object"},"FamilyArea":{"additionalProperties":false,"properties":{"label":{"maxLength":120,"minLength":1,"type":"string"},"postalCode":{"pattern":"^[0-9]{5}$","type":"string"},"region":{"pattern":"^[A-Z]{2}$","type":"string"}},"required":["label"],"type":"object"},"FamilyBudget":{"additionalProperties":false,"properties":{"amount":{"description":"At most two decimal places.","maximum":1000000,"minimum":0,"type":"number"},"currency":{"pattern":"^[A-Z]{3}$","type":"string"},"limit":{"enum":["hard_cap","preference"]},"period":{"enum":["day","week","month","session","season","year","total"]},"scope":{"enum":["per_child","household"]}},"required":["amount","currency","period","scope","limit"],"type":"object"},"FamilyChildFields":{"additionalProperties":false,"description":"Optional registration fields. A planning label can identify a profile without full names; enabling registration reuse requires both child.firstName and child.lastName.","properties":{"child.allergies":{"maxLength":4000,"type":"string"},"child.birthDate":{"maxLength":10,"type":"string"},"child.firstName":{"maxLength":255,"type":"string"},"child.grade":{"maxLength":100,"type":"string"},"child.lastName":{"maxLength":255,"type":"string"},"child.notes":{"maxLength":4000,"type":"string"},"emergencyContact.name":{"maxLength":255,"type":"string"},"emergencyContact.phone":{"maxLength":64,"type":"string"},"profile.bio":{"maxLength":500,"type":"string"},"profile.color":{"description":"The child's accent: a design-system accent name such as purple or turquoise, never a raw color. The site owns the palette.","maxLength":32,"pattern":"^[a-z]+(-[a-z]+)*$","type":"string"},"profile.goals":{"maxLength":300,"type":"string"},"profile.location":{"description":"Where the child is, as the parent names it (a place label).","maxLength":120,"type":"string"},"profile.schedule":{"maxLength":300,"type":"string"}},"type":"object"},"FamilyChildInput":{"additionalProperties":false,"description":"A profile needs no name: a child may be saved before it is named, and readers then call it by its label, else its first name, else Child N. Registration reuse requires both names. PUT replaces fields and preferences; when planning is omitted, existing planning values are retained.","properties":{"fields":{"$ref":"#/components/schemas/FamilyChildFields"},"planning":{"$ref":"#/components/schemas/FamilyChildPlanning"},"preferences":{"$ref":"#/components/schemas/FamilyChildPreferences"},"reuseAllowed":{"default":false,"type":"boolean"},"source":{"$ref":"#/components/schemas/FamilyApprovalSource"},"version":{"minimum":1,"type":"integer"}},"required":["fields"],"type":"object"},"FamilyChildPlanning":{"additionalProperties":false,"description":"Enduring planning details registration forms do not use. A label alone can start a profile; registration reuse still needs first and last names. A date of birth always wins over an age: a save that gives both keeps the date of birth, and a date of birth replaces a saved age. Nothing is inferred.","properties":{"age":{"additionalProperties":false,"properties":{"asOf":{"format":"date","type":"string"},"years":{"maximum":21,"minimum":0,"type":"integer"}},"required":["years","asOf"],"type":"object","deprecated":true,"description":"Deprecated: send fields[\"child.birthDate\"] instead. Still accepted for older clients; ignored when a date of birth is given, and a profile with a date of birth has no age."},"gradeSchoolYear":{"description":"The school year fields[\"child.grade\"] applies to, such as 2026-2027.","pattern":"^[0-9]{4}-[0-9]{4}$","type":"string"},"label":{"maxLength":60,"type":"string"},"requirements":{"items":{"additionalProperties":false,"properties":{"category":{"enum":["health_safety","dietary","accessibility","supervision","communication","social","other"]},"detail":{"description":"A practical parent-stated condition; no diagnosis is needed.","maxLength":300,"minLength":1,"type":"string"},"kind":{"enum":["required","preference"]}},"required":["kind","category","detail"],"type":"object"},"maxItems":20,"type":"array"},"requirementsStatus":{"enum":["unknown","none","listed"]}},"type":"object"},"FamilyChildPreferences":{"additionalProperties":false,"properties":{"interests":{"items":{"maxLength":100,"type":"string"},"maxItems":20,"type":"array"},"interestsStatus":{"description":"unknown means the parent has not answered; none means no interests; listed requires interests. Omitted, it is listed when interests are present and otherwise unknown.","enum":["unknown","none","listed"]},"notes":{"maxLength":2000,"type":"string"}},"type":"object"},"FamilyChildProfile":{"properties":{"createdAt":{"format":"date-time","type":"string"},"fields":{"$ref":"#/components/schemas/FamilyChildFields"},"id":{"format":"uuid","type":"string"},"photoUrl":{"description":"The child's photo (GET /api/users/viewer/children/{childId}/photo?v=\u003chash\u003e), served only to the child's owner and guardians; null when none. The URL changes when the photo does.","type":["string","null"]},"planning":{"$ref":"#/components/schemas/FamilyChildPlanning"},"preferences":{"$ref":"#/components/schemas/FamilyChildPreferences"},"provenance":{"additionalProperties":{"$ref":"#/components/schemas/FamilyProvenance"},"description":"Current-value approval for saved registration fields, preferences and planning values. Removed values leave no entry; unchanged values keep their provenance. Another adult's conversation reference is omitted.","type":"object"},"reuseAllowed":{"type":"boolean"},"role":{"description":"owner for the family's owner; guardian for a member (the per-child routes' word). familyRole says which member.","enum":["owner","guardian"],"type":"string"},"updatedAt":{"format":"date-time","type":"string"},"version":{"minimum":1,"type":"integer"},"familyRole":{"$ref":"#/components/schemas/FamilyRole"}},"required":["id","version","fields","preferences","reuseAllowed","provenance","createdAt","updatedAt","role","planning","photoUrl","familyRole"],"type":"object","description":"A caregiver sees the care view: child.firstName, child.lastName, child.allergies, child.notes, emergencyContact.name, emergencyContact.phone and profile.color, the planning label and requirements, the photo; no preferences or provenance."},"FamilyCorrection":{"additionalProperties":false,"description":"Narrow correction: each named key replaces its value and null removes it. Unnamed values and their approvals are unchanged. The merged record is validated like a complete submission. The authenticated viewer is always the approving actor. Optional source {type:\"manual\"} or {type:\"assistant\", conversationId, turnId} names one of the viewer's own completed Assistant turns that proposed the change; the write itself is the explicit approval.","properties":{"changes":{"minProperties":1,"type":"object"},"source":{"$ref":"#/components/schemas/FamilyApprovalSource"},"version":{"minimum":0,"type":"integer"}},"required":["version","changes"],"type":"object"},"FamilyHousehold":{"properties":{"preferences":{"$ref":"#/components/schemas/FamilyHouseholdPreferences"},"provenance":{"additionalProperties":{"$ref":"#/components/schemas/FamilyProvenance"},"type":"object"},"updatedAt":{"format":"date-time","type":["string","null"]},"version":{"description":"0 when nothing is saved.","type":"integer"},"referral":{"$ref":"#/components/schemas/FamilyAccountHouseholdReferral"}},"required":["version","preferences","provenance","updatedAt","referral"],"type":"object"},"FamilyHouseholdPreferences":{"additionalProperties":false,"properties":{"budget":{"$ref":"#/components/schemas/FamilyBudget"},"caregiverAvailability":{"additionalProperties":false,"description":"Recurring windows when a caregiver is available, date exceptions that replace them, and the times a caregiver can drop off or pick up.","properties":{"dropoff":{"$ref":"#/components/schemas/FamilyTimeRange"},"exceptions":{"items":{"additionalProperties":false,"properties":{"available":{"type":"boolean"},"date":{"format":"date","type":"string"},"hours":{"$ref":"#/components/schemas/FamilyTimeRange"}},"required":["date","available"],"type":"object"},"maxItems":60,"type":"array"},"pickup":{"$ref":"#/components/schemas/FamilyTimeRange"},"windows":{"items":{"additionalProperties":false,"properties":{"days":{"items":{"enum":["mon","tue","wed","thu","fri","sat","sun"]},"minItems":1,"type":"array"},"end":{"type":"string"},"start":{"type":"string"}},"required":["days","start","end"],"type":"object"},"maxItems":14,"type":"array"}},"type":"object"},"emergencyContacts":{"description":"Every distinct emergency contact of the family, including those its children point at. Index 0 is the primary contact; the rest are backups. Reorder by sending the whole list; a contact left out is removed, and children pointing at it lose their emergency contact.","items":{"$ref":"#/components/schemas/FamilyAccountEmergencyContact"},"maxItems":20,"type":"array"},"planningIntent":{"$ref":"#/components/schemas/FamilyAccountPlanningIntent"},"searchArea":{"$ref":"#/components/schemas/FamilyAccountSearchArea"},"timezone":{"description":"IANA time zone, such as America/Los_Angeles.","type":"string"},"travel":{"$ref":"#/components/schemas/FamilyTravel"},"home":{"$ref":"#/components/schemas/FamilyAccountLocation"},"places":{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/FamilyAccountSavedPlace"},"description":"The household's saved places, in the parent's order. PATCH sends the whole list: an entry with an id is that place, one with a placeId is new, and a place left out is removed."}},"type":"object"},"FamilyProvenance":{"description":"Approval of the current value only. Another adult's sourceReference is never returned; removed values leave no entry.","properties":{"actorId":{"format":"uuid","type":"string"},"approvedAt":{"format":"date-time","type":"string"},"source":{"enum":["manual","assistant"]},"sourceReference":{"properties":{"conversationId":{"format":"uuid","type":"string"},"turnId":{"format":"uuid","type":"string"}},"type":"object"},"version":{"description":"Record version at which this value was approved or reconfirmed.","type":"integer"}},"required":["source","actorId","approvedAt"],"type":"object"},"FamilyReconfirmation":{"additionalProperties":false,"description":"Records a fresh approval of unchanged current values and advances the record version. A key without a saved value returns 409.","properties":{"keys":{"items":{"type":"string"},"maxItems":40,"minItems":1,"type":"array"},"source":{"$ref":"#/components/schemas/FamilyApprovalSource"},"version":{"minimum":1,"type":"integer"}},"required":["version","keys"],"type":"object"},"FamilyTimeRange":{"additionalProperties":false,"properties":{"end":{"pattern":"^[0-2][0-9]:[0-5][0-9]$","type":"string"},"start":{"pattern":"^[0-2][0-9]:[0-5][0-9]$","type":"string"}},"required":["start","end"],"type":"object"},"FamilyTravel":{"additionalProperties":false,"description":"Needs maxMinutes or maxDistance. Without origin, travel is measured from the search area.","properties":{"limit":{"enum":["hard_cap","preference"]},"maxDistance":{"additionalProperties":false,"properties":{"unit":{"enum":["mi","km"]},"value":{"exclusiveMinimum":0,"maximum":500,"type":"number"}},"required":["value","unit"],"type":"object"},"maxMinutes":{"maximum":240,"minimum":1,"type":"integer"},"mode":{"enum":["driving","transit","walking","cycling"]},"origin":{"$ref":"#/components/schemas/FamilyArea"}},"required":["mode","limit"],"type":"object"},"BookingError":{"type":"object","required":["error","message"],"properties":{"error":{"const":true},"message":{"type":"string","description":"A sentence you can show the parent as is."},"data":{"type":"null"}}},"BookingMoney":{"type":"object","required":["amountCents","currency"],"properties":{"amountCents":{"type":"integer","description":"Amount in the smallest currency unit, such as cents."},"currency":{"type":"string","pattern":"^[A-Z]{3}$","description":"ISO 4217 currency code."}},"example":{"amountCents":45000,"currency":"USD"}},"BookingStatus":{"type":"string","enum":["authorizing","prepared","submitting","submit_uncertain","accepted","running","awaiting_decision","applied","confirmed","waitlisted","failed","cancelled","uncertain"],"description":"The step the booking is at, for support and diagnostics. Apps decide what to show from `phase`; each status belongs to exactly one phase, and a new status may be added to an existing phase without notice.","x-open-enum":true},"BookingPaymentStatus":{"type":"string","enum":["none","hold_pending","held","captured","released","failed"],"description":"The state of the hold on the parent’s card. `hold_pending` means a hold attempt’s outcome is unknown; it’s retried with the same idempotency key."},"BookingShareableField":{"type":"string","enum":["child.birthDate","child.grade","child.allergies","guardian.phone"],"description":"Optional fields the parent can choose to share. The child’s first and last name and the guardian’s name and email are always shared. child.* fields come from the child’s profile; guardian.phone is the contact phone on the account of the adult booking (the owner or a co-parent), the phone their profile shows; without one, sharing it is refused."},"BookingAlwaysSharedField":{"type":"string","enum":["child.firstName","child.lastName","guardian.email","guardian.name"],"description":"Fields every booking shares: the child's names from their profile, and the guardian's name and email from the account of the adult booking (the owner or a co-parent), never the child's profile. A booking cannot be made without them."},"FamilyBookingForm":{"type":"object","additionalProperties":false,"required":["sessionId","alwaysShared","shareFields"],"description":"Profile-sharing choices used by family booking approval. These remain uniform across sessions; the canonical session booking_fields describes provider requirements separately and does not authorize sharing or execution.","properties":{"sessionId":{"type":"string","format":"uuid"},"alwaysShared":{"type":"array","items":{"$ref":"#/components/schemas/BookingAlwaysSharedField"}},"shareFields":{"type":"array","items":{"$ref":"#/components/schemas/BookingShareableField"}}}},"BookingApprovalInput":{"type":"object","additionalProperties":false,"required":["idempotencyKey","childProfileId","childProfileVersion","sessionId","maxSpend","allowWaitlist","autoAcceptDecisions","shareFields","answers","approveBooking"],"properties":{"idempotencyKey":{"type":"string","format":"uuid","description":"Generate one per approval form. Resending the identical body returns the same booking with `200`; reusing the key with a different body returns `409`."},"childProfileId":{"type":"string","format":"uuid"},"childProfileVersion":{"type":"integer","minimum":1,"description":"The profile version the parent reviewed. If the profile has changed since, the request returns `409`."},"sessionId":{"type":"string","format":"uuid","description":"The catalog session to book."},"maxSpend":{"$ref":"#/components/schemas/BookingMoney","description":"The camp's spending limit, including the camp's own fees and tax, in the session’s currency and at most 2,500,000 cents. School’s Out’s service fee (`payment.serviceFeeCents`, from `GET /api/users/viewer/booking-policy`) is added on top: the hold on the parent’s card is this limit plus the fee, less `useCreditsCents`."},"allowWaitlist":{"type":"boolean","description":"Accept a waitlist place if the session is full."},"autoAcceptDecisions":{"type":"boolean","description":"Let School’s Out answer routine camp questions and accept session changes within the spending limit. It never raises spending above the limit, signs a waiver, or creates a camp account."},"shareFields":{"type":"array","items":{"$ref":"#/components/schemas/BookingShareableField"},"uniqueItems":true},"answers":{"type":"array","maxItems":50,"items":{"type":"object","additionalProperties":false,"required":["key","label","value"],"properties":{"key":{"type":"string","maxLength":100},"label":{"type":"string","maxLength":500},"value":{"type":["string","boolean","number"]}}}},"approveBooking":{"const":true,"description":"The parent’s explicit approval. Any other value returns `400`."},"useCreditsCents":{"type":"integer","minimum":1,"description":"Optional. Wallet credits to put toward the booking, at most the spending limit plus the service fee, and the wallet balance (`409` otherwise); USD bookings only. They are held at approval, reduce the card hold by the same amount, are applied before the card at capture, and are returned if the booking gets no place."},"paymentMethod":{"type":"string","enum":["default","backup"],"description":"Optional. Which of the parent’s saved cards to hold; defaults to the default card."},"reuseAllowed":{"const":true,"description":"Optional. The parent's consent, given with this approval, to reuse the child's saved details for registrations; it is saved on the child as `reuseAllowed` (the profile's owner only, `403` otherwise). Without it, a child whose profile does not allow reuse yet returns `409`. Once the profile allows reuse, approvals need not send it."},"profileFields":{"$ref":"#/components/schemas/FamilyChildFields","description":"Optional. Child details the parent chose to save to the child's profile with this booking (\"Save new details to profile\"). They are saved as `PATCH .../children/{childId}` saves `changes`, in the booking's transaction and from `childProfileVersion`, before the booking reads the profile; the booking records the new version. Details the parent does not save go in `answers`."}},"example":{"idempotencyKey":"0b7c3f52-9a41-4e2d-8f6a-3c1d5e7b9a20","childProfileId":"1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93","childProfileVersion":4,"sessionId":"c3a9e1f2-6d4b-4a8c-b2e7-0f5d9c1a3b48","maxSpend":{"amountCents":45000,"currency":"USD"},"allowWaitlist":true,"autoAcceptDecisions":false,"shareFields":["child.birthDate"],"answers":[{"key":"photo_consent","label":"Photo consent","value":false}],"approveBooking":true}},"BookingDecisionOption":{"type":"object","required":["optionId","label","total"],"properties":{"optionId":{"type":"string"},"label":{"type":"string"},"total":{"oneOf":[{"$ref":"#/components/schemas/BookingMoney"},{"type":"null"}],"description":"The camp’s total if the parent chooses this option."}}},"BookingPendingDecision":{"type":"object","required":["decisionId","code","message","options","proposedOptionId","expiresAt","screenshot"],"properties":{"decisionId":{"type":"string","format":"uuid"},"code":{"type":"string","enum":["unknown_question","choose_option","confirm_total","session_changed","waiver_signature","provider_account_required","unverified_step","answer_required","consent_required","registration_workflow_required"],"x-open-enum":true},"message":{"type":"string"},"options":{"type":"array","items":{"$ref":"#/components/schemas/BookingDecisionOption"}},"proposedOptionId":{"type":["string","null"]},"expiresAt":{"type":"string","format":"date-time","description":"When the camp’s checkout hold ends. After this, the booking fails with `decision_expired`."},"screenshot":{"description":"The camp’s page as Hallpass last saw it, for an `unverified_step` decision; null otherwise.","oneOf":[{"type":"null"},{"type":"object","additionalProperties":false,"required":["mediaType","data","width","height","capturedAt"],"properties":{"mediaType":{"type":"string","enum":["image/jpeg","image/png"]},"data":{"type":"string","description":"Base64 of the image."},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1},"capturedAt":{"type":"string","format":"date-time"}}}]}}},"BookingDecisionRecord":{"type":"object","required":["decisionId","code","outcome","optionId","decidedBy","decidedAt"],"properties":{"decisionId":{"type":"string","format":"uuid"},"code":{"type":"string"},"outcome":{"type":"string","enum":["accepted","declined","expired"],"x-open-enum":true},"optionId":{"type":["string","null"]},"decidedBy":{"type":["string","null"],"enum":["parent","auto",null]},"decidedAt":{"type":"string","format":"date-time","description":"When the question was answered."}}},"BookingReceipt":{"type":"object","required":["providerReference","providerStatus","amount","confirmedAt","source"],"properties":{"providerReference":{"type":"string","description":"The camp’s confirmation or order number."},"providerStatus":{"type":"string","enum":["enrolled","waitlisted","application_received"],"x-open-enum":true,"description":"`application_received` on an applied booking."},"amount":{"oneOf":[{"$ref":"#/components/schemas/BookingMoney"},{"type":"null"}],"description":"What the parent was charged. Present only when there was a charge."},"confirmedAt":{"type":"string","format":"date-time","description":"When the camp confirmed the booking."},"source":{"type":"string","enum":["hallpass","parent"],"description":"`hallpass`: a provider receipt School’s Out verified. `parent`: the parent reported that an application became a registration; `amount` is then `null`."}}},"BookingFailure":{"type":"object","required":["code","message","retryable"],"properties":{"code":{"$ref":"#/components/schemas/BookingFailureCode"},"message":{"type":"string"},"retryable":{"type":"boolean","description":"Whether a new, re-approved booking might succeed."}}},"BookingFailureCode":{"type":"string","enum":["application_declined","blocked_by_provider","booking_expired","booking_rejected","deadline_exceeded","decision_declined","decision_expired","eligibility_mismatch","needs_person","not_available","payment_authentication_required","payment_declined","payment_hold_unresolved","payment_method_required","price_above_approval","provider_unavailable","registration_closed","sold_out","stopped_before_payment","unsupported_provider"],"description":"Why a booking didn't happen: Hallpass's camp booking codes (sold_out, price_above_approval, decision_expired, ...) and School's Out's own (payment_declined, payment_method_required, payment_hold_unresolved, booking_expired, booking_rejected, application_declined, ...). Apps map the codes they know and show the rest by `message`; a new code may be added without notice.","x-open-enum":true},"BookingPhase":{"type":"string","enum":["in_progress","needs_parent","applied","booked","waitlisted","not_booked","checking"],"description":"What the booking means to the parent, and which payload it carries. `in_progress`: School’s Out is booking it. `needs_parent`: the camp asked a question only the parent can answer; `decision` carries it. `applied`: the camp received an application and answers later; `receipt` carries it, nothing is charged, and the parent reports the answer with the outcome route. `booked`: the camp enrolled the child; `receipt` carries the confirmation (`receipt.source` is `parent` when the parent reported it). `waitlisted`: the child holds a waitlist place; `receipt` carries it. `not_booked`: the booking ended without a place; `failure` explains why, or is `null` when it was cancelled. `checking`: the outcome isn’t known yet; `checkingWith` says whether School’s Out is resolving the card hold or confirming with the camp. Never show `checking` as booked or failed. Adding a phase is a breaking change."},"FamilyBookingCommon":{"type":"object","description":"The fields every booking carries, whatever its phase.","required":["id","phase","status","paymentStatus","childProfileId","organization","activity","session","approval","progressStage","decisions","pollAfterMs","createdAt","updatedAt","waitlist","canceledBy","refund","dueOnProviderSite","note","askAgainAfter","replacesBookingId","replacedByBookingId","payment","cancellation","bookedBy","hold"],"properties":{"id":{"type":"string","format":"uuid"},"phase":{"$ref":"#/components/schemas/BookingPhase"},"status":{"$ref":"#/components/schemas/BookingStatus"},"paymentStatus":{"$ref":"#/components/schemas/BookingPaymentStatus"},"childProfileId":{"type":["string","null"],"format":"uuid","description":"`null` after the profile is deleted. The booking itself remains."},"organization":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}},"activity":{"anyOf":[{"type":"object","required":["id","name"],"additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]}}},{"type":"null"}],"description":"The booked session's catalog activity, as it was at approval like organization and session; null when the session had none. A change of session stays within it."},"session":{"type":"object","required":["id","name","startDate","endDate","startTime","endTime","dates","dayTimes"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"startDate":{"type":["string","null"],"format":"date","description":"First day of the session."},"endDate":{"type":["string","null"],"format":"date","description":"Last day of the session."},"startTime":{"type":["string","null"],"pattern":"^[0-9]{2}:[0-9]{2}$","description":"Daily start time (HH:MM) when the session has one; snapshotted at approval."},"endTime":{"type":["string","null"],"pattern":"^[0-9]{2}:[0-9]{2}$","description":"Daily end time (HH:MM) when the session has one."},"dates":{"type":"array","maxItems":60,"items":{"type":"string","format":"date"},"description":"Published attendance dates, at most 60; empty when the camp published only a date range."},"dayTimes":{"type":"array","maxItems":60,"items":{"$ref":"#/components/schemas/FamilyPlanDayTime"},"description":"The days whose own times replace startTime and endTime, by date, snapshotted at approval: a day with different hours, or an overnight stay's drop-off (first day, startTime only) and pick-up (last day, endTime only) with neither between. Empty when every day keeps startTime and endTime."}}},"approval":{"type":"object","required":["approvedAt","maxSpend","allowWaitlist","autoAcceptDecisions","sharedFields"],"properties":{"approvedAt":{"type":"string","format":"date-time","description":"When the parent approved the booking."},"maxSpend":{"$ref":"#/components/schemas/BookingMoney"},"allowWaitlist":{"type":"boolean"},"autoAcceptDecisions":{"type":"boolean"},"sharedFields":{"type":"array","items":{"$ref":"#/components/schemas/BookingShareableField"},"description":"The optional fields the parent chose to share. Always-shared fields aren’t listed."}}},"progressStage":{"type":["string","null"],"enum":["resolving_session","filling_form","reviewing_total","paying","confirming",null],"x-open-enum":true,"description":"The furthest checkout stage Hallpass reported, or `null` before it reports one. New stages may be added."},"decisions":{"type":"array","items":{"$ref":"#/components/schemas/BookingDecisionRecord"}},"pollAfterMs":{"type":["integer","null"],"description":"How long to wait before reading the booking again. `null` once nothing will change without the parent."},"createdAt":{"type":"string","format":"date-time","description":"When the booking was created."},"updatedAt":{"type":"string","format":"date-time","description":"When the booking last changed."},"waitlist":{"oneOf":[{"type":"object","required":["position","updatedAt"],"properties":{"position":{"type":["integer","null"],"minimum":1,"description":"The place the camp reported, 1 is next; `null` when it gave none."},"updatedAt":{"type":"string","format":"date-time"}}},{"type":"null"}]},"canceledBy":{"type":["string","null"],"enum":["parent","provider","system",null],"description":"Who cancelled a cancelled booking."},"refund":{"oneOf":[{"type":"object","required":["method","amountCents","walletCents","status"],"properties":{"method":{"type":"string","enum":["credits","card"]},"amountCents":{"type":"integer","minimum":1,"description":"Everything the family paid, refunded."},"walletCents":{"type":"integer","minimum":0,"description":"The part returned to the wallet at once: all of it for `credits`, the credits part for `card`."},"status":{"type":"string","enum":["pending","done"],"description":"`pending` until the card refund is confirmed."}}},{"type":"null"}]},"dueOnProviderSite":{"oneOf":[{"type":"object","required":["label","url"],"properties":{"label":{"type":"string"},"url":{"type":"string","format":"uri"}},"description":"Something the parent must still do on the camp’s site; cleared by the outcome `provider_task_done`."},{"type":"null"}]},"note":{"type":["string","null"],"maxLength":1000,"description":"What the family should know for this booking, such as what to bring, from the provider’s email. An undone email update puts back the note it replaced."},"askAgainAfter":{"type":["string","null"],"format":"date-time","description":"Don’t ask about this application’s outcome again before this time."},"replacesBookingId":{"type":["string","null"],"format":"uuid","description":"The booking this one changes to another session."},"replacedByBookingId":{"type":["string","null"],"format":"uuid","description":"The live booking changing this one. This booking stays until that one is booked."},"payment":{"type":"object","required":["paymentMethod","creditsCents","cardHoldCents","serviceFeeCents"],"properties":{"paymentMethod":{"type":"string","enum":["default","backup"]},"creditsCents":{"type":"integer","minimum":0,"description":"Wallet credits held at approval; they pay the charge before the card."},"cardHoldCents":{"type":"integer","minimum":0,"description":"What is pending on the parent's card while it is held (paymentStatus hold_pending or held), else 0: the spending limit plus serviceFeeCents, less creditsCents, and at least Stripe's minimum charge ($0.50) when anything is left for the card. No hold is placed when the credits cover the whole of it. Capture charges only what the receipt and the service fee leave after the credits."},"serviceFeeCents":{"type":"integer","minimum":0,"description":"School’s Out’s service fee for this booking (one child), fixed at approval from the booking policy. It is charged, with the camp’s price, when the booking is booked or applied (an application costs only this fee); until then the card hold covers it. 0 for a booking that ended without it: waitlisted, or not booked. A refund after the camp cancels, or after a change, returns it."}},"additionalProperties":false},"cancellation":{"$ref":"#/components/schemas/FamilyBookingCancellation"},"bookedBy":{"anyOf":[{"type":"object","required":["userId","name"],"properties":{"userId":{"type":"string","format":"uuid"},"name":{"type":["string","null"]}},"additionalProperties":false},{"type":"null"}],"description":"The family member (co-parent) who approved the booking; null when the family's owner did. The booking, its card hold and its payments are the owner's."},"hold":{"description":"Why a running run is waiting on something nobody can answer: Pilgrim holding an earlier task for a person's review, Pilgrim's assistance switched off at its Mac, or Hallpass retrying its worker. `null` while the run moves on its own.","oneOf":[{"type":"null"},{"type":"object","additionalProperties":false,"required":["code","message","since"],"properties":{"code":{"type":"string","enum":["pilgrim_queue_held","pilgrim_assistance_off","executor_retrying","pilgrim_busy"],"x-open-enum":true},"message":{"type":"string","minLength":1,"maxLength":500,"description":"What the run waits for, written for the requester."},"since":{"type":"string","format":"date-time"}}}]}}},"FamilyBookingInProgress":{"description":"School’s Out is placing the card hold or booking with the camp.","allOf":[{"$ref":"#/components/schemas/FamilyBookingCommon"},{"type":"object","required":["phase","status","decision","receipt","failure","checkingWith"],"properties":{"phase":{"type":"string","const":"in_progress"},"status":{"type":"string","enum":["authorizing","prepared","submitting","submit_uncertain","accepted","running"],"x-open-enum":true},"decision":{"type":"null"},"receipt":{"type":"null"},"failure":{"type":"null"},"checkingWith":{"type":"null"}}}]},"FamilyBookingNeedsParent":{"description":"The camp asked a question only the parent can answer. Answer it before `decision.expiresAt`.","allOf":[{"$ref":"#/components/schemas/FamilyBookingCommon"},{"type":"object","required":["phase","status","decision","receipt","failure","checkingWith"],"properties":{"phase":{"type":"string","const":"needs_parent"},"status":{"type":"string","enum":["awaiting_decision"],"x-open-enum":true},"decision":{"$ref":"#/components/schemas/BookingPendingDecision"},"receipt":{"type":"null"},"failure":{"type":"null"},"checkingWith":{"type":"null"}}}]},"FamilyBookingBooked":{"description":"The camp enrolled the child. `receipt` has been checked against the approval. `paymentStatus` may still be `held` while the charge settles.","allOf":[{"$ref":"#/components/schemas/FamilyBookingCommon"},{"type":"object","required":["phase","status","decision","receipt","failure","checkingWith"],"properties":{"phase":{"type":"string","const":"booked"},"status":{"type":"string","enum":["confirmed"],"x-open-enum":true},"decision":{"type":"null"},"receipt":{"$ref":"#/components/schemas/BookingReceipt"},"failure":{"type":"null"},"checkingWith":{"type":"null"}}}]},"FamilyBookingWaitlisted":{"description":"The child holds a waitlist place. `receipt.amount` is present when the camp took a deposit.","allOf":[{"$ref":"#/components/schemas/FamilyBookingCommon"},{"type":"object","required":["phase","status","decision","receipt","failure","checkingWith"],"properties":{"phase":{"type":"string","const":"waitlisted"},"status":{"type":"string","enum":["waitlisted"],"x-open-enum":true},"decision":{"type":"null"},"receipt":{"$ref":"#/components/schemas/BookingReceipt"},"failure":{"type":"null"},"checkingWith":{"type":"null"}}}]},"FamilyBookingNotBooked":{"description":"The booking ended without a place. `failure` is `null` only when the parent cancelled cleanly.","allOf":[{"$ref":"#/components/schemas/FamilyBookingCommon"},{"type":"object","required":["phase","status","decision","receipt","failure","checkingWith"],"properties":{"phase":{"type":"string","const":"not_booked"},"status":{"type":"string","enum":["failed","cancelled"],"x-open-enum":true},"decision":{"type":"null"},"receipt":{"type":"null"},"failure":{"oneOf":[{"$ref":"#/components/schemas/BookingFailure"},{"type":"null"}]},"checkingWith":{"type":"null"}}}]},"FamilyBookingChecking":{"description":"The outcome isn’t known yet. Never show it as booked or failed.","allOf":[{"$ref":"#/components/schemas/FamilyBookingCommon"},{"type":"object","required":["phase","status","decision","receipt","failure","checkingWith"],"properties":{"phase":{"type":"string","const":"checking"},"status":{"type":"string","enum":["uncertain"],"x-open-enum":true},"decision":{"type":"null"},"receipt":{"type":"null"},"failure":{"type":"null"},"checkingWith":{"type":"string","enum":["card","camp"],"description":"`card`: a card hold School’s Out could not resolve. Nothing reached the camp, the parent may cancel, and the child may be booked again. `camp`: School’s Out is confirming the outcome with the camp."}}}]},"FamilyBooking":{"description":"A family booking. `phase` selects the variant and decides which of `decision`, `receipt`, `failure` and `checkingWith` are present.","oneOf":[{"$ref":"#/components/schemas/FamilyBookingInProgress"},{"$ref":"#/components/schemas/FamilyBookingNeedsParent"},{"$ref":"#/components/schemas/FamilyBookingApplied"},{"$ref":"#/components/schemas/FamilyBookingBooked"},{"$ref":"#/components/schemas/FamilyBookingWaitlisted"},{"$ref":"#/components/schemas/FamilyBookingNotBooked"},{"$ref":"#/components/schemas/FamilyBookingChecking"}],"discriminator":{"propertyName":"phase","mapping":{"in_progress":"#/components/schemas/FamilyBookingInProgress","needs_parent":"#/components/schemas/FamilyBookingNeedsParent","applied":"#/components/schemas/FamilyBookingApplied","booked":"#/components/schemas/FamilyBookingBooked","waitlisted":"#/components/schemas/FamilyBookingWaitlisted","not_booked":"#/components/schemas/FamilyBookingNotBooked","checking":"#/components/schemas/FamilyBookingChecking"}}},"BookingEventKind":{"type":"string","enum":["approval_granted","payment_hold_placed","payment_hold_failed","task_submitted","task_submit_uncertain","task_status_observed","decision_requested","decision_accepted","decision_declined","confirmation_received","payment_captured","payment_capture_failed","payment_released","cancel_requested","reconciled","change_requested","change_completed","outcome_reported","refund_chosen","refund_completed","credits_held","credits_released","credits_applied","group_rate_credited"],"x-open-enum":true,"description":"What happened. New kinds may be added; show `summary` for a kind you don’t recognise."},"BookingActor":{"type":"string","enum":["parent","auto","hallpass","system","staff"],"x-open-enum":true,"description":"Who caused the event. New actors may be added."},"BookingEvent":{"type":"object","required":["kind","status","actor","summary","createdAt"],"properties":{"kind":{"$ref":"#/components/schemas/BookingEventKind"},"status":{"$ref":"#/components/schemas/BookingStatus","description":"The booking’s status after this event."},"actor":{"$ref":"#/components/schemas/BookingActor"},"summary":{"type":"string","maxLength":500},"createdAt":{"type":"string","format":"date-time","description":"When the event was recorded."}}},"FamilyBookingDetail":{"allOf":[{"$ref":"#/components/schemas/FamilyBooking"},{"type":"object","required":["events"],"properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/BookingEvent"},"description":"The full history, oldest first."}}}]},"BookingPaymentMethod":{"type":"object","required":["brand","last4"],"properties":{"brand":{"type":"string","description":"Can be empty; show “Card” in that case."},"last4":{"type":"string","pattern":"^[0-9]{4}$"}}},"FamilyAccountViewerProfile":{"description":"The signed-in parent's identity. phone is the editable contact phone; phoneVerified is true while it is the account's verified phone (POST /api/auth/phone/check). Verifying a phone does not turn on two-step login: SMS two-step login is its own switch (POST and DELETE /api/users/viewer/two-step/sms) that texts the verified phone.","properties":{"fullName":{"maxLength":120,"type":["string","null"],"description":"The parent's name: their legacy profile first and last name joined by a space. Writing it splits it at its first space."},"preferredName":{"maxLength":60,"type":["string","null"]},"email":{"type":"string"},"emailVerified":{"type":"boolean"},"pendingEmail":{"description":"The address an email change waits on (POST /api/users/viewer/email-change) until its link is confirmed; null when none. email stays the sign-in address until then.","format":"email","type":["string","null"]},"phone":{"description":"E.164.","type":["string","null"]},"phoneVerified":{"description":"phone is the account's verified phone, which texts (notifications, SMS two-step login) go to.","type":"boolean"},"hasPassword":{"description":"The account can sign in with a password. False for an account that signs in only with Google, a magic link or a passkey.","type":"boolean"},"passwordChangedAt":{"description":"When the password was set (at sign-up or enrollment) or last changed. Null when hasPassword is false, and also for a password stored before this date was recorded.","format":"date-time","type":["string","null"]},"twoStepEnabled":{"description":"At least one second factor is enrolled (twoStepMethods is not empty).","type":"boolean"},"twoStepMethods":{"description":"The enrolled second factors, in the order sign-in offers them: passkey (a passkey is registered) and sms (SMS two-step login is on).","type":"array","uniqueItems":true,"items":{"enum":["passkey","sms"],"type":"string"}}},"required":["fullName","preferredName","email","emailVerified","pendingEmail","phone","phoneVerified","hasPassword","passwordChangedAt","twoStepEnabled","twoStepMethods"],"type":"object","additionalProperties":false},"FamilyAccountViewer":{"description":"The viewer response. Only profile is part of the family contract; the snake_case user, credits, access and organizations fields are retained for Intelligence and are not described here.","properties":{"profile":{"$ref":"#/components/schemas/FamilyAccountViewerProfile"}},"required":["profile"],"type":"object"},"FamilyAccountDeviceSession":{"description":"A signed-in device. It names no place: the API has no trustworthy coarse location for a sign-in.","properties":{"id":{"format":"uuid","type":"string"},"device":{"description":"Browser and platform from the User-Agent at sign-in, for example Chrome on macOS.","type":"string"},"kind":{"enum":["laptop","phone","other"],"type":"string"},"lastActiveAt":{"description":"Updated at most once per 15 minutes.","format":"date-time","type":"string"},"createdAt":{"format":"date-time","type":"string"},"current":{"description":"The session this request's cookie names.","type":"boolean"}},"required":["id","device","kind","lastActiveAt","createdAt","current"],"type":"object","additionalProperties":false},"FamilyAccountExport":{"description":"A data export. expiresAt is when the emailed download link lapses; until then it opens any number of times.","properties":{"id":{"format":"uuid","type":"string"},"status":{"enum":["pending","running","ready","failed","expired"],"type":"string"},"requestedAt":{"format":"date-time","type":"string"},"readyAt":{"format":"date-time","type":["string","null"]},"expiresAt":{"format":"date-time","type":["string","null"]}},"required":["id","status","requestedAt","readyAt","expiresAt"],"type":"object","additionalProperties":false},"FamilyAccountPlace":{"description":"A place the parent can choose as the household's home area, its home, or one of its saved places.","properties":{"placeId":{"description":"Opaque; accepted by PATCH /api/users/viewer/household for one hour.","type":"string"},"label":{"type":"string","description":"The area the place is in (\"Sellwood, Portland, OR\"), also for a street address."},"address":{"type":["string","null"],"maxLength":200,"description":"The street address a search matched; null for an area (a city or ZIP code) or a device's location."},"postalCode":{"type":["string","null"]},"region":{"description":"Two-letter US state code.","type":["string","null"]},"point":{"properties":{"latitude":{"maximum":90,"minimum":-90,"type":"number"},"longitude":{"maximum":180,"minimum":-180,"type":"number"}},"required":["latitude","longitude"],"type":"object","additionalProperties":false},"precision":{"$ref":"#/components/schemas/FamilyAccountPlacePrecision"},"served":{"description":"Inside an area School's Out serves.","type":"boolean"}},"required":["placeId","label","address","postalCode","region","point","precision","served"],"type":"object","additionalProperties":false},"FamilyAccountPlacePrecision":{"enum":["building","parcel","street","locality","region","unknown","device"],"type":"string","description":"How exactly a place's point places it. building, parcel and street are a geocoded street address; locality and region an area's centre; device the point a device sent (Use my location); unknown a geocoder answer without one. building, parcel, street and device are street level: only a match at one of them carries an address."},"FamilyAccountLocation":{"description":"Where the household's home is, as a place lookup resolved it: set from a placeId (see PATCH /api/users/viewer/household); clients never send its point or address.","type":"object","additionalProperties":false,"required":["label","address","point","precision"],"properties":{"label":{"type":"string","minLength":1,"maxLength":120,"description":"The parent's name for it, else the area the lookup named."},"address":{"type":["string","null"],"maxLength":200,"description":"The street address the lookup matched; null for an area or a device's location."},"point":{"properties":{"latitude":{"maximum":90,"minimum":-90,"type":"number"},"longitude":{"maximum":180,"minimum":-180,"type":"number"}},"required":["latitude","longitude"],"type":"object","additionalProperties":false},"precision":{"$ref":"#/components/schemas/FamilyAccountPlacePrecision"}}},"FamilyAccountSavedPlace":{"description":"One of the household's saved places besides its home, located like the home from a placeId.","type":"object","additionalProperties":false,"required":["id","kind","childId","label","address","point","precision"],"properties":{"id":{"type":"string","format":"uuid","description":"Send it back to keep the place (and its location); leave it out with a placeId for a new place."},"kind":{"enum":["work","school","other"],"type":"string"},"childId":{"type":["string","null"],"format":"uuid","description":"For a school place, the child whose school it is: a school the school directory does not list. At most one school place per child; null otherwise."},"label":{"type":"string","minLength":1,"maxLength":120},"address":{"type":["string","null"],"maxLength":200},"point":{"properties":{"latitude":{"maximum":90,"minimum":-90,"type":"number"},"longitude":{"maximum":180,"minimum":-180,"type":"number"}},"required":["latitude","longitude"],"type":"object","additionalProperties":false},"precision":{"$ref":"#/components/schemas/FamilyAccountPlacePrecision"}}},"FamilyAccountSearchArea":{"description":"The household's home area. point is set only from a placeId (see PATCH); clients cannot send one.","properties":{"label":{"maxLength":120,"minLength":1,"type":"string"},"postalCode":{"pattern":"^[0-9]{5}$","type":"string"},"region":{"pattern":"^[A-Z]{2}$","type":"string"},"point":{"properties":{"latitude":{"maximum":90,"minimum":-90,"type":"number"},"longitude":{"maximum":180,"minimum":-180,"type":"number"}},"required":["latitude","longitude"],"type":"object","additionalProperties":false},"radiusMiles":{"maximum":50,"minimum":1,"type":"integer"},"fromHome":{"type":"boolean","description":"Present and true when the search area is centred on the household's home (the same point). Derived on every save; a sent value is ignored."}},"required":["label"],"type":"object","additionalProperties":false},"FamilyAccountEmergencyContact":{"properties":{"id":{"type":"string","format":"uuid","description":"Sent on every contact the API returns. Send it back to keep the contact (children point at it); leave it out for a new contact."},"name":{"maxLength":255,"type":"string"},"relationship":{"maxLength":60,"type":"string","description":"May be empty, as for contacts added from a child profile."},"phone":{"maxLength":64,"type":"string","description":"E.164 when it parses as a phone number, otherwise as typed."}},"required":["name","relationship","phone"],"type":"object","additionalProperties":false,"description":"One of the household's emergency contacts. Children point at it by id, so their emergency contact follows its edits; a name or a phone may be empty, never both."},"FamilyAccountPlanningIntent":{"properties":{"planning":{"items":{"enum":["after_school","summer","school_break","weekend","browsing"],"type":"string"},"type":"array"},"neededWhen":{"enum":["this_month","next_few_months","planning_ahead",null],"type":["string","null"]}},"required":["planning","neededWhen"],"type":"object","additionalProperties":false},"FamilyAccountHouseholdReferral":{"description":"The household's stable referral code, how many accounts joined with it, and how many it admits in all.","properties":{"code":{"pattern":"^[A-Z]{1,8}-[0-9]{2,6}$","type":"string"},"joined":{"minimum":0,"type":"integer"},"limit":{"description":"How many families the code admits in all, at sign-up or by redemption; joined counts toward it. Once joined reaches it, the code is refused with referral_limit_reached.","minimum":0,"type":"integer"}},"required":["code","joined","limit"],"type":"object","additionalProperties":false},"FamilyAccountReferralRedemption":{"properties":{"code":{"type":"string"},"redeemedAt":{"format":"date-time","type":"string"},"credited":{"description":"Whether the wallet granted the referral credit. While referral rewards are off, a redemption is recorded without credit and stays false; while they are on, a credit not granted yet is granted later.","type":"boolean"}},"required":["code","redeemedAt","credited"],"type":"object","additionalProperties":false},"FamilyAccountCalendarSync":{"type":"object","required":["confirmed","waitlisted"],"properties":{"confirmed":{"type":"boolean","description":"Write booked sessions to School's Out's calendar in this account."},"waitlisted":{"type":"boolean","description":"Write waitlisted sessions to School's Out's calendar in this account."}}},"FamilyAccountCalendarFailure":{"type":"object","required":["code","at"],"properties":{"code":{"type":"string","x-open-enum":true,"enum":["authorization_failed","authorization_revoked","refresh_failed","push_failed"],"description":"authorization_failed: the provider refused a connection attempt (an existing connection keeps working). authorization_revoked: the provider no longer accepts the stored grant or password; reconnect. refresh_failed: reading events failed. push_failed: making School's Out's own calendar or writing bookings to it failed."},"at":{"type":"string","format":"date-time"}}},"FamilyAccountGoogleCalendarConnection":{"type":"object","required":["connected","emailAddress","connectedAt","lastSyncedAt","sync","failure","calendars","needsRegrant"],"properties":{"connected":{"type":"boolean"},"emailAddress":{"type":["string","null"],"description":"The connected Google account, which may differ from the sign-in email; null when disconnected."},"connectedAt":{"type":["string","null"],"format":"date-time"},"lastSyncedAt":{"type":["string","null"],"format":"date-time","description":"When the event cache last refreshed successfully."},"sync":{"$ref":"#/components/schemas/FamilyAccountCalendarSync"},"failure":{"oneOf":[{"$ref":"#/components/schemas/FamilyAccountCalendarFailure"},{"type":"null"}],"description":"The last authorization or sync failure; null once a sync succeeds."},"calendars":{"type":"array","items":{"$ref":"#/components/schemas/FamilyAccountCalendarChoice"},"description":"The parent's calendars as last listed (School's Out's own excluded); empty before the first refresh or when disconnected. A grant without the calendar-list scope lists only the primary calendar."},"needsRegrant":{"type":"boolean","description":"True when the grant predates or declined calendar.calendarlist.readonly: only the primary calendar is read until the parent connects again."}},"description":"The single-connection shape of the /google-calendar routes, which act on the viewer's first Google connection (in connection order). Kept for app releases before multiple accounts; read GET /api/users/viewer/calendars for every connection."},"FamilyAccountCalendarConnection":{"type":"object","additionalProperties":false,"required":["id","provider","account","connectedAt","lastSyncedAt","sync","failure","calendars","needsRegrant"],"description":"One of the viewer's own calendar accounts, without its secret.","properties":{"id":{"type":"string","format":"uuid"},"provider":{"type":"string","x-open-enum":true,"enum":["google","apple"],"description":"google: a Google account. apple: an Apple iCloud account."},"account":{"type":"string","description":"The connected account, lowercased: the Google account address (which may differ from the sign-in email) or the Apple ID."},"connectedAt":{"type":"string","format":"date-time"},"lastSyncedAt":{"type":["string","null"],"format":"date-time","description":"When the event cache last refreshed successfully."},"sync":{"$ref":"#/components/schemas/FamilyAccountCalendarSync"},"failure":{"oneOf":[{"$ref":"#/components/schemas/FamilyAccountCalendarFailure"},{"type":"null"}],"description":"The last authorization or sync failure; null once a sync succeeds."},"calendars":{"type":"array","items":{"$ref":"#/components/schemas/FamilyAccountCalendarChoice"},"description":"The account's calendars and which are read; empty until the first refresh lists them."},"needsRegrant":{"type":"boolean","description":"True when a Google grant cannot list calendars, so only its primary calendar is read until the parent reconnects. Always false for Apple."}}},"FamilyAccountCalendarConnections":{"type":"object","additionalProperties":false,"required":["connections"],"properties":{"connections":{"type":"array","items":{"$ref":"#/components/schemas/FamilyAccountCalendarConnection"},"description":"In the order they were first connected."}}},"FamilyAccountCalendarEvent":{"type":"object","required":["id","calendar","title","date","endDate","allDay","startTime","endTime","htmlLink","childIds","busy","adult"],"properties":{"id":{"type":"string","description":"The viewer's own event: its provider event ID, stable for links and googleEventCovers. A busy block: an opaque stable ID (busy:\u003chash\u003e)."},"calendar":{"oneOf":[{"$ref":"#/components/schemas/FamilyAccountCalendarRef"},{"type":"null"}],"description":"The viewer's calendar the event came from; null for a busy block."},"title":{"type":["string","null"],"description":"Null for a busy block."},"date":{"type":"string","format":"date","description":"Local start date in the household time zone."},"endDate":{"type":"string","format":"date","description":"Local end date, inclusive."},"allDay":{"type":"boolean"},"startTime":{"type":["string","null"],"pattern":"^[0-2][0-9]:[0-5][0-9]$","description":"Local HH:MM; null when all day."},"endTime":{"type":["string","null"],"pattern":"^[0-2][0-9]:[0-5][0-9]$","description":"Local HH:MM; null when all day."},"htmlLink":{"type":["string","null"],"description":"The event's page in the provider's web calendar; null for a busy block and for providers without one (Apple)."},"childIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"The viewer's accessible children whose first name (or planning label) appears in the title of the viewer's own event as a whole word, case-insensitively. Unordered. Always empty for a busy block."},"busy":{"type":"boolean","description":"True for another adult's event, shown as a busy block."},"adult":{"$ref":"#/components/schemas/FamilyAccountCalendarAdult"}},"description":"One event of the family's merged Google Calendar read: every connected adult's (owner, co-parent and caregiver) cached events. The viewer's own events are whole (busy: false). Another adult's event is a busy block (busy: true): only its time span and adult; calendar, title and htmlLink are null, childIds is empty and id is opaque. The API leaves the rest out of the response; another adult's calendar list is never shown. Another adult's copy of an event the viewer has too shows once, as the viewer's."},"FamilyAccountCalendarEvents":{"type":"object","required":["events","stale"],"properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/FamilyAccountCalendarEvent"}},"stale":{"type":"boolean","description":"True when a connected account's last refresh failed and its older cached events are served."}}},"FamilyAccountCalendarDisconnection":{"type":"object","required":["connected","revoked"],"properties":{"connected":{"const":false,"type":"boolean"},"revoked":{"type":"boolean","description":"Whether the provider confirmed the grant revoked or already invalid."}}},"FamilyAccountGoogleCalendarAuthorization":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"Google consent URL for web; one-use API browser bootstrap URL for native."}}},"FamilyAccountCalendarRef":{"type":"object","additionalProperties":false,"required":["id","name"],"description":"The calendar an event came from.","properties":{"id":{"type":"string","description":"The provider's calendar ID: a Google calendar ID (a primary calendar's is the Google account address) or an Apple calendar's CalDAV URL."},"name":{"type":"string","description":"The calendar's name as the parent sees it, such as Family or Work."}}},"FamilyAccountCalendarChoice":{"type":"object","additionalProperties":false,"required":["id","name","primary","selected"],"description":"One of the parent's calendars and whether its events are read.","properties":{"id":{"type":"string"},"name":{"type":"string"},"primary":{"type":"boolean"},"selected":{"type":"boolean","description":"Read into the event cache. Without a saved choice, the calendars Google Calendar shows are read."}}},"FamilyPlanSchool":{"type":"object","additionalProperties":false,"required":["id","name","kind","city","region","postalCode","distanceMiles","calendarStatus"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"kind":{"enum":["public","private","charter","preschool"]},"city":{"type":"string"},"region":{"type":"string","pattern":"^[A-Z]{2}$"},"postalCode":{"oneOf":[{"type":"string","pattern":"^[0-9]{5}$"},{"type":"null"}]},"distanceMiles":{"oneOf":[{"type":"number","minimum":0,"description":"From the household's home, to a tenth of a mile; null when no home point is known or the school has no location."},{"type":"null"}]},"calendarStatus":{"enum":["published","pending","none"],"description":"published: the school's calendar source published days in its current term; pending: a calendar source is linked but has published nothing for the current term; none: no calendar source."}}},"FamilyPlanChildSchool":{"type":"object","additionalProperties":false,"required":["schoolId","name","kind","address","point"],"properties":{"schoolId":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"name":{"type":"string","description":"The school's name, the name the parent typed, or a default label for an answer without one."},"kind":{"enum":["public","private","charter","preschool","homeschool","other"],"description":"The school's kind; for an answer that names no school, private, homeschool, or other (not found)."},"address":{"type":["string","null"],"description":"A school's address on one line, its street first when the directory has one (\"5700 SE 39th Ave, Portland, OR 97202\"); null for an answer that names no school."},"point":{"type":["object","null"],"additionalProperties":false,"required":["latitude","longitude"],"properties":{"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180}},"description":"The school's location from its directory, as every family location reads (the household's home and places: point {latitude, longitude}); null when it has none, or for an answer that names no school."}}},"FamilyPlanSchoolCalendarDay":{"type":"object","additionalProperties":false,"required":["date","kind","label","breakId","breakName","endsAt","source","updateId"],"properties":{"date":{"type":"string","format":"date"},"kind":{"enum":["no_school","break","early_release","holiday"]},"label":{"type":"string"},"breakId":{"oneOf":[{"type":"string","description":"Groups the consecutive days of one break."},{"type":"null"}]},"breakName":{"oneOf":[{"type":"string"},{"type":"null"}]},"endsAt":{"oneOf":[{"type":"string","pattern":"^[0-9]{2}:[0-9]{2}$","description":"When students are released on an early-release day."},{"type":"null"}]},"source":{"enum":["school","email","parent"]},"updateId":{"oneOf":[{"type":"string","format":"uuid","description":"The email update that set the day, which can be undone."},{"type":"null"}]}}},"FamilyPlanSchoolCalendarSchool":{"type":"object","additionalProperties":false,"required":["schoolId","name","childIds","status","term","weekdays","hours","days"],"properties":{"schoolId":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"name":{"type":"string"},"childIds":{"type":"array","items":{"type":"string","format":"uuid"}},"status":{"enum":["published","pending","none"]},"term":{"oneOf":[{"type":"object","additionalProperties":false,"required":["start","end"],"properties":{"start":{"type":"string","format":"date"},"end":{"type":"string","format":"date"}}},{"type":"null"}]},"weekdays":{"type":"array","items":{"type":"integer","minimum":0,"maximum":6},"description":"Days the school is normally in session, 0 = Monday."},"hours":{"oneOf":[{"type":"object","additionalProperties":false,"required":["start","end"],"properties":{"start":{"type":"string","pattern":"^[0-9]{2}:[0-9]{2}$"},"end":{"type":"string","pattern":"^[0-9]{2}:[0-9]{2}$"}}},{"type":"null"}]},"days":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanSchoolCalendarDay"},"description":"Days out or released early, after the family's overrides, by date. Days in session are not listed. Summer is a break labeled Summer break on the school's weekdays: after the term's last day through Labor Day, and before a term that starts after June 15 and by September, from June 15; a published day or an override on those dates wins."}}},"FamilyPlanSchoolCalendar":{"type":"object","additionalProperties":false,"required":["schools"],"properties":{"schools":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanSchoolCalendarSchool"}}}},"FamilyPlanError":{"type":"object","required":["error","message"],"additionalProperties":false,"properties":{"error":{"const":true},"message":{"type":"string","description":"A sentence you can show the parent as is."},"data":{"type":"null"}}},"FamilyPlanRecurrence":{"type":"object","required":["weekdays","from","until"],"additionalProperties":false,"properties":{"weekdays":{"type":"array","minItems":1,"maxItems":7,"uniqueItems":true,"items":{"type":"integer","minimum":0,"maximum":6},"description":"Days of the week, 0 = Monday."},"from":{"type":"string","format":"date"},"until":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Last day, inclusive; null while it goes on."}}},"FamilyPlan":{"type":"object","required":["id","source","childIds","title","organizationId","activityId","sessionId","dates","startTime","endTime","dayTimes","recurrence","status","askAgainAfter","group","coveredBy","note","bookingId","editable","organizationName","price","canceledBy","dueOnProviderSite"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^(booking|plan):[0-9a-f-]{36}$","description":"\"booking:\u003cuuid\u003e\" for a Hallpass booking, \"plan:\u003cuuid\u003e\" for a plan made here."},"source":{"enum":["booking","external","covered"]},"childIds":{"type":"array","items":{"type":"string","format":"uuid"}},"title":{"type":"string"},"organizationId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"organizationName":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The camp's name: as the booking was made, else the catalog's live camp; null without one."},"activityId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"sessionId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"dates":{"type":"array","maxItems":400,"items":{"type":"string","format":"date"},"description":"The occurrences inside the requested range (all of them, up to 400, when no range was requested), earliest first. A recurrence is expanded; a booking that published only a date range lists every day of it."},"startTime":{"anyOf":[{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$"},{"type":"null"}]},"endTime":{"anyOf":[{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$"},{"type":"null"}]},"dayTimes":{"type":"array","maxItems":400,"items":{"$ref":"#/components/schemas/FamilyPlanDayTime"},"description":"The plan's own times on those of its listed dates whose times differ from startTime and endTime: a multi-day booking whose session keeps different hours on different days, or an overnight stay (drop-off on its first day, pick-up on its last, neither between). Earliest first; empty when every listed date keeps startTime and endTime."},"recurrence":{"anyOf":[{"$ref":"#/components/schemas/FamilyPlanRecurrence"},{"type":"null"}]},"status":{"enum":["booked","applied","needs_parent","waitlisted","registered","unconfirmed","covered","canceled","failed"],"description":"Bookings map their phase: booked, applied, needs_parent and waitlisted as is; in_progress and checking are unconfirmed; not_booked is canceled when someone cancelled it, else failed. External plans hold registered, applied (the family applied on the provider’s site and waits for its answer), needs_parent (it still has to finish there) or unconfirmed (nobody said yet), the same words as the booking phases."},"askAgainAfter":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Set only for applied: Home doesn't ask whether the application became a registration before this time (null asks now). A booking plan carries its booking's askAgainAfter."},"group":{"enum":["active","pending","waitlist","closed","past"],"description":"active: booked, registered or covered. pending: applied, needs_parent or unconfirmed. waitlist: waitlisted. closed: canceled or failed. past: any plan that is not closed and whose last day is before today."},"coveredBy":{"anyOf":[{"enum":["family","sitter","other"]},{"type":"null"}]},"note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]},"bookingId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"price":{"anyOf":[{"$ref":"#/components/schemas/BookingMoney"},{"type":"null"}],"description":"A booking's listed price; null for a plan made here or a session that stated none."},"canceledBy":{"anyOf":[{"enum":["parent","provider","system"]},{"type":"null"}],"description":"Who canceled a canceled booking; null otherwise."},"dueOnProviderSite":{"anyOf":[{"$ref":"#/components/schemas/FamilyPlanProviderTask"},{"type":"null"}],"description":"Something the parent must still do on the provider's site for a booking that is not closed; null otherwise."},"editable":{"type":"boolean","description":"True for a plan the viewer made. Plans for a shared child are visible to the child's owner and all of its guardians, but only the adult who made one may change or delete it; booking plans change through their booking."}}},"FamilyPlanInput":{"type":"object","additionalProperties":false,"required":["childIds","source"],"description":"Send exactly one of dates and recurrence, except for an external plan with a sessionId, which may send neither and takes the session's days. Without a title, sessionId or activityId names the plan from the catalog.","properties":{"childIds":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","format":"uuid"},"description":"Children the viewer owns or is a guardian of."},"source":{"enum":["external","covered"],"description":"external: registered on the provider's own site. covered: the family covers the days itself."},"sessionId":{"type":"string","format":"uuid"},"activityId":{"type":"string","format":"uuid"},"title":{"type":"string","minLength":1,"maxLength":120,"description":"Required without sessionId or activityId."},"dates":{"type":"array","minItems":1,"maxItems":400,"uniqueItems":true,"items":{"type":"string","format":"date"},"description":"The plan's days. Optional for an external plan with a sessionId: left out (with recurrence), the session's own days are used."},"recurrence":{"$ref":"#/components/schemas/FamilyPlanRecurrence"},"startTime":{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","description":"Send with endTime. Left out on an external plan that takes its session's days, the session's daily clock is used when it has one."},"endTime":{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$"},"coveredBy":{"enum":["family","sitter","other"],"description":"Required for covered plans, and only for them."},"status":{"enum":["registered","applied","needs_parent","unconfirmed"],"default":"registered","description":"External plans only. applied sets askAgainAfter, so Home asks about the application later."}}},"FamilyPlanChangeInput":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"status":{"enum":["registered","applied","canceled"],"description":"registered and applied apply to external plans only. applied again is \"not yet\" for an applied plan: it resets askAgainAfter."},"dates":{"type":"array","minItems":1,"maxItems":400,"uniqueItems":true,"items":{"type":"string","format":"date"},"description":"Replaces the occurrences, including a recurrence."},"note":{"anyOf":[{"type":"string","minLength":1,"maxLength":500},{"type":"null"}],"description":"null removes the note."}}},"FamilyPlanGoogleEventCover":{"type":"object","required":["eventId","covers"],"additionalProperties":false,"properties":{"eventId":{"type":"string","minLength":1,"maxLength":200},"covers":{"type":"boolean"}}},"FamilyPlanChildDayMarks":{"type":"object","additionalProperties":false,"properties":{"need":{"anyOf":[{"const":"not_needed"},{"type":"null"}],"description":"\"We're covered\" without saying how."},"googleEventCovers":{"anyOf":[{"$ref":"#/components/schemas/FamilyPlanGoogleEventCover"},{"type":"null"}],"description":"Whether a Google event covers the day."},"conflict":{"anyOf":[{"enum":["skip","leave","fine"]},{"type":"null"}]},"hiddenIdeas":{"anyOf":[{"type":"array","maxItems":50,"items":{"type":"string","format":"uuid"}},{"type":"null"}],"description":"Activity ids the family hid for the day; an empty list or null clears them."}}},"FamilyPlanDayMarks":{"type":"object","additionalProperties":false,"description":"Absent keys are unset. In a request, a present key replaces the stored value and null clears it; a child set to null clears all of that child's marks.","properties":{"open":{"anyOf":[{"enum":["open","not"]},{"type":"null"}],"description":"Household-level; friends who share plans see 'open'."},"children":{"type":"object","description":"Keyed by child id.","additionalProperties":{"anyOf":[{"$ref":"#/components/schemas/FamilyPlanChildDayMarks"},{"type":"null"}]}}}},"FamilyPlanCalendarCover":{"type":"object","required":["state","source","planId","title","activityId"],"additionalProperties":false,"properties":{"state":{"enum":["planned","idea","open","not_needed"],"description":"First match: planned (an active plan, or a Google event the family said covers the day), not_needed (the family said no cover is needed), idea (a saved option, or a followed session, is available that day and not hidden in the day's marks; title names the first), else open."},"source":{"anyOf":[{"enum":["booking","external","covered","calendar"]},{"type":"null"}],"description":"For a planned day: the covering plan's source (booking: a Hallpass booking; external: a registration made elsewhere; covered: the family's own cover), or calendar for a Google event the family said covers the day. Null otherwise."},"planId":{"anyOf":[{"type":"string"},{"type":"null"}]},"title":{"anyOf":[{"type":"string"},{"type":"null"}]},"activityId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"The covering plan's activity, or the first idea's; null without one."}}},"FamilyPlanCalendarFriend":{"type":"object","required":["userId","publicName","publicNameKnown"],"additionalProperties":false,"properties":{"userId":{"type":"string","format":"uuid"},"publicName":{"type":["string","null"]},"publicNameKnown":{"type":"boolean","description":"False when the parent has no name to show; publicName is then null and the client writes its own wording."}}},"FamilyPlanCalendarEntry":{"type":"object","required":["id","kind","title","startMinutes","endMinutes","planId","eventId","activityId","activity","recurring","friends"],"additionalProperties":false,"properties":{"id":{"type":"string"},"kind":{"enum":["plan","pending","gap","event","flag","school"],"description":"school: the day off or early release. plan: an active plan. pending: a pending or waitlisted plan. gap: a day off no plan covers, over the school's hours (or the default care hours). event: a Google event naming the child. flag: an alert of the day for the child (registration opening, or a spot that opened)."},"title":{"type":"string"},"startMinutes":{"anyOf":[{"type":"integer","minimum":0,"maximum":1440},{"type":"null"}],"description":"Minutes after midnight. A plan's are that day's own: a multi-day booking keeps each day's drop-off and pick-up (dayTimes on the plan), so an overnight stay starts on its first day and ends on its last, with neither between."},"endMinutes":{"anyOf":[{"type":"integer","minimum":0,"maximum":1440},{"type":"null"}]},"planId":{"anyOf":[{"type":"string"},{"type":"null"}]},"eventId":{"anyOf":[{"type":"string"},{"type":"null"}]},"activityId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"activity":{"anyOf":[{"$ref":"#/components/schemas/FamilyPlanActivity"},{"type":"null"}],"description":"On plan and pending entries: the plan's catalog activity (name and photo); null without one, or once it left the catalog."},"recurring":{"type":"boolean","description":"True for a plan that repeats weekly (a plan with a recurrence)."},"friends":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanCalendarFriend"},"description":"On plan and pending entries: friends who share their plans and have a booking or registered plan for the same session."}}},"FamilyPlanCalendarChildDay":{"type":"object","required":["childId","school","schoolLabel","cover","entries","ideas"],"additionalProperties":false,"properties":{"childId":{"type":"string","format":"uuid"},"school":{"enum":["in_session","no_school","early_release","break","none"],"description":"none without a school calendar, outside the school's term and its summer, and on days the school does not meet. Summer around the term (see the school calendar's days) is break."},"schoolLabel":{"anyOf":[{"type":"string"},{"type":"null"}]},"cover":{"$ref":"#/components/schemas/FamilyPlanCalendarCover"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanCalendarEntry"},"description":"detail=day only; empty in a summary."},"ideas":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanIdea"},"description":"detail=day only (empty in a summary): the options behind an idea cover; empty for any other cover."}}},"FamilyPlanCalendarAlert":{"type":"object","required":["trackId","title","childIds","kind"],"additionalProperties":false,"properties":{"trackId":{"type":"string"},"title":{"type":"string"},"childIds":{"type":"array","items":{"type":"string","format":"uuid"}},"kind":{"enum":["registration_opens","spot_opens"]}}},"FamilyPlanCalendarFriendOpen":{"type":"object","required":["userId","publicName","publicNameKnown","childFirstName"],"additionalProperties":false,"properties":{"userId":{"type":"string","format":"uuid"},"publicName":{"type":["string","null"]},"publicNameKnown":{"type":"boolean","description":"False when the parent has no name to show; publicName is then null and the client writes its own wording."},"childFirstName":{"type":"null","description":"Always null: an open mark is household-level."}}},"FamilyPlanCalendarDay":{"type":"object","required":["date","weekend","open","children","events","alerts","friendsOpen"],"additionalProperties":false,"properties":{"date":{"type":"string","format":"date"},"weekend":{"type":"boolean"},"open":{"anyOf":[{"enum":["open","not","auto"]},{"type":"null"}],"description":"The family's open mark; unmarked, auto when a child the read covers (every child, or only childId) has a day off that nothing covers yet (open to plans on its own), else null."},"children":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanCalendarChildDay"}},"events":{"type":"array","items":{"$ref":"#/components/schemas/FamilyAccountCalendarEvent"},"description":"detail=day only; empty in a summary."},"alerts":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanCalendarAlert"}},"friendsOpen":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanCalendarFriendOpen"}}}},"FamilyPlanCalendarMonth":{"type":"object","required":["month","booked","toPlan"],"additionalProperties":false,"properties":{"month":{"type":"string","pattern":"^[0-9]{4}-[0-9]{2}$"},"booked":{"type":"integer","minimum":0,"description":"Child-days in the range with planned cover."},"toPlan":{"type":"integer","minimum":0,"description":"Child-days in the range that are gaps with open cover."}}},"FamilyPlanCalendar":{"type":"object","required":["days","months"],"additionalProperties":false,"properties":{"days":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanCalendarDay"}},"months":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanCalendarMonth"}}}},"FamilyPlanHomeAction":{"oneOf":[{"type":"object","required":["kind","bookingId","options"],"additionalProperties":false,"properties":{"kind":{"const":"answer_outcome"},"bookingId":{"type":"string","format":"uuid"},"options":{"type":"array","items":{"enum":["registered","not_registered","not_yet"]}}}},{"type":"object","required":["kind","planId","options"],"additionalProperties":false,"properties":{"kind":{"const":"answer_plan"},"planId":{"type":"string"},"options":{"type":"array","items":{"enum":["registered","applied","canceled"]}}},"description":"PATCH the plan with one of options as its status. For an applied plan, applied is \"not yet\": Home asks again later."},{"type":"object","required":["kind","bookingId"],"additionalProperties":false,"properties":{"kind":{"const":"finish_booking"},"bookingId":{"type":"string","format":"uuid"}}},{"type":"object","required":["kind","sessionId"],"additionalProperties":false,"properties":{"kind":{"const":"book_spot"},"sessionId":{"type":"string","format":"uuid"}}},{"type":"object","required":["kind","bookingId"],"additionalProperties":false,"properties":{"kind":{"const":"refund_choice"},"bookingId":{"type":"string","format":"uuid"}}},{"type":"object","required":["kind","url"],"additionalProperties":false,"properties":{"kind":{"const":"open_provider"},"url":{"type":"string"}}},{"type":"object","required":["kind","date","childIds"],"additionalProperties":false,"properties":{"kind":{"const":"find_options"},"date":{"type":"string","format":"date"},"childIds":{"type":"array","items":{"type":"string","format":"uuid"}}}},{"description":"Complete the child's profile.","type":"object","required":["kind","childId"],"additionalProperties":false,"properties":{"kind":{"const":"edit_child"},"childId":{"type":"string","format":"uuid"}}},{"description":"Answer the registration form's new required questions.","type":"object","required":["kind","registrationId"],"additionalProperties":false,"properties":{"kind":{"const":"answer_form"},"registrationId":{"type":"string","format":"uuid"}}}]},"FamilyPlanHomeItem":{"type":"object","required":["id","kind","date","childIds","title","line","fromEmail","activityId","planIds","actions"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^[a-z_]+:[A-Za-z0-9-]{1,80}$","description":"Stable \"\u003ckind\u003e:\u003csubjectId\u003e\"; dismiss it with E-62."},"kind":{"enum":["canceled_by_provider","confirm_registration","spot_opened","finish_booking","missing_detail","due_on_provider_site","conflict","email_question"],"description":"spot_opened: a session the family is waitlisted for or follows shows open places (book_spot). missing_detail: a child with upcoming plans lacks profile details a booking sends (edit_child), or a live registration lacks answers its form now requires (answer_form)."},"date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}]},"childIds":{"type":"array","items":{"type":"string","format":"uuid"}},"title":{"type":"string"},"line":{"type":"string"},"fromEmail":{"type":"boolean"},"activityId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"planIds":{"type":"array","items":{"type":"string","pattern":"^(booking|plan):[0-9a-f-]{36}$"},"description":"The plans the item is about: a booking's (and a waitlisted booking's open spot), an unconfirmed plan's, or both plans of a conflict. Empty otherwise."},"actions":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanHomeAction"}}}},"FamilyPlanHomeNextToPlan":{"type":"object","required":["key","dates","breakName","childIds","state","ideas"],"additionalProperties":false,"properties":{"key":{"type":"string","description":"The break id, or the date of a single day off."},"dates":{"type":"array","items":{"type":"string","format":"date"}},"breakName":{"anyOf":[{"type":"string"},{"type":"null"}]},"childIds":{"type":"array","items":{"type":"string","format":"uuid"}},"state":{"enum":["open","idea"]},"ideas":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanIdea"},"description":"The options the group's days offer, in day order; empty for an open group."}}},"FamilyPlanHomeUpdate":{"type":"object","required":["id","at","text","detail","fromEmail","undoable","undone","undoneText"],"additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"at":{"type":"string","format":"date-time"},"text":{"type":"string"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}]},"fromEmail":{"type":"boolean"},"undoable":{"type":"boolean"},"undone":{"type":"boolean"},"undoneText":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"For an undone update: the sentence to show in its place. Null otherwise."}}},"FamilyPlanHomeRegistrationAlert":{"type":"object","required":["trackId","organizationId","organization","activityId","title","season","opensOn"],"additionalProperties":false,"properties":{"trackId":{"type":"string"},"organizationId":{"type":"string","format":"uuid","description":"The camp the alert is about; link here when `activityId` is null."},"organization":{"anyOf":[{"$ref":"#/components/schemas/FamilyPlanOrganization"},{"type":"null"}],"description":"The camp's name and logo; null once the camp left the catalog."},"activityId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"The followed activity; null when the family follows the whole camp."},"title":{"type":"string"},"season":{"anyOf":[{"type":"string"},{"type":"null"}]},"opensOn":{"type":"string","format":"date"}},"description":"A followed activity or camp whose registration opens soon. A followed camp’s opening has no activity."},"FamilyPlanHomeWaitlist":{"type":"object","required":["bookingId","position","text"],"additionalProperties":false,"properties":{"bookingId":{"type":"string","format":"uuid"},"position":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}]},"text":{"type":"string"}}},"FamilyPlanHome":{"type":"object","required":["needsYou","nextToPlan","updates","registrationAlerts","waitlist","featuredBookingId","friends"],"additionalProperties":false,"properties":{"needsYou":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanHomeItem"}},"nextToPlan":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanHomeNextToPlan"}},"updates":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanHomeUpdate"}},"registrationAlerts":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanHomeRegistrationAlert"}},"waitlist":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanHomeWaitlist"}},"featuredBookingId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"friends":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanHomeFriend"},"description":"Friends who share plans with the family, newest friendship first, at most a dozen (policy)."}}},"FamilyBookingApplied":{"description":"The camp received an application and answers later. Only the service fee is charged (`payment.serviceFeeCents`); the rest of any hold is released. Ask the parent with the outcome route.","allOf":[{"$ref":"#/components/schemas/FamilyBookingCommon"},{"type":"object","required":["phase","status","decision","receipt","failure","checkingWith"],"properties":{"phase":{"type":"string","const":"applied"},"status":{"type":"string","enum":["applied"],"x-open-enum":true},"decision":{"type":"null"},"receipt":{"$ref":"#/components/schemas/BookingReceipt"},"failure":{"type":"null"},"checkingWith":{"type":"null"}}}]},"FamilyWallet":{"type":"object","required":["balanceCents","currency"],"properties":{"balanceCents":{"type":"integer","minimum":0,"description":"What School’s Out owes the family, in cents."},"currency":{"const":"USD"}}},"FamilyWalletEntry":{"type":"object","required":["id","kind","label","detail","amountCents","method","bookingId","occurredAt"],"properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["top_up","refund","booking_payment","booking_hold","hold_released","adjustment","gift_card","referral","group_rate"],"x-open-enum":true},"label":{"type":"string","examples":["Credits added","Refund · Robotics Week","Gift card redeemed","Referral credit"]},"detail":{"type":["string","null"],"description":"“Camp · Child” for a booking entry."},"amountCents":{"type":"integer","description":"Signed from the family’s side: positive adds to the wallet, negative spends or holds."},"method":{"oneOf":[{"type":"object","required":["brand","last4"],"properties":{"brand":{"type":"string"},"last4":{"type":"string","pattern":"^[0-9]{4}$"}}},{"const":"wallet"},{"type":"null"}],"description":"The card of a top-up, `wallet` for a booking entry the wallet paid or got back, else `null` (a group rate credit is given, not paid)."},"bookingId":{"type":["string","null"],"format":"uuid"},"occurredAt":{"type":"string","format":"date-time"}}},"FamilyPayment":{"type":"object","description":"One ledger transaction that moved the family’s money, by card, wallet or both.","required":["id","kind","label","detail","cardCents","walletCents","currency","card","serviceFeeCents","bookingId","giftCardId","occurredAt"],"properties":{"id":{"type":"string","format":"uuid","description":"The ledger transaction."},"kind":{"type":"string","enum":["booking_payment","refund","top_up","gift_card_purchase","gift_card_redeemed","referral","adjustment","group_rate"],"x-open-enum":true},"label":{"type":"string","examples":["Paid · Robotics Week","Refund · Robotics Week","Credits added","Gift card bought","Gift card redeemed","Referral credit"]},"detail":{"type":["string","null"],"description":"“Camp · Child” for a booking’s payment or refund; null otherwise."},"cardCents":{"type":["integer","null"],"description":"What moved on the card, signed from the family’s side: negative is a charge, positive a refund. Null when no card was involved."},"walletCents":{"type":["integer","null"],"description":"What moved in the wallet, signed from the family’s side: positive is a credit, negative spends credits. Null when the wallet was not involved."},"currency":{"type":"string","pattern":"^[A-Z]{3}$"},"card":{"oneOf":[{"type":"object","required":["brand","last4"],"properties":{"brand":{"type":"string"},"last4":{"type":"string","pattern":"^[0-9]{4}$"}}},{"type":"null"}],"description":"The card charged or refunded: for a top-up or a gift card purchase the card it charged, for a booking's card payment or card refund the card its hold was placed on. Null when no card was involved."},"serviceFeeCents":{"type":["integer","null"],"minimum":0,"description":"The service fee within this payment: what a booking payment charged for it, or what a refund returned of it. Null when the payment holds none."},"bookingId":{"type":["string","null"],"format":"uuid"},"giftCardId":{"type":["string","null"],"format":"uuid","description":"The gift card bought or redeemed."},"occurredAt":{"type":"string","format":"date-time"}}},"FamilyBookingStaffFollowUp":{"type":"object","description":"A booking whose money Stripe refused to move after the provider side was settled.","required":["bookingId","ownerUserId","reason","status","paymentStatus","paymentIntentId","refund","cardPaidCents","currency","organizationName","sessionName","updatedAt"],"properties":{"bookingId":{"type":"string","format":"uuid"},"ownerUserId":{"type":["string","null"],"format":"uuid","description":"Null when the family’s account was deleted."},"reason":{"type":"string","enum":["card_refund_refused","card_capture_refused","payment_deferred"],"x-open-enum":true,"description":"`card_refund_refused`: refund the family by hand. `card_capture_refused`: the provider was paid and the family card could not be charged; collect from the family. `payment_deferred`: the camp deferred payment and the family's hold (and any credits held for the booking) is kept; release it, or capture it once School's Out has paid the camp."},"status":{"type":"string"},"paymentStatus":{"type":"string"},"paymentIntentId":{"type":["string","null"],"description":"The family card’s Stripe PaymentIntent."},"refund":{"oneOf":[{"type":"object","required":["method","amountCents","walletCents","status"],"properties":{"method":{"type":"string","enum":["credits","card"]},"amountCents":{"type":"integer","minimum":1,"description":"Everything the family paid, refunded."},"walletCents":{"type":"integer","minimum":0,"description":"The part returned to the wallet at once: all of it for `credits`, the credits part for `card`."},"status":{"type":"string","enum":["pending","done"],"description":"`pending` until the card refund is confirmed."}}},{"type":"null"}]},"cardPaidCents":{"type":"integer","minimum":0,"description":"What the card paid at capture; a card refund returns at most this."},"currency":{"type":"string"},"organizationName":{"type":["string","null"]},"sessionName":{"type":["string","null"]},"updatedAt":{"type":"string","format":"date-time"}}},"FamilyBookingStaffResolution":{"type":"object","description":"How staff resolved a booking whose money waited on them.","required":["bookingId","reason","resolution","note"],"properties":{"bookingId":{"type":"string","format":"uuid"},"reason":{"type":"string","enum":["card_refund_refused","card_capture_refused","payment_deferred"],"x-open-enum":true,"description":"`card_refund_refused`: refund the family by hand. `card_capture_refused`: the provider was paid and the family card could not be charged; collect from the family. `payment_deferred`: the camp deferred payment and the family's hold (and any credits held for the booking) is kept; release it, or capture it once School's Out has paid the camp."},"resolution":{"type":"string","enum":["refunded","collected","written_off","hold_released","hold_captured"],"x-open-enum":true},"note":{"type":"string"}}},"FamilyPaymentMethodCard":{"type":"object","required":["id","brand","last4","expMonth","expYear","name"],"properties":{"id":{"type":"string","pattern":"^pm_"},"brand":{"type":"string"},"last4":{"type":"string","pattern":"^[0-9]{4}$"},"expMonth":{"type":"integer","minimum":0,"maximum":12,"description":"0 until the next sync reads it."},"expYear":{"type":"integer","minimum":0},"name":{"type":["string","null"],"description":"The cardholder’s name Stripe has for the card (billing_details.name, as Stripe Elements sends it); null when it has none or Stripe was not read."}}},"FamilyPaymentMethodBillingAddress":{"type":"object","required":["line1","city","region","postalCode"],"properties":{"line1":{"type":"string","minLength":1,"maxLength":200},"city":{"type":"string","minLength":1,"maxLength":100},"region":{"type":"string","pattern":"^[A-Z]{2}$"},"postalCode":{"type":"string","pattern":"^[0-9]{5}$"}}},"FamilyPaymentMethods":{"type":"object","required":["paymentMethod","default","backup","cards","billingAddress","receiptEmail"],"properties":{"paymentMethod":{"oneOf":[{"$ref":"#/components/schemas/BookingPaymentMethod"},{"type":"null"}],"description":"The default card’s brand and last4; kept for apps that read only it."},"default":{"oneOf":[{"$ref":"#/components/schemas/FamilyPaymentMethodCard"},{"type":"null"}]},"backup":{"oneOf":[{"$ref":"#/components/schemas/FamilyPaymentMethodCard"},{"type":"null"}]},"cards":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPaymentMethodCard"},"description":"Every card saved to the parent’s Stripe customer, newest first, including any in neither slot, which PATCH can make the default or the backup again. When Stripe can’t be read, the default and backup alone."},"billingAddress":{"oneOf":[{"$ref":"#/components/schemas/FamilyPaymentMethodBillingAddress"},{"type":"null"}]},"receiptEmail":{"type":["string","null"],"format":"email","maxLength":254,"description":"Where payment receipts are emailed instead of the account email; null sends them to the account email."}}},"FamilyBookingCancelInput":{"type":"object","additionalProperties":false,"properties":{"refundMethod":{"type":"string","enum":["credits","card"]},"reason":{"type":"string","enum":["plans_changed","sick","found_another","schedule_conflict","other"]}}},"BookingCancellationAction":{"type":"string","enum":["cancel","choose_refund"],"description":"What POST .../cancel does with this booking. cancel: stops it and releases anything held. choose_refund: the camp cancelled it; the parent chooses how the payment comes back (refundMethod)."},"BookingCancellationBlock":{"type":"string","enum":["checkout_started","checking_with_camp","booked","waitlisted","ended","cancel_requested","refund_chosen","nothing_paid"],"description":"Why POST .../cancel refuses this booking. booked and waitlisted: parents can't cancel a place here yet (show \"contact us\"); the group cancellation rules will allow it up to a deadline. checkout_started: the camp's checkout has begun paying. checking_with_camp: School's Out is checking the outcome with the camp. ended: already failed or cancelled. refund_chosen and nothing_paid: the camp cancelled and there is no refund left to choose. cancel_requested: a cancel is already on its way; sending it again returns the booking unchanged (`200`), with no second request."},"FamilyBookingCancellation":{"type":"object","additionalProperties":false,"required":["allowed","action","reason","deadline","refundableCents","methods"],"description":"What POST .../cancel accepts for this booking right now, decided by the same code the route runs, so a Cancel button shown from it never meets a 409 unless the booking moved on. It widens when the group cancellation rules land (booked bookings up to a deadline).","properties":{"allowed":{"type":"boolean"},"action":{"$ref":"#/components/schemas/BookingCancellationAction"},"reason":{"anyOf":[{"$ref":"#/components/schemas/BookingCancellationBlock"},{"type":"null"}],"description":"null exactly when allowed."},"deadline":{"type":["string","null"],"format":"date-time","description":"The last moment the route accepts it; null while no rule sets one (always, today)."},"refundableCents":{"type":"integer","minimum":0,"description":"What the route refunds: everything the family paid for choose_refund, 0 for cancel (which releases holds and charges nothing)."},"methods":{"type":"array","items":{"type":"string","enum":["credits","card"]},"description":"The refundMethod values the route acts on; empty when it refunds nothing."}}},"FamilyBookingChangeInput":{"type":"object","additionalProperties":false,"required":["idempotencyKey","sessionId"],"properties":{"idempotencyKey":{"type":"string","format":"uuid","description":"One per change request; resending it returns the same new booking with `200`."},"sessionId":{"type":"string","format":"uuid","description":"Another open session of the same activity."},"maxSpend":{"$ref":"#/components/schemas/BookingMoney","description":"Defaults to the original spending limit."},"refundMethod":{"type":"string","enum":["credits","card"],"description":"How the original’s payment comes back once the change completes; default `card`."}},"example":{"idempotencyKey":"3e9a1c7b-5d2f-4b8e-a6c4-0f1e2d3c4b5a","sessionId":"d4b8f2a1-7c3e-4f9d-8a6b-1e2f3c4d5a6b","refundMethod":"credits"}},"FamilyBookingOutcomeInput":{"type":"object","additionalProperties":false,"required":["outcome"],"properties":{"outcome":{"type":"string","enum":["registered","not_registered","not_yet","provider_task_done"],"description":"`registered`: the application became a place (applied → booked, `receipt.source: parent`). `not_registered`: no place (applied → not_booked with failure `application_declined`; any hold is released). `not_yet`: ask again in a few days (`askAgainAfter`). `provider_task_done`: the parent finished `dueOnProviderSite`, which is cleared."}}},"FamilyWalletTopUpInput":{"type":"object","additionalProperties":false,"required":["idempotencyKey","amountCents"],"properties":{"idempotencyKey":{"type":"string","format":"uuid","description":"One per top-up. Resend it after a `503`, or after the parent completes authentication, to finish the same top-up; with different details it returns `409`."},"amountCents":{"type":"integer","minimum":1000,"maximum":50000},"paymentMethod":{"type":"string","enum":["default","backup"],"description":"Optional. Which of the parent’s saved cards to charge; defaults to the default card."}},"example":{"idempotencyKey":"6c5b4a39-2817-4f6e-9d5c-4b3a29180f7e","amountCents":2500,"paymentMethod":"default"}},"FamilyGiftCard":{"type":"object","additionalProperties":false,"required":["id","amountCents","currency","status","codeLast4","recipientName","recipientEmail","emailStatus","message","card","purchasedAt","redeemedAt"],"description":"A gift card the viewer bought. The code itself is shown only once, in the purchase response; this shows its last four characters.","properties":{"id":{"type":"string","format":"uuid"},"amountCents":{"type":"integer","minimum":1},"currency":{"type":"string","const":"USD"},"status":{"type":"string","enum":["active","redeemed"],"description":"`active` until someone redeems the code into their wallet. Gift cards do not expire."},"codeLast4":{"type":"string","pattern":"^[2-9A-HJKMNP-Z]{4}$"},"recipientName":{"type":["string","null"],"maxLength":100},"recipientEmail":{"type":["string","null"],"format":"email","description":"Where School's Out emails the code; null when the purchaser shares it themselves."},"emailStatus":{"type":"string","enum":["none","pending","sent","failed"],"description":"Delivery of the code to `recipientEmail`: `none` without one, `pending` while queued or retrying, `sent`, or `failed` after the last retry. Replaying the purchase issues a new code and emails it again."},"message":{"type":["string","null"],"maxLength":300},"card":{"type":"object","additionalProperties":false,"required":["brand","last4"],"properties":{"brand":{"type":"string"},"last4":{"type":"string","pattern":"^[0-9]{4}$"}},"description":"The purchaser's saved card that paid."},"purchasedAt":{"type":"string","format":"date-time"},"redeemedAt":{"type":["string","null"],"format":"date-time"}},"example":{"id":"5b0f8d2e-3c41-4b8a-9e6f-2d7a1c9b4e30","amountCents":5000,"currency":"USD","status":"active","codeLast4":"Q7XM","recipientName":"Jordan","recipientEmail":"jordan@example.com","emailStatus":"pending","message":"Happy summer!","card":{"brand":"visa","last4":"4242"},"purchasedAt":"2026-10-16T17:04:11Z","redeemedAt":null}},"FamilyGiftCardPurchaseInput":{"type":"object","additionalProperties":false,"required":["idempotencyKey","amountCents"],"properties":{"idempotencyKey":{"type":"string","format":"uuid","description":"One key is one gift card and one card charge. Replaying it after the card is active issues a NEW code and the previous code stops working."},"amountCents":{"type":"integer","minimum":1000,"maximum":50000,"description":"Bounds are policy and may change."},"recipientName":{"type":["string","null"],"maxLength":100},"recipientEmail":{"type":["string","null"],"format":"email","description":"When set, the code is also emailed to this address (with recipientName and message) shortly after the purchase."},"message":{"type":["string","null"],"maxLength":300},"paymentMethod":{"type":"string","enum":["default","backup"],"default":"default","description":"Which of the viewer's own saved cards to charge."}},"example":{"idempotencyKey":"0d6f1c9a-2b7e-4f3d-8a1c-5e9b7d2f4a60","amountCents":5000,"recipientName":"Jordan","recipientEmail":"jordan@example.com","message":"Happy summer!","paymentMethod":"default"}},"FamilyGiftCardIssued":{"type":"object","additionalProperties":false,"required":["giftCard","code"],"properties":{"giftCard":{"$ref":"#/components/schemas/FamilyGiftCard"},"code":{"type":"string","pattern":"^[2-9A-HJKMNP-Z]{4}(-[2-9A-HJKMNP-Z]{4}){3}$","description":"The redemption code. Share it with the recipient; it stays readable to you through GET /api/users/viewer/gift-cards/{giftCardId}/code."}},"example":{"giftCard":{"id":"5b0f8d2e-3c41-4b8a-9e6f-2d7a1c9b4e30","amountCents":5000,"currency":"USD","status":"active","codeLast4":"Q7XM","recipientName":"Jordan","recipientEmail":"jordan@example.com","emailStatus":"pending","message":"Happy summer!","card":{"brand":"visa","last4":"4242"},"purchasedAt":"2026-10-16T17:04:11Z","redeemedAt":null},"code":"K7MD-3QPA-WX9H-Q7XM"},"description":"A gift card you bought with its redemption code. The code is stored sealed, so it can be shown to you again, before and after it is redeemed; answers carrying it are never cached."},"FamilyGiftCardRedeemInput":{"type":"object","additionalProperties":false,"required":["code"],"properties":{"code":{"type":"string","description":"The 16-character code; case, spaces and hyphens are ignored."}},"example":{"code":"k7md-3qpa-wx9h-q7xm"}},"FamilyFeedCampMention":{"type":"object","additionalProperties":false,"properties":{"organizationId":{"type":"string","format":"uuid"},"name":{"type":"string","description":"The camp's name."},"logoUrl":{"type":["string","null"],"description":"The camp's logo; null when none is known. Once rendered it is the logo's public, cross-origin rendition (GET /api/media/logos/{file}), else its source URL."}},"required":["organizationId","name","logoUrl"],"description":"A mention of the post's own camp. A post only ever mentions the camp it is about."},"FamilyFeedLinkTarget":{"description":"A catalog record by id, or an outside page by URL.","oneOf":[{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","enum":["activity","organization"]},"id":{"type":"string","format":"uuid"}},"required":["kind","id"]},{"type":"object","additionalProperties":false,"properties":{"kind":{"const":"url","type":"string"},"url":{"type":"string","format":"uri"}},"required":["kind","url"]}]},"FamilyFeedLink":{"type":"object","additionalProperties":false,"properties":{"text":{"type":"string"},"target":{"$ref":"#/components/schemas/FamilyFeedLinkTarget"}},"required":["text","target"]},"FamilyFeedRun":{"description":"One piece of a paragraph: plain text, a mention of the post's own camp, or a link.","oneOf":[{"type":"object","additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"]},{"type":"object","additionalProperties":false,"properties":{"camp":{"$ref":"#/components/schemas/FamilyFeedCampMention"}},"required":["camp"]},{"type":"object","additionalProperties":false,"properties":{"link":{"$ref":"#/components/schemas/FamilyFeedLink"}},"required":["link"]}]},"FamilyFeedImage":{"type":"object","additionalProperties":false,"properties":{"url":{"type":"string","description":"An image of the post's own activity or camp; a relative path for an uploaded brand image."},"alt":{"type":"string"},"position":{"type":["string","null"],"description":"A CSS object-position for cropping; null when unspecified."}},"required":["url","alt","position"]},"FamilyFeedPost":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["pick","camp","friend","local","event"]},"topic":{"type":"string","description":"A topic mark the app can draw."},"headline":{"type":"string"},"body":{"type":"array","description":"Paragraphs, each an array of runs.","items":{"type":"array","items":{"$ref":"#/components/schemas/FamilyFeedRun"}}},"image":{"description":"The post's photo: its activity's first photo (cover, else first gallery photo), else its camp's, else its pool item's own image. A logo is never a post's photo (a camp mention carries it as logoUrl), so a post without a photo has null and the app fills the slot.","oneOf":[{"type":"null"},{"$ref":"#/components/schemas/FamilyFeedImage"}]},"until":{"type":"string","format":"date","description":"The last family-local date the post is relevant: an event's or closure's end, or the offering's next session start."},"reason":{"type":"string","description":"Why the post is in the feed, one line."},"signal":{"type":["string","null"],"enum":["like","dislike","dont_show_again",null]},"done":{"type":"boolean","description":"Whether the viewer marked the post done in Plans' To do (PUT /api/users/viewer/feed/posts/{postId}/done)."},"organizationId":{"type":["string","null"],"format":"uuid","description":"The camp the post is about."},"activityId":{"type":["string","null"],"format":"uuid","description":"The activity the post is about."}},"required":["id","kind","topic","headline","body","image","until","reason","signal","done","organizationId","activityId"]},"FamilyFeedEdition":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"date":{"type":"string","format":"date","description":"The family-local date."},"slot":{"type":"string","enum":["morning","evening"]},"label":{"type":"string","description":"The edition's name in the household time zone, for example \"Monday morning\"."},"publishedAt":{"type":"string","format":"date-time"},"posts":{"type":"array","items":{"$ref":"#/components/schemas/FamilyFeedPost"}}},"required":["id","date","slot","label","publishedAt","posts"]},"FamilyFeedNote":{"type":"object","additionalProperties":false,"description":"The adult's daily note: one line that sets the mood for the day, a credited quote from a classic book or a small true fact, picked from the feed's pool on the adult's first read of the family-local date and the same on every read that date. greeting and pose follow the time of the read; the line does not.","properties":{"date":{"type":"string","format":"date","description":"The family-local date the line is for."},"id":{"type":"string","description":"The pool line's permanent id, or staff for a line staff wrote for the family's area and date."},"type":{"type":"string","enum":["quote","fact","welcome","staff"],"description":"quote (a short quote from a book first published before 1930, always shown with its credit), fact (a small true fact), welcome (while the adult has no children set up), or staff (a line staff wrote for the family's area and date)."},"greeting":{"type":"string","description":"The card's header for the time it is now in the family's time zone: Good morning, Good afternoon, Good evening, or on a weekend Good Saturday morning."},"text":{"type":"string","description":"The line: at most about 25 words and two sentences, no emoji."},"credit":{"type":["string","null"],"description":"A quote's author and book, shown under it: \"Lewis Carroll, Through the Looking-Glass\". Null for a fact, a welcome line, or a staff line."},"doodle":{"type":"string","enum":["air","art","animals","festival","food","general","library","music","park","rain","roads","school","science","seasonal","snow","sports","sun","theater","transit","water"],"description":"The illustration drawn beside the line, one of the feed's topic marks."},"pose":{"$ref":"#/components/schemas/FamilyFeedNotePose"},"seenAt":{"type":["string","null"],"format":"date-time","description":"When the adult first marked it seen."},"savedAt":{"type":["string","null"],"format":"date-time","description":"When the adult saved it to their Notes; null unless saved."}},"required":["date","id","type","greeting","text","credit","doodle","pose","seenAt","savedAt"]},"FamilyFeed":{"type":"object","additionalProperties":false,"properties":{"edition":{"description":"The current edition, also among editions.","$ref":"#/components/schemas/FamilyFeedEdition"},"editions":{"description":"The current edition's day: its morning edition, then its evening edition. Both are published on the day's first read, so the evening is out before it becomes current.","type":"array","minItems":2,"maxItems":2,"items":{"$ref":"#/components/schemas/FamilyFeedEdition"}},"note":{"$ref":"#/components/schemas/FamilyFeedNote"}},"required":["edition","editions","note"]},"FamilyNotificationTarget":{"description":"Where opening the notification leads.","oneOf":[{"type":"object","additionalProperties":false,"properties":{"kind":{"const":"booking","type":"string"},"bookingId":{"type":"string","format":"uuid"}},"required":["kind","bookingId"]},{"type":"object","additionalProperties":false,"properties":{"kind":{"const":"activity","type":"string"},"activityId":{"type":"string","format":"uuid"},"sessionId":{"type":["string","null"],"format":"uuid"}},"required":["kind","activityId","sessionId"]},{"type":"object","additionalProperties":false,"properties":{"kind":{"const":"organization","type":"string"},"organizationId":{"type":"string","format":"uuid"}},"required":["kind","organizationId"]},{"type":"object","additionalProperties":false,"properties":{"kind":{"const":"person","type":"string"},"userId":{"type":"string","format":"uuid"}},"required":["kind","userId"]},{"type":"object","additionalProperties":false,"properties":{"kind":{"const":"collection","type":"string"},"collectionId":{"type":"string","format":"uuid"}},"required":["kind","collectionId"]},{"type":"object","additionalProperties":false,"description":"A question about one of the family's plans (did you get a spot, or confirm a plan found in a registration email): open it on Plans (Home).","properties":{"kind":{"const":"plan","type":"string"},"planId":{"type":"string","format":"uuid"}},"required":["kind","planId"]},{"type":"object","additionalProperties":false,"properties":{"kind":{"const":"screen","type":"string"},"screen":{"type":"string","enum":["home","wallet","family","settings"]}},"required":["kind","screen"]},{"type":"object","required":["kind","event"],"properties":{"kind":{"const":"family","type":"string"},"event":{"type":"string","enum":["member_joined","invitation_declined","member_removed","role_changed","new_invite_requested"]},"userId":{"type":"string","format":"uuid","description":"The adult the event is about, when they have an account."},"invitationId":{"type":"string","format":"uuid","description":"The invitation the event is about."}},"additionalProperties":false,"description":"A change to the family's members (an account notification): open the family's members."}]},"FamilyNotification":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["booking","waitlist","friend","alert","account"]},"title":{"type":"string"},"body":{"type":"string"},"target":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/FamilyNotificationTarget"}]},"createdAt":{"type":"string","format":"date-time"},"readAt":{"type":["string","null"],"format":"date-time"}},"required":["id","kind","title","body","target","createdAt","readAt"]},"FamilyNotificationChannels":{"type":"object","additionalProperties":false,"properties":{"email":{"type":"boolean"},"push":{"type":"boolean"},"sms":{"type":"boolean"}},"required":["email","push","sms"],"description":"Whether each channel is on. Push goes to the family's signed-in browsers that registered a push subscription."},"FamilyNotificationPreferenceValues":{"type":"object","additionalProperties":false,"properties":{"channels":{"type":"object","additionalProperties":false,"properties":{"bookings":{"$ref":"#/components/schemas/FamilyNotificationChannels"},"waitlist":{"$ref":"#/components/schemas/FamilyNotificationChannels"},"schedule":{"$ref":"#/components/schemas/FamilyNotificationChannels"},"groups":{"$ref":"#/components/schemas/FamilyNotificationChannels"},"friends":{"$ref":"#/components/schemas/FamilyNotificationChannels"},"assistant":{"$ref":"#/components/schemas/FamilyNotificationChannels"},"receipts":{"$ref":"#/components/schemas/FamilyNotificationEmailChannel"}},"required":["bookings","waitlist","schedule"],"description":"Every channel of bookings, waitlist and schedule. `groups`, `friends`, `assistant` and `receipts` may be left out, each keeping its saved value (its default for a family that never set it)."},"quietHours":{"$ref":"#/components/schemas/FamilyNotificationQuietHours"},"productNews":{"type":"boolean","description":"Whether the family gets School's Out's marketing email (Product news), apart from product notifications. Stored only: nothing sends it yet."}},"required":["channels"],"description":"`quietHours` and `productNews` may be left out, keeping their saved values. Any other key is rejected."},"FamilyNotificationPreferences":{"type":"object","additionalProperties":false,"properties":{"channels":{"type":"object","additionalProperties":false,"properties":{"bookings":{"$ref":"#/components/schemas/FamilyNotificationChannels"},"waitlist":{"$ref":"#/components/schemas/FamilyNotificationChannels"},"schedule":{"$ref":"#/components/schemas/FamilyNotificationChannels"},"groups":{"$ref":"#/components/schemas/FamilyNotificationChannels"},"friends":{"$ref":"#/components/schemas/FamilyNotificationChannels"},"assistant":{"$ref":"#/components/schemas/FamilyNotificationChannels"},"receipts":{"$ref":"#/components/schemas/FamilyNotificationEmailChannel"}},"required":["bookings","waitlist","schedule","groups","friends","assistant","receipts"],"description":"Each notification row’s channels, and `receipts`: whether payment receipts are emailed (email only; on unless the family turns it off). Nothing publishes under `groups`, `friends` or `assistant` yet."},"quietHours":{"$ref":"#/components/schemas/FamilyNotificationQuietHours"},"productNews":{"type":"boolean","description":"Whether the family gets School's Out's marketing email (Product news), apart from product notifications. Stored only: nothing sends it yet."},"version":{"type":"integer","minimum":0,"description":"0 until the family first saves; a family at version 0 sees the defaults."}},"required":["channels","quietHours","productNews","version"]},"FamilyPushSubscriptionInput":{"type":"object","additionalProperties":false,"description":"A browser's PushSubscription.toJSON().","properties":{"endpoint":{"type":"string","format":"uri","maxLength":2048,"description":"The browser's push service URL (https, at a known browser push service)."},"expirationTime":{"type":["number","null"],"description":"Accepted and not kept."},"keys":{"$ref":"#/components/schemas/FamilyPushSubscriptionKeys"}},"required":["endpoint","keys"]},"FamilyPushSubscriptionKeys":{"type":"object","additionalProperties":false,"properties":{"p256dh":{"type":"string","description":"base64url uncompressed P-256 public key (65 bytes)."},"auth":{"type":"string","description":"base64url authentication secret (16 bytes)."}},"required":["p256dh","auth"]},"FamilyPushSubscription":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"endpoint":{"type":"string"},"createdAt":{"type":"string","format":"date-time","description":"When the sign-in session first subscribed."},"updatedAt":{"type":"string","format":"date-time","description":"When the sign-in session last subscribed."}},"required":["id","endpoint","createdAt","updatedAt"]},"FamilyPlanDayTime":{"type":"object","additionalProperties":false,"required":["date","startTime","endTime"],"description":"A plan's own start and end time on one date. A null time is none that day.","properties":{"date":{"type":"string","format":"date"},"startTime":{"anyOf":[{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$"},{"type":"null"}]},"endTime":{"anyOf":[{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$"},{"type":"null"}]}}},"FamilyStaffChargeFollowUp":{"type":"object","additionalProperties":false,"description":"A wallet top-up or gift card purchase whose PaymentIntent Stripe never named within its idempotency window: it was closed as unresolved instead of being resent, and only Stripe can tell whether the card was charged. Look it up there by stripeIdempotencyKey. Staff close it with PATCH /api/admin/family-bookings/charges/{kind}/{id}/follow-up.","required":["kind","id","ownerUserId","stripeIdempotencyKey","amountCents","currency","createdAt","updatedAt"],"properties":{"kind":{"type":"string","enum":["top_up","gift_card"],"x-open-enum":true},"id":{"type":"string","format":"uuid"},"ownerUserId":{"type":["string","null"],"format":"uuid","description":"The family (or gift card purchaser); null once the account is gone."},"stripeIdempotencyKey":{"type":"string","description":"The Idempotency-Key every attempt sent to Stripe: wallet-top-up-\u003cid\u003e or gift-card-\u003cid\u003e."},"amountCents":{"type":"integer","minimum":1},"currency":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time","description":"When it was closed as unresolved."}}},"FamilyStaffChargeResolution":{"type":"object","additionalProperties":false,"description":"How staff closed an unresolved saved-card charge.","required":["kind","id","resolution","note"],"properties":{"kind":{"type":"string","enum":["top_up","gift_card"],"x-open-enum":true},"id":{"type":"string","format":"uuid"},"resolution":{"type":"string","enum":["refunded","written_off","resolved_in_stripe"],"x-open-enum":true},"note":{"type":"string"}}},"FamilyPlanActivity":{"type":"object","required":["id","name","imageUrl"],"additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"imageUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"description":"A catalog activity as a calendar entry shows it: its name and first photo (its cover, else its first gallery photo; null without one)."},"FamilyPlanOrganization":{"type":"object","required":["id","name","logoUrl"],"additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"logoUrl":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The camp's selected logo; null when it has none. Once rendered it is the logo's public, cross-origin rendition (GET /api/media/logos/{file}), else its source URL."}},"description":"A camp as an idea or a registration alert shows it: its name and selected logo (null without one)."},"FamilyPlanProviderTask":{"type":"object","required":["label","url"],"additionalProperties":false,"properties":{"label":{"type":"string"},"url":{"type":"string"}},"description":"Something the parent must still do on the provider's site, and where."},"FamilyPlanIdea":{"type":"object","required":["activityId","sessionId","title","organization","price","startTime","endTime","friends"],"additionalProperties":false,"properties":{"activityId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"sessionId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"title":{"type":"string"},"organization":{"anyOf":[{"$ref":"#/components/schemas/FamilyPlanOrganization"},{"type":"null"}]},"price":{"anyOf":[{"$ref":"#/components/schemas/BookingMoney"},{"type":"null"}],"description":"The session's price when it states one quote for the whole session, as a booking's listed price reads it; null otherwise."},"startTime":{"anyOf":[{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$"},{"type":"null"}],"description":"The session's daily start time when every day keeps the same hours; null otherwise."},"endTime":{"anyOf":[{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$"},{"type":"null"}]},"friends":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanCalendarFriend"},"description":"Friends who share their plans and have a booking or registered plan for the idea's session."}},"description":"An option for a day nothing covers yet: a saved option, a followed session or a catalog suggestion for the child, with what a screen books from. The catalog parts (organization, price, times) are null for a record no longer in the catalog. At most a few per day or group (policy), one per session."},"FamilyPlanHomeFriend":{"type":"object","required":["userId","publicName","publicNameKnown","photoUrl"],"additionalProperties":false,"properties":{"userId":{"type":"string","format":"uuid"},"publicName":{"type":["string","null"]},"publicNameKnown":{"type":"boolean","description":"False when the parent has no name to show; publicName is then null and the client writes its own wording."},"photoUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}}},"FamilyBookingPolicy":{"type":"object","description":"The booking money policy: School’s Out’s service fee per child, which every new booking carries (one booking is one child), and the group rate, which refunds one child’s fee to each family’s wallet once two or more families’ bookings for a session are confirmed.","required":["serviceFee","groupRate"],"additionalProperties":false,"properties":{"serviceFee":{"type":"object","required":["perChild"],"additionalProperties":false,"properties":{"perChild":{"$ref":"#/components/schemas/BookingMoney","description":"Charged per child, per booking, when it is booked or applied."}}},"groupRate":{"oneOf":[{"type":"object","required":["perFamily","families"],"additionalProperties":false,"properties":{"perFamily":{"$ref":"#/components/schemas/BookingMoney","description":"Credited once per family and session, whatever its number of children: one child’s service fee."},"families":{"type":"integer","minimum":2,"description":"How many families’ confirmed bookings for one session earn it."}}},{"type":"null"}],"description":"Null when there is no service fee to refund."}}},"FamilyNotificationEmailChannel":{"type":"object","additionalProperties":false,"properties":{"email":{"type":"boolean"}},"required":["email"],"description":"Whether the row is sent by email; it has no push or SMS."},"FamilyNotificationQuietHours":{"type":"object","additionalProperties":false,"properties":{"on":{"type":"boolean"},"from":{"type":"integer","minimum":0,"maximum":23,"description":"The hour quiet hours start, on a 24-hour clock in the household's time zone; quiet hours never hold a household without one."},"to":{"type":"integer","minimum":0,"maximum":23,"description":"The hour they end; never equal to `from`, and earlier than it when the window wraps past midnight."}},"required":["on","from","to"],"description":"While on, email, push and SMS wait until the window ends; the inbox entry is stored at once. Registration reminders (registration or spots opening at a followed camp) come through them."},"FamilyBookingQuoteInput":{"type":"object","additionalProperties":false,"required":["sessionId","childProfileIds"],"description":"What booking a session for some children would cost, with the payment choices an approval takes.","properties":{"sessionId":{"type":"string","format":"uuid"},"childProfileIds":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","format":"uuid"},"description":"The children in checkout order. Credits are applied to them in this order."},"maxSpend":{"$ref":"#/components/schemas/BookingMoney","description":"The spending limit each approval will send, validated as an approval's (positive, at most 2500000 cents, the listed price's currency when the session lists one). Omit to quote the session's listed price; without either, every child's `money` is null."},"useCreditsCents":{"type":"integer","minimum":1,"description":"Wallet credits to spend across the children, in USD cents. More than the wallet holds is capped at the balance. Omit to spend none."},"paymentMethod":{"type":"string","enum":["default","backup"],"description":"The saved card the approvals will hold. Defaults to `default`."}}},"FamilyBookingQuoteChild":{"type":"object","additionalProperties":false,"required":["childProfileId","childProfileVersion","eligibility","wouldWaitlist","money"],"description":"One child's booking as an approval would make it.","properties":{"childProfileId":{"type":"string","format":"uuid"},"childProfileVersion":{"type":"integer","minimum":1,"description":"The profile version to send as the approval's `childProfileVersion`."},"eligibility":{"type":"object","additionalProperties":false,"required":["alreadyBooked","reuseConsentMissing","missingFields"],"properties":{"alreadyBooked":{"type":"boolean","description":"The child already has a booking in progress or confirmed for this session; an approval is refused (409). Such a child has no `money` and takes no credits."},"reuseConsentMissing":{"type":"boolean","description":"The profile does not allow reuse yet. The approval must send `reuseAllowed: true` (only the profile's owner may), or it is refused (409)."},"missingFields":{"type":"array","items":{"$ref":"#/components/schemas/BookingAlwaysSharedField"},"description":"Fields of the session's booking form (`alwaysShared`) the booking would send empty: the child's from their profile, the guardian's from the account of the adult quoting (the owner or a co-parent), exactly as the approval reads them. Save the child's with the approval's `profileFields` and the guardian's on the account, or the approval is refused (400)."}}},"wouldWaitlist":{"type":["boolean","null"],"description":"Whether this child would take a waitlist spot, counting the places the children before it take: true when the session is waitlisting or its places run out, false when an open session still has a place. Null when the catalog cannot tell (no capacity listed); Hallpass decides then. A waitlist spot is never charged; its hold is released."},"money":{"oneOf":[{"type":"object","additionalProperties":false,"required":["maxSpend","serviceFee","creditsApplied","cardHold","stripeMinimumApplied"],"properties":{"maxSpend":{"$ref":"#/components/schemas/BookingMoney","description":"The spending limit: `maxSpend` as sent, else the session's listed price."},"serviceFee":{"$ref":"#/components/schemas/BookingMoney","description":"The service fee the booking carries, charged when it is booked or applied."},"creditsApplied":{"$ref":"#/components/schemas/BookingMoney","description":"The credits to send as this approval's `useCreditsCents` (omit it when zero)."},"cardHold":{"$ref":"#/components/schemas/BookingMoney","description":"What the approval holds on the card: the limit plus the fee, less the credits. Equals the booking's `payment.cardHoldCents`."},"stripeMinimumApplied":{"type":"boolean","description":"The card part left after credits is below Stripe's minimum, so the hold is raised to it; capture still charges only what is owed."}}},{"type":"null"}],"description":"Null when the child is already booked, or when no `maxSpend` was sent and the session has no single listed session price."}}},"FamilyBookingQuote":{"type":"object","additionalProperties":false,"required":["sessionId","children","credits","paymentMethod","totals"],"description":"Checkout totals from the API, built from the same functions approval and settlement use. Each child is one booking; send one approval per child with its `maxSpend`, `childProfileVersion` and `creditsApplied`. Reading a quote books and holds nothing.","properties":{"sessionId":{"type":"string","format":"uuid"},"children":{"type":"array","items":{"$ref":"#/components/schemas/FamilyBookingQuoteChild"},"description":"In the order asked."},"credits":{"type":"object","additionalProperties":false,"required":["available","requested","applied"],"properties":{"available":{"$ref":"#/components/schemas/BookingMoney","description":"The wallet balance."},"requested":{"oneOf":[{"$ref":"#/components/schemas/BookingMoney"},{"type":"null"}],"description":"`useCreditsCents` as sent; null without it."},"applied":{"$ref":"#/components/schemas/BookingMoney","description":"Credits applied across the children, first child first, each at most its limit plus fee. None for a session not priced in USD."}}},"paymentMethod":{"type":"object","additionalProperties":false,"required":["choice","saved"],"properties":{"choice":{"type":"string","enum":["default","backup"]},"saved":{"type":"boolean","description":"The account has that saved card. Without it, an approval that needs a hold fails with `payment_method_required`."}}},"totals":{"type":"object","additionalProperties":false,"required":["maxSpend","serviceFees","credits","cardHolds"],"description":"Sums over the children with `money`.","properties":{"maxSpend":{"$ref":"#/components/schemas/BookingMoney"},"serviceFees":{"$ref":"#/components/schemas/BookingMoney"},"credits":{"$ref":"#/components/schemas/BookingMoney"},"cardHolds":{"$ref":"#/components/schemas/BookingMoney"}}}}},"FamilyMemberRole":{"type":"string","enum":["co_parent","caregiver"],"description":"A member's role. co_parent: books (the owner's card pays), sees payments and credits, edits children and plans. caregiver: sees the granted children's care view, schedules and plans; no money, no edits."},"FamilyRole":{"type":"string","enum":["owner","co_parent","caregiver"],"description":"The viewer's role in the family they act in. The owner owns the children, bookings, wallet and cards, and pays."},"FamilyAdult":{"type":"object","required":["userId","name","email","you"],"properties":{"userId":{"type":"string","format":"uuid"},"name":{"type":["string","null"],"description":"The account's full name; null when it has none."},"email":{"type":["string","null"],"format":"email","description":"Null for a caregiver viewer, who sees the family's adults by name only."},"you":{"type":"boolean"}}},"FamilyMember":{"allOf":[{"$ref":"#/components/schemas/FamilyAdult"},{"type":"object","required":["role","relation","allChildren","childIds","joinedAt"],"properties":{"role":{"$ref":"#/components/schemas/FamilyMemberRole"},"relation":{"type":["string","null"],"description":"How the member relates to the family, as the owner wrote it (Grandparent)."},"allChildren":{"type":"boolean","description":"The member sees every child, including children added later."},"childIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"The children the member sees, oldest first."},"joinedAt":{"type":"string","format":"date-time"}}}]},"FamilyInvitationStatus":{"type":"string","enum":["pending","accepted","declined","canceled","expired"],"description":"Derived from the row: canceled by the owner, expired after expiresAt (also when a newer invitation to the address replaced an expired one)."},"FamilyInvitation":{"type":"object","required":["id","email","name","role","relation","allChildren","childIds","status","createdAt","sentAt","expiresAt","declinedAt","newInviteRequestedAt"],"properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"name":{"type":["string","null"],"description":"The invitee's name as the owner wrote it."},"role":{"$ref":"#/components/schemas/FamilyMemberRole"},"relation":{"type":["string","null"]},"allChildren":{"type":"boolean"},"childIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"The invited children still in the family; every child of the family when allChildren."},"status":{"$ref":"#/components/schemas/FamilyInvitationStatus"},"createdAt":{"type":"string","format":"date-time"},"sentAt":{"type":"string","format":"date-time","description":"The latest send; a resend or copied link moves it and expiresAt."},"expiresAt":{"type":"string","format":"date-time"},"declinedAt":{"type":["string","null"],"format":"date-time"},"newInviteRequestedAt":{"type":["string","null"],"format":"date-time","description":"When the invitee asked for a new invitation."}}},"FamilyMembers":{"type":"object","required":["owner","members","invitations","viewerRole"],"properties":{"owner":{"$ref":"#/components/schemas/FamilyAdult"},"members":{"type":"array","items":{"$ref":"#/components/schemas/FamilyMember"},"description":"By join time."},"invitations":{"type":"array","items":{"$ref":"#/components/schemas/FamilyInvitation"},"description":"Open invitations (pending or expired) and those declined in the last seven days, newest first. Empty for a caregiver viewer."},"viewerRole":{"$ref":"#/components/schemas/FamilyRole"}}},"FamilyInvitationPreview":{"type":"object","required":["status","inviterName","householdName","role","relation","name","email","childFirstNames","adults","children","expiresAt","newInviteRequestedAt"],"properties":{"status":{"$ref":"#/components/schemas/FamilyInvitationStatus"},"inviterName":{"type":"string","description":"The owner's full name, else the local part of their email."},"householdName":{"type":"string","description":"\"Okafor family\" from the owner's surname, else \"jordan’s family\"."},"role":{"$ref":"#/components/schemas/FamilyMemberRole"},"relation":{"type":["string","null"],"description":"Null unless pending."},"name":{"type":["string","null"],"description":"The invitee's name as the owner wrote it; null unless pending."},"email":{"type":["string","null"],"description":"The invited address masked (r***@example.com); null unless pending."},"childFirstNames":{"type":"array","items":{"type":"string"},"description":"The invited children's first names; empty unless pending."},"adults":{"type":"integer","minimum":0,"description":"The family's adults (owner and members); 0 unless pending."},"children":{"type":"integer","minimum":0,"description":"The family's children; 0 unless pending."},"expiresAt":{"type":"string","format":"date-time"},"newInviteRequestedAt":{"type":["string","null"],"format":"date-time"}}},"FamilyBookingSchedule":{"type":"object","additionalProperties":false,"required":["id","phase","status","childProfileId","organization","activity","session","progressStage","hold","pollAfterMs","waitlist","canceledBy","note","replacesBookingId","replacedByBookingId","bookedBy","createdAt","updatedAt"],"description":"A booking as a caregiver (a role without read_money) sees it: the schedule, with no money, approval, decisions, payment, refund or events.","properties":{"id":{"type":"string","format":"uuid"},"phase":{"$ref":"#/components/schemas/BookingPhase"},"status":{"$ref":"#/components/schemas/BookingStatus"},"childProfileId":{"type":["string","null"],"format":"uuid","description":"`null` after the profile is deleted. The booking itself remains."},"organization":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}},"activity":{"anyOf":[{"type":"object","required":["id","name"],"additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]}}},{"type":"null"}],"description":"The booked session's catalog activity, as it was at approval like organization and session; null when the session had none. A change of session stays within it."},"session":{"type":"object","required":["id","name","startDate","endDate","startTime","endTime","dates","dayTimes"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"startDate":{"type":["string","null"],"format":"date","description":"First day of the session."},"endDate":{"type":["string","null"],"format":"date","description":"Last day of the session."},"startTime":{"type":["string","null"],"pattern":"^[0-9]{2}:[0-9]{2}$","description":"Daily start time (HH:MM) when the session has one; snapshotted at approval."},"endTime":{"type":["string","null"],"pattern":"^[0-9]{2}:[0-9]{2}$","description":"Daily end time (HH:MM) when the session has one."},"dates":{"type":"array","maxItems":60,"items":{"type":"string","format":"date"},"description":"Published attendance dates, at most 60; empty when the camp published only a date range."},"dayTimes":{"type":"array","maxItems":60,"items":{"$ref":"#/components/schemas/FamilyPlanDayTime"},"description":"The days whose own times replace startTime and endTime, by date, snapshotted at approval: a day with different hours, or an overnight stay's drop-off (first day, startTime only) and pick-up (last day, endTime only) with neither between. Empty when every day keeps startTime and endTime."}}},"progressStage":{"type":["string","null"],"enum":["resolving_session","filling_form","reviewing_total","paying","confirming",null],"x-open-enum":true,"description":"The furthest checkout stage Hallpass reported, or `null` before it reports one. New stages may be added."},"hold":{"description":"Why a running run is waiting on something nobody can answer: Pilgrim holding an earlier task for a person's review, Pilgrim's assistance switched off at its Mac, or Hallpass retrying its worker. `null` while the run moves on its own.","oneOf":[{"type":"null"},{"type":"object","additionalProperties":false,"required":["code","message","since"],"properties":{"code":{"type":"string","enum":["pilgrim_queue_held","pilgrim_assistance_off","executor_retrying"],"x-open-enum":true},"message":{"type":"string","minLength":1,"maxLength":500,"description":"What the run waits for, written for the requester."},"since":{"type":"string","format":"date-time"}}}]},"pollAfterMs":{"type":["integer","null"],"description":"How long to wait before reading the booking again. `null` once nothing will change without the parent."},"waitlist":{"oneOf":[{"type":"object","required":["position","updatedAt"],"properties":{"position":{"type":["integer","null"],"minimum":1,"description":"The place the camp reported, 1 is next; `null` when it gave none."},"updatedAt":{"type":"string","format":"date-time"}}},{"type":"null"}]},"canceledBy":{"type":["string","null"],"enum":["parent","provider","system",null],"description":"Who cancelled a cancelled booking."},"note":{"type":["string","null"],"maxLength":1000,"description":"What the family should know for this booking, such as what to bring, from the provider’s email. An undone email update puts back the note it replaced."},"replacesBookingId":{"type":["string","null"],"format":"uuid","description":"The booking this one changes to another session."},"replacedByBookingId":{"type":["string","null"],"format":"uuid","description":"The live booking changing this one. This booking stays until that one is booked."},"bookedBy":{"anyOf":[{"type":"object","required":["userId","name"],"properties":{"userId":{"type":"string","format":"uuid"},"name":{"type":["string","null"]}},"additionalProperties":false},{"type":"null"}],"description":"The family member (co-parent) who approved the booking; null when the family's owner did. The booking, its card hold and its payments are the owner's."},"createdAt":{"type":"string","format":"date-time","description":"When the booking was created."},"updatedAt":{"type":"string","format":"date-time","description":"When the booking last changed."}}},"FamilyFeedInstructions":{"type":"object","additionalProperties":false,"required":["instructions","defaultInstructions"],"properties":{"instructions":{"type":["string","null"],"minLength":1,"maxLength":4000,"description":"The signed-in adult's own words for what their feed should carry; null while they read the default feed. Every adult in a family (owner, co-parent, caregiver) has their own: they shape only that adult's feed and its \"You asked for …\" items."},"defaultInstructions":{"type":"string","description":"Read-only. The feed's wording of what it carries for this adult while they have written none, from the family's area and the children the adult sees. Saving null returns the adult to it; saving this text is allowed and changes nothing the feed does."}}},"FamilyFeedInstructionsInput":{"type":"object","additionalProperties":false,"required":["instructions"],"properties":{"instructions":{"type":["string","null"],"minLength":1,"maxLength":4000,"description":"The adult's own words, trimmed; 1 to 4000 characters. null removes them and returns the adult to the default feed."}}},"FamilyAccountCalendarAdult":{"type":"object","additionalProperties":false,"required":["userId","name"],"description":"The family adult whose calendar an event is in.","properties":{"userId":{"type":"string","format":"uuid"},"name":{"type":["string","null"],"description":"The account's full name; null when it has none."}}},"FamilyGroupPerson":{"type":"object","required":["userId","publicName","publicNameKnown","photoUrl"],"properties":{"userId":{"type":"string","format":"uuid"},"publicName":{"type":["string","null"]},"publicNameKnown":{"type":"boolean","description":"False when the parent has no name to show; publicName is then null and the client writes its own wording."},"photoUrl":{"type":["string","null"],"description":"The photo the parent uploaded (an API path carrying its hash), else their Google picture (an absolute URL); null when there is neither."}},"additionalProperties":false,"description":"A parent as other parents see them: public name and photo, never the email."},"FamilyGroupLastMessage":{"type":"object","required":["author","sender","text","createdAt"],"properties":{"author":{"enum":["parent","assistant"]},"sender":{"oneOf":[{"$ref":"#/components/schemas/FamilyGroupPerson"},{"type":"null"}]},"text":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}},"additionalProperties":false,"description":"The group's newest message the viewer can read."},"FamilyGroup":{"type":"object","required":["id","name","owned","memberCount","lastSeq","lastReadSeq","unread","lastMessage","joinedAt","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":80},"memberCount":{"type":"integer","minimum":1},"lastSeq":{"type":"integer","minimum":0,"description":"The seq of the group's newest message."},"lastReadSeq":{"type":"integer","minimum":0,"description":"The newest message the viewer has read."},"unread":{"type":"integer","minimum":0,"maximum":100,"description":"Messages from others after lastReadSeq, counted up to 100."},"lastMessage":{"oneOf":[{"$ref":"#/components/schemas/FamilyGroupLastMessage"},{"type":"null"}]},"joinedAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"owned":{"type":"boolean","description":"You own the group, so you may rename and delete it. The owner is its creator while they are a member, else its longest-standing member."}},"additionalProperties":false},"FamilyGroupMember":{"type":"object","required":["userId","publicName","publicNameKnown","photoUrl","joinedAt"],"properties":{"userId":{"type":"string","format":"uuid"},"publicName":{"type":["string","null"]},"publicNameKnown":{"type":"boolean","description":"False when the parent has no name to show; publicName is then null and the client writes its own wording."},"photoUrl":{"type":["string","null"],"description":"The photo the parent uploaded (an API path carrying its hash), else their Google picture (an absolute URL); null when there is neither."},"joinedAt":{"type":"string","format":"date-time"}},"additionalProperties":false},"FamilyGroupSentInvitation":{"type":"object","required":["id","invitee","invitedBy","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"invitee":{"$ref":"#/components/schemas/FamilyGroupPerson"},"invitedBy":{"$ref":"#/components/schemas/FamilyGroupPerson"},"createdAt":{"type":"string","format":"date-time"}},"additionalProperties":false},"FamilyGroupDetail":{"type":"object","required":["id","name","owned","memberCount","lastSeq","lastReadSeq","unread","lastMessage","joinedAt","createdAt","members","invitations"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":80},"memberCount":{"type":"integer","minimum":1},"lastSeq":{"type":"integer","minimum":0,"description":"The seq of the group's newest message."},"lastReadSeq":{"type":"integer","minimum":0,"description":"The newest message the viewer has read."},"unread":{"type":"integer","minimum":0,"maximum":100,"description":"Messages from others after lastReadSeq, counted up to 100."},"lastMessage":{"oneOf":[{"$ref":"#/components/schemas/FamilyGroupLastMessage"},{"type":"null"}]},"joinedAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"members":{"type":"array","items":{"$ref":"#/components/schemas/FamilyGroupMember"},"description":"In the order they joined."},"invitations":{"type":"array","items":{"$ref":"#/components/schemas/FamilyGroupSentInvitation"},"description":"Open invitations, oldest first."},"owned":{"type":"boolean","description":"You own the group, so you may rename and delete it. The owner is its creator while they are a member, else its longest-standing member."}},"additionalProperties":false},"FamilyGroupInvitation":{"type":"object","required":["id","groupId","groupName","memberCount","invitedBy","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"groupId":{"type":"string","format":"uuid"},"groupName":{"type":"string"},"memberCount":{"type":"integer","minimum":1},"invitedBy":{"$ref":"#/components/schemas/FamilyGroupPerson"},"createdAt":{"type":"string","format":"date-time"}},"additionalProperties":false},"FamilyGroupMessage":{"type":"object","required":["id","seq","author","sender","mine","clientMessageId","text","assistantStatus","replyToMessageId","assistant","shared","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"seq":{"type":"integer","minimum":1,"description":"The message's place in the group: every member sees one order."},"author":{"enum":["parent","assistant"]},"sender":{"oneOf":[{"$ref":"#/components/schemas/FamilyGroupPerson"},{"type":"null"}]},"mine":{"type":"boolean"},"clientMessageId":{"type":["string","null"],"description":"Your own message's idempotency key; null on others' messages."},"text":{"type":"string","description":"The sender's words; empty on a share sent without a note."},"assistantStatus":{"enum":["pending","answered","failed",null],"description":"For a parent's message that asked Assistant: pending until its reply arrives; null otherwise."},"replyToMessageId":{"type":["string","null"],"format":"uuid","description":"On Assistant's reply, the question it answers."},"assistant":{"type":["object","null"],"description":"On Assistant's reply: failed, and when it answered its results, sources, limitations and suggestedReplies as POST /api/assistant returns them.","additionalProperties":true},"createdAt":{"type":"string","format":"date-time"},"shared":{"oneOf":[{"$ref":"#/components/schemas/FamilyGroupShared"},{"type":"null"}]}},"additionalProperties":false},"FamilyGroupMessages":{"type":"object","required":["messages","latestSeq","lastReadSeq","hasEarlier","assistantPending"],"properties":{"messages":{"type":"array","items":{"$ref":"#/components/schemas/FamilyGroupMessage"},"description":"Oldest first."},"latestSeq":{"type":"integer","minimum":0,"description":"The group's newest seq: poll with after=latestSeq."},"lastReadSeq":{"type":"integer","minimum":0},"hasEarlier":{"type":"boolean","description":"Older messages you can read exist before this page."},"assistantPending":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Questions Assistant is still answering. One that leaves this list without a reply was lost."}},"additionalProperties":false},"FamilyChildDocument":{"type":"object","required":["id","childId","title","fileName","contentType","byteSize","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"childId":{"type":"string","format":"uuid"},"title":{"type":"string","minLength":1,"maxLength":120},"fileName":{"type":"string","minLength":1,"maxLength":200},"contentType":{"enum":["application/pdf","image/jpeg","image/png"]},"byteSize":{"type":"integer","minimum":1},"createdAt":{"type":"string","format":"date-time"}},"additionalProperties":false},"FamilyProviderAccount":{"type":"object","additionalProperties":false,"required":["platform","provider","site","usernameDisplay","createdAt","lastUsedAt"],"properties":{"platform":{"type":"string","enum":["coursestorm"],"x-open-enum":true},"provider":{"type":"string","description":"The provider's name for display, such as CourseStorm."},"site":{"type":"string","format":"uri"},"usernameDisplay":{"type":"string","description":"Masked, such as r…@example.com."},"createdAt":{"type":"string","format":"date-time","description":"When Hallpass created the account at the provider and kept it for the adult."},"lastUsedAt":{"type":["string","null"],"format":"date-time","description":"When a later booking last attached it."}}},"FamilyFeedNotePose":{"type":"object","additionalProperties":false,"description":"How Mellow is drawn beside the note, from the time of the read in the family's time zone.","properties":{"time":{"type":"string","enum":["morning","afternoon","evening","late"],"description":"morning from 5 am, afternoon from noon, evening from 5 pm, late from 9 pm."},"season":{"type":"string","enum":["spring","summer","autumn","winter"]}},"required":["time","season"]},"FamilyFeedSavedNote":{"type":"object","additionalProperties":false,"description":"A line in the adult's Notes.","properties":{"date":{"type":"string","format":"date"},"id":{"type":"string"},"type":{"type":"string","enum":["quote","fact","welcome","staff"]},"text":{"type":"string"},"credit":{"type":["string","null"],"description":"A quote's author and book, shown under it: \"Lewis Carroll, Through the Looking-Glass\". Null for a fact, a welcome line, or a staff line."},"doodle":{"type":"string","enum":["air","art","animals","festival","food","general","library","music","park","rain","roads","school","science","seasonal","snow","sports","sun","theater","transit","water"]},"savedAt":{"type":"string","format":"date-time"}},"required":["date","id","type","text","credit","doodle","savedAt"]},"FamilyEmailCheckUnresolved":{"type":"object","additionalProperties":false,"required":["messageId","organizationId","organizationName","sessionId","activityId","reason"],"description":"A registration-like email from an organization the viewer showed interest in that the check did not record. Ask the parent with the client's own \"Did you register?\" flow.","properties":{"messageId":{"type":"string","description":"The Gmail message id."},"organizationId":{"type":"string","format":"uuid"},"organizationName":{"type":"string"},"sessionId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"The matched session when the reason is child; null otherwise."},"activityId":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"reason":{"enum":["session","child"],"description":"session: no single upcoming session matched. child: the email names none of the viewer's children and they see more than one."}}},"FamilyExpoPushSubscriptionInput":{"type":"object","additionalProperties":false,"description":"A native app's Expo push token, from expo-notifications getExpoPushTokenAsync().","properties":{"kind":{"const":"expo","type":"string"},"token":{"type":"string","pattern":"^(ExponentPushToken|ExpoPushToken)\\[[A-Za-z0-9_-]{1,200}\\]$","description":"The Expo push token, ExponentPushToken[...]."},"platform":{"type":"string","enum":["ios","android"]},"deviceName":{"type":["string","null"],"maxLength":100,"description":"Optional; shown in the family's own list. Blank is null."}},"required":["kind","token","platform"]},"FamilyExpoPushSubscription":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"kind":{"const":"expo","type":"string"},"token":{"type":"string"},"platform":{"type":"string","enum":["ios","android"]},"deviceName":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time","description":"When the sign-in session first subscribed."},"updatedAt":{"type":"string","format":"date-time","description":"When the sign-in session last subscribed."}},"required":["id","kind","token","platform","deviceName","createdAt","updatedAt"]},"FamilyRegistrationQuestion":{"type":"object","required":["key","label","hint","control","options"],"additionalProperties":false,"description":"A question the camp's current registration form requires that the registration has not answered.","properties":{"key":{"type":"string","description":"Where the answer is stored, \"\u003csection\u003e.\u003cfield\u003e\": a canonical attendee field (child, guardian, secondaryGuardian or emergencyContact) or answers.\u003cquestion key\u003e for the camp's own question. Send the answer under this key."},"label":{"type":"string","description":"The question as the form asks it."},"hint":{"type":"string","description":"The form's help text; empty when none."},"control":{"enum":["text","textarea","email","phone","date","number","choice","multi-choice","yes-no"],"description":"How the form asks it. date is YYYY-MM-DD; choice takes one of options; multi-choice a list of them; yes-no is \"Yes\" or \"No\"; text and the others are text (up to 200 characters, textarea 2000)."},"options":{"type":"array","items":{"type":"string"},"description":"The offered choices for choice and multi-choice; empty otherwise."}}},"FamilyRegistrationQuestions":{"type":"object","required":["registration","formVersion","questions"],"additionalProperties":false,"properties":{"registration":{"type":"object","required":["id","organizationId","organizationName","confirmationCode","childProfileId","status"],"additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"organizationName":{"type":"string"},"confirmationCode":{"type":"string"},"childProfileId":{"type":["string","null"],"format":"uuid","description":"The saved child profile the registration was made from; null after that profile was removed."},"status":{"enum":["enrolled","waitlisted"],"description":"A cancelled registration is not available here."}}},"formVersion":{"type":"string","description":"The camp's form as read; send it back with the answers. Empty when the camp has no form (and so no questions)."},"questions":{"type":"array","items":{"$ref":"#/components/schemas/FamilyRegistrationQuestion"},"description":"The questions still missing, in form order; empty when the registration answers everything the form requires."}}},"FamilyGroupPlanAnswer":{"type":"object","required":["member","answer"],"properties":{"member":{"$ref":"#/components/schemas/FamilyGroupPerson"},"answer":{"enum":["yes","maybe","no"]}},"additionalProperties":false},"FamilyGroupPlan":{"type":"object","required":["id","groupId","title","note","date","time","activity","createdBy","mine","removable","answers","yourAnswer","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"groupId":{"type":"string","format":"uuid"},"title":{"type":"string","minLength":1,"maxLength":120},"note":{"type":["string","null"],"maxLength":1000},"date":{"type":"string","format":"date","description":"The day of the plan."},"time":{"type":["string","null"],"pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","description":"Local time on date (HH:MM, 24-hour), as entered; null when the plan has no set time."},"activity":{"oneOf":[{"$ref":"#/components/schemas/FamilySocialItemSummary"},{"type":"null"}],"description":"The catalog activity the plan is for; available false once the catalog no longer shows it."},"createdBy":{"oneOf":[{"$ref":"#/components/schemas/FamilyGroupPerson"},{"type":"null"}],"description":"Who proposed the plan; null once their account is deleted."},"mine":{"type":"boolean","description":"You proposed the plan."},"removable":{"type":"boolean","description":"You may delete the plan: you proposed it or you own the group."},"answers":{"type":"array","items":{"$ref":"#/components/schemas/FamilyGroupPlanAnswer"},"description":"Members' answers, oldest first. A member who leaves the group takes their answer with them."},"yourAnswer":{"enum":["yes","maybe","no",null]},"createdAt":{"type":"string","format":"date-time"}},"additionalProperties":false,"description":"A plan the group is making: a day, an optional time and activity, and who's going."},"FamilyGroupShared":{"type":"object","required":["kind","id","title","available","collection","activity","plan"],"properties":{"kind":{"enum":["collection","activity","plan"]},"id":{"type":["string","null"],"format":"uuid","description":"The shared item; null once a shared collection or plan is deleted."},"title":{"type":"string","minLength":1,"maxLength":200,"description":"The item's name when it was shared."},"available":{"type":"boolean","description":"You can open the item now: a collection you own or may open, an activity the catalog shows, a plan the group still has. Otherwise show the title alone."},"collection":{"oneOf":[{"$ref":"#/components/schemas/FamilySocialCollectionSummary"},{"type":"null"}],"description":"The collection card, when kind is collection and available."},"activity":{"oneOf":[{"$ref":"#/components/schemas/FamilySocialItemSummary"},{"type":"null"}],"description":"The activity card, when kind is activity."},"plan":{"oneOf":[{"$ref":"#/components/schemas/FamilyGroupPlan"},{"type":"null"}],"description":"The plan, when kind is plan and available."}},"additionalProperties":false,"description":"What a message shares, shown as a card."}},"securitySchemes":{"AccountCredential":{"description":"Current account credential. HTTP APIs and research tools do not accept SQL statements. Intelligence alternatively uses its signed session cookie; cookie POST requests require the session-bound CSRF token and trusted origin. Family routes accept the account credential only with x-session-id naming a live session of that account.","in":"header","name":"x-api-key","type":"apiKey"},"ApiKeyAuth":{"description":"Account credential or named machine key for integrations. Keep it server-side and send it in x-api-key. Intelligence browser requests separately support the existing signed session cookie; cookie mutations require CSRF verification. Family routes read family data only through a session-backed credential: the cookie naming a live session, or the account credential with x-session-id naming a live session of that account. The catalog-only consumer site key (SCHOOLS_OUT_CATALOG_API_KEY, held by the parent site's server) reads only the catalog list, facets and detail routes through the family projection with no household; public GET routes serve it as an anonymous visitor, every route that needs an account answers it 403, and with x-session-id or x-viewer-id it is 401.","in":"header","name":"x-api-key","type":"apiKey"},"CrawlerCapability":{"type":"http","scheme":"bearer","description":"Short-lived staff capability issued by POST /api/ingestion/crawler-worker/access. Bound to the exact worker ID and browser Origin. Send to that worker origin only; API cookies and x-api-key do not authenticate worker requests."},"CrawlerPublisher":{"type":"http","scheme":"bearer","description":"Dedicated server-only CRAWLER_SYNC_TOKEN. This credential publishes counts and ready URL snapshots; browser Origin requests are rejected."},"PilgrimFleetReporter":{"type":"http","scheme":"bearer","description":"Dedicated manager-only PILGRIM_FLEET_REPORT_TOKEN. Grants only sanitized node-status publication."},"OnboardingKey":{"type":"apiKey","in":"header","name":"x-onboarding-key","description":"A key from ONBOARDING_PUBLIC_KEYS, held by the parent app for its signed-out first-time onboarding. It opens only the /api/onboarding routes; it is not an account, machine, service or site credential, and the identity resolver never reads it."}}},"info":{"description":"Read-only external integration access to the canonical School’s Out catalog and scores. Legacy Insights datasets require ADMIN access. SQL, generic database access, physical table names, and writes are not part of the public API. Product and operator operations are marked x-internal and retain their own credential, plan, role, origin and CSRF requirements.","title":"School’s Out family API","version":"0.2.0"},"openapi":"3.1.0","paths":{"/api/users/viewer/saved":{"get":{"tags":["Internal family social"],"x-internal":true,"summary":"List saved items","description":"The Saved list, newest first. Each item lists the viewer's own collections that hold it (`collectionIds`). `counts` count every save, available or not, whatever `kind`, page or `keys` was asked for. `keys` answers \"is this saved?\" for one page of cards: only the saved ones come back, all on one page with a null `nextCursor`, and `cursor` and `limit` are refused with it.","parameters":[{"name":"kind","in":"query","required":false,"schema":{"enum":["activity","organization"]},"description":"Only this kind."},{"name":"keys","in":"query","required":false,"schema":{"type":"string"},"description":"Up to 100 comma-separated item keys."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"nextCursor from the previous page."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Page size."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["items","nextCursor","counts"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/FamilySocialSavedItem"}},"nextCursor":{"type":["string","null"],"description":"Opaque; pass as cursor for the next page. Null on the last page."},"counts":{"type":"object","required":["total","activities","organizations"],"additionalProperties":false,"description":"Every save of the viewer, available or not.","properties":{"total":{"type":"integer","minimum":0},"activities":{"type":"integer","minimum":0},"organizations":{"type":"integer","minimum":0}}}}}}}}}},"400":{"description":"Invalid kind, keys, cursor or limit."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"List saved items","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/saved\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/saved/{itemKey}":{"put":{"tags":["Internal family social"],"x-internal":true,"summary":"Save an item","description":"Idempotent: 201 when saved now, 200 when it already was. Undo sends back the `savedAt` a DELETE returned, accepted only within the undo window, so the item returns to its old position.","parameters":[{"name":"itemKey","in":"path","required":true,"schema":{"type":"string","pattern":"^(activity|organization):[0-9a-f-]{36}$"},"description":"activity:\u003cuuid\u003e or organization:\u003cuuid\u003e, URL-encoded."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"savedAt":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"Already saved.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["item"],"properties":{"item":{"$ref":"#/components/schemas/FamilySocialItemSummary"}}}}}}}},"201":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["item"],"properties":{"item":{"$ref":"#/components/schemas/FamilySocialItemSummary"}}}}}}}},"400":{"description":"Invalid key or savedAt."},"401":{"description":"Current account required."},"404":{"description":"Not in the catalog."},"409":{"description":"Saved is full (only while a saved cap is on), or the undo window has passed."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Save an item","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/saved/activity:$ACTIVITY_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'"}]},"delete":{"tags":["Internal family social"],"x-internal":true,"summary":"Unsave an item","description":"Removes the item from Saved and from all of the viewer's collections in one transaction. `restore` is what Undo sends back; it is null when the item was not saved.","parameters":[{"name":"itemKey","in":"path","required":true,"schema":{"type":"string","pattern":"^(activity|organization):[0-9a-f-]{36}$"},"description":"activity:\u003cuuid\u003e or organization:\u003cuuid\u003e, URL-encoded."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["removed","restore"],"properties":{"removed":{"type":"boolean"},"restore":{"oneOf":[{"$ref":"#/components/schemas/FamilySocialRestore"},{"type":"null"}]}}}}}}}},"400":{"description":"Invalid key."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Unsave an item","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/saved/activity:$ACTIVITY_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/saved/{itemKey}/collections":{"put":{"tags":["Internal family social"],"x-internal":true,"summary":"Set an item's collections","description":"The item ends up saved and in exactly these of the viewer's collections.","parameters":[{"name":"itemKey","in":"path","required":true,"schema":{"type":"string","pattern":"^(activity|organization):[0-9a-f-]{36}$"},"description":"activity:\u003cuuid\u003e or organization:\u003cuuid\u003e, URL-encoded."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["collectionIds"],"properties":{"collectionIds":{"type":"array","maxItems":100,"items":{"type":"string","format":"uuid"}}},"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["item","collectionIds"],"properties":{"item":{"$ref":"#/components/schemas/FamilySocialItemSummary"},"collectionIds":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}}}},"400":{"description":"Invalid key or collectionIds."},"401":{"description":"Current account required."},"404":{"description":"A collection is not the viewer's, or the item is not in the catalog."},"409":{"description":"Saved or a collection is full (only while a cap is on)."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Set an item's collections","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/saved/activity:$ACTIVITY_ID/collections\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"collectionIds\":[\"$COLLECTION_ID\"]}'"}]}},"/api/users/viewer/collections":{"get":{"tags":["Internal family social"],"x-internal":true,"summary":"List your and followed collections","description":"`own`: the viewer's collections, most recently changed first. `following`: the collections the viewer follows, most recently followed first. One made private or archived stays as a `hidden` summary, so it can be seen and unfollowed; one whose owner blocked or was blocked by the viewer, or whose owner's account is gone, is left out. Each list is keyset-paged on its own order: `limit` applies to each, and a `cursor` continues the one list `include` names.","parameters":[{"name":"include","in":"query","required":false,"schema":{"type":"string"},"description":"own, following or own,following (default both). With cursor, exactly one."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"ownNextCursor or followingNextCursor from the previous page, with include naming that list."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Page size of each list."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["own","following","ownNextCursor","followingNextCursor"],"properties":{"own":{"type":"array","items":{"$ref":"#/components/schemas/FamilySocialCollectionSummary"}},"following":{"type":"array","items":{"$ref":"#/components/schemas/FamilySocialCollectionSummary"}},"ownNextCursor":{"type":["string","null"],"description":"Opaque; pass as cursor with include=own for the next page. Null on the last page or when own was not included."},"followingNextCursor":{"type":["string","null"],"description":"Opaque; pass as cursor with include=following for the next page. Null on the last page or when following was not included."}}}}}}}},"400":{"description":"Invalid include, cursor or limit, or a cursor with both lists."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"List your and followed collections","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/collections\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"post":{"tags":["Internal family social"],"x-internal":true,"summary":"Create a collection","description":"Names are 1–60 characters after trimming, not \"Saved\", unique per parent ignoring case. With `itemKey` the item is saved and added.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":60},"visibility":{"enum":["private","public"],"default":"private"},"itemKey":{"type":"string"}}}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["collection"],"properties":{"collection":{"$ref":"#/components/schemas/FamilySocialCollectionSummary"}}}}}}}},"400":{"description":"Invalid name, visibility or itemKey."},"401":{"description":"Current account required."},"404":{"description":"itemKey is not in the catalog."},"409":{"description":"Duplicate name, or the collection or saved cap is reached (only while one is on)."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Create a collection","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/collections\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"Rainy days\"}'"}]}},"/api/users/viewer/collections/{collectionId}":{"get":{"tags":["Internal family social"],"x-internal":true,"summary":"Read a collection","description":"The viewer's own collection, or one they follow, with a page of items newest first. A followed collection made private or archived answers its `hidden` summary with no items. Other private, blocked and missing collections all answer 404.","parameters":[{"name":"collectionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"nextCursor from the previous page."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Page size."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["collection"],"properties":{"collection":{"$ref":"#/components/schemas/FamilySocialCollectionDetail"}}}}}}}},"400":{"description":"Invalid cursor or limit."},"401":{"description":"Current account required."},"404":{"description":"Not available to the viewer."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Read a collection","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/collections/$COLLECTION_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"patch":{"tags":["Internal family social"],"x-internal":true,"summary":"Rename or change visibility","description":"Owner only. Making a public collection private keeps its follows; their reads answer 404 until it is public again.","parameters":[{"name":"collectionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"name":{"type":"string","minLength":1,"maxLength":60},"visibility":{"enum":["private","public"]}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["collection"],"properties":{"collection":{"$ref":"#/components/schemas/FamilySocialCollectionSummary"}}}}}}}},"400":{"description":"Invalid name or visibility."},"401":{"description":"Current account required."},"403":{"description":"Visible to the viewer but someone else's."},"404":{"description":"Not available to the viewer."},"409":{"description":"Duplicate name."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Rename or change visibility","code":"curl -X PATCH \"https://apis.schoolsoutapp.com/api/users/viewer/collections/$COLLECTION_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"visibility\":\"public\"}'"}]},"delete":{"tags":["Internal family social"],"x-internal":true,"summary":"Delete a collection","description":"Owner only. Its items stay in Saved; its follows are removed.","parameters":[{"name":"collectionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["deleted"],"properties":{"deleted":{"const":true}}}}}}}},"401":{"description":"Current account required."},"403":{"description":"Someone else's."},"404":{"description":"Not available to the viewer."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Delete a collection","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/collections/$COLLECTION_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/collections/{collectionId}/items/{itemKey}":{"put":{"tags":["Internal family social"],"x-internal":true,"summary":"Add an item to a collection","description":"Owner only. Saves the item if needed. The item joins the collection now; its place in the collection is never chosen by the client.","parameters":[{"name":"collectionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"itemKey","in":"path","required":true,"schema":{"type":"string","pattern":"^(activity|organization):[0-9a-f-]{36}$"},"description":"activity:\u003cuuid\u003e or organization:\u003cuuid\u003e, URL-encoded."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["collection"],"properties":{"collection":{"$ref":"#/components/schemas/FamilySocialCollectionSummary"}}}}}}}},"400":{"description":"Invalid key, or a body with properties."},"401":{"description":"Current account required."},"403":{"description":"Someone else's."},"404":{"description":"Collection not available, or item not in the catalog."},"409":{"description":"The collection or Saved is full (only while a cap is on)."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Add an item to a collection","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/collections/$COLLECTION_ID/items/activity:$ACTIVITY_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"delete":{"tags":["Internal family social"],"x-internal":true,"summary":"Remove an item from a collection","description":"Owner only. The item stays in Saved.","parameters":[{"name":"collectionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"itemKey","in":"path","required":true,"schema":{"type":"string","pattern":"^(activity|organization):[0-9a-f-]{36}$"},"description":"activity:\u003cuuid\u003e or organization:\u003cuuid\u003e, URL-encoded."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["collection"],"properties":{"collection":{"$ref":"#/components/schemas/FamilySocialCollectionSummary"}}}}}}}},"400":{"description":"Invalid key."},"401":{"description":"Current account required."},"403":{"description":"Someone else's."},"404":{"description":"Collection not available."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Remove an item from a collection","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/collections/$COLLECTION_ID/items/activity:$ACTIVITY_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/followed-collections/{collectionId}":{"put":{"tags":["Internal family social"],"x-internal":true,"summary":"Follow a collection","description":"Follows a visible public collection.","parameters":[{"name":"collectionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["following"],"properties":{"following":{"type":"boolean"}}}}}}}},"401":{"description":"Current account required."},"404":{"description":"Not visible to the viewer."},"409":{"description":"Your own collection, or the follow cap is reached (only while it is on)."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Follow a collection","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/followed-collections/$COLLECTION_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"delete":{"tags":["Internal family social"],"x-internal":true,"summary":"Unfollow a collection","description":"Always allowed, including for a collection that went private.","parameters":[{"name":"collectionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["following"],"properties":{"following":{"type":"boolean"}}}}}}}},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Unfollow a collection","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/followed-collections/$COLLECTION_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/shared-collections/{collectionId}":{"get":{"tags":["Internal family social"],"x-internal":true,"summary":"Open a shared collection","description":"A public collection by link, with a page of its available items. Private (except to its owner), archived and missing collections all answer 404.","parameters":[{"name":"collectionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"nextCursor from the previous page."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Page size."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["collection"],"properties":{"collection":{"$ref":"#/components/schemas/FamilySocialCollectionDetail"}}}}}}}},"400":{"description":"Invalid cursor or limit."},"404":{"description":"Not available."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-auth":"Public. A signed-in parent (cookie or account credential) is optional: with one, `following` and saved times are filled, blocked owners answer 404, and the response is `private, no-store`; without one it is `public, max-age=60`. Machine keys and delegated service requests read as signed out.","x-curl-examples":[{"title":"Open a shared collection","code":"curl \"https://apis.schoolsoutapp.com/api/shared-collections/$COLLECTION_ID\""}]}},"/api/featured-collections":{"get":{"tags":["Internal family social"],"x-internal":true,"summary":"List featured shelves","description":"Public staff and organization collections placed on a shelf, in position order, each with its first 12 available items.","parameters":[{"name":"placement","in":"query","required":false,"schema":{"enum":["explore","businesses"],"default":"explore"},"description":"The shelf."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":20,"default":20},"description":"How many collections."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["collections"],"properties":{"collections":{"type":"array","items":{"$ref":"#/components/schemas/FamilySocialFeaturedCollection"}}}}}}}}},"400":{"description":"Invalid placement or limit."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-auth":"Public. A signed-in parent (cookie or account credential) is optional: with one, `following` and saved times are filled, blocked owners answer 404, and the response is `private, no-store`; without one it is `public, max-age=60`. Machine keys and delegated service requests read as signed out.","x-curl-examples":[{"title":"List featured shelves","code":"curl \"https://apis.schoolsoutapp.com/api/featured-collections\""}]}},"/api/tracking":{"get":{"tags":["Internal family social"],"x-internal":true,"summary":"List follows","description":"The account's followed organizations, activities and sessions with their alerts, newest first, plus waitlist statistics. Every target is resolved in one query.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":500},"description":"Page size."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"nextCursor from the previous page."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["count","limit","tracked","nextCursor","waitlist"],"properties":{"count":{"type":"integer","description":"All of the account's follows, across pages."},"limit":{"type":["integer","null"],"description":"The most follows an account may have; null while follows are uncapped."},"nextCursor":{"type":["string","null"],"description":"Opaque; pass as cursor for the next page. Null on the last page."},"tracked":{"type":"array","items":{"$ref":"#/components/schemas/FamilySocialTrackedRecord"}},"waitlist":{"type":"object","required":["catalog","tracked"],"properties":{"catalog":{"type":"object","required":["sessionsWithWaitlist","organizationsWithWaitlistMention","organizationsChecked"],"properties":{"sessionsWithWaitlist":{"type":"integer"},"organizationsWithWaitlistMention":{"type":"integer"},"organizationsChecked":{"type":"integer"}}},"tracked":{"type":"object","required":["sessionsWithWaitlist","organizationsWithWaitlistMention","organizationsChecked"],"properties":{"sessionsWithWaitlist":{"type":"integer"},"organizationsWithWaitlistMention":{"type":"integer"},"organizationsChecked":{"type":"integer"}}}}}}}}}}}},"400":{"description":"No account viewer, or an invalid limit or cursor."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account (cookie, account credential, or the service credential with x-viewer-id for Intelligence). Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"List follows","code":"curl \"https://apis.schoolsoutapp.com/api/tracking\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"post":{"tags":["Internal family social"],"x-internal":true,"summary":"Follow a record","description":"Follows a catalog record. Following it again with `alerts` replaces them; without `alerts` it keeps them, and a new follow takes the kind's defaults.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["targetKind","targetId"],"properties":{"targetKind":{"enum":["organization","activity","session"]},"targetId":{"type":"string","format":"uuid"},"alerts":{"type":"array","items":{"enum":["registration_opens","spot_opens","dates_posted","updates"]},"description":"What to hear about. Omitted on a new follow: organization → updates, registration_opens; activity → registration_opens, dates_posted; session → spot_opens. Omitted on a repeat follow: the follow keeps its alerts. [] follows without alerts."}}}}}},"responses":{"200":{"description":"Already followed; alerts replaced.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["tracked","created"],"properties":{"tracked":{"$ref":"#/components/schemas/FamilySocialTrackedRecord"},"created":{"type":"boolean"}}}}}}}},"201":{"description":"Followed.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["tracked","created"],"properties":{"tracked":{"$ref":"#/components/schemas/FamilySocialTrackedRecord"},"created":{"type":"boolean"}}}}}}}},"400":{"description":"Invalid target or alerts."},"401":{"description":"Current account required."},"404":{"description":"Not in the catalog."},"409":{"description":"The follow cap is reached (only while it is on)."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account (cookie, account credential, or the service credential with x-viewer-id for Intelligence). Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Follow a record","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/tracking\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"targetKind\":\"activity\",\"targetId\":\"$ACTIVITY_ID\"}'"}]},"delete":{"tags":["Internal family social"],"x-internal":true,"summary":"Unfollow a record","description":"Stops following a record.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["targetKind","targetId"],"properties":{"targetKind":{"enum":["organization","activity","session"]},"targetId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["removed","targetKind","targetId"],"properties":{"removed":{"type":"boolean"},"targetKind":{"type":"string"},"targetId":{"type":"string","format":"uuid"}}}}}}}},"400":{"description":"Invalid target."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account (cookie, account credential, or the service credential with x-viewer-id for Intelligence). Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Unfollow a record","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/tracking\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"targetKind\":\"activity\",\"targetId\":\"$ACTIVITY_ID\"}'"}]}},"/api/users/viewer/profile":{"get":{"tags":["Internal family social"],"x-internal":true,"summary":"Read your public profile","description":"How other parents see you and your privacy choices. Version 0 means nothing has been saved yet.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["profile"],"properties":{"profile":{"$ref":"#/components/schemas/FamilySocialProfile"}}}}}}}},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Read your public profile","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/profile\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"patch":{"tags":["Internal family social"],"x-internal":true,"summary":"Change privacy","description":"Versioned write. Turning sharePlans on or off applies to every friendship at once.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["version","changes"],"properties":{"version":{"type":"integer","minimum":0},"changes":{"type":"object","required":["privacy"],"properties":{"privacy":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"sharePlans":{"type":"boolean"},"childProfilesVisible":{"type":"boolean"},"dataSharing":{"type":"boolean"}}}},"additionalProperties":false}},"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["profile"],"properties":{"profile":{"$ref":"#/components/schemas/FamilySocialProfile"}}}}}}}},"400":{"description":"Invalid version or privacy value."},"401":{"description":"Current account required."},"409":{"description":"Stale version."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Change privacy","code":"curl -X PATCH \"https://apis.schoolsoutapp.com/api/users/viewer/profile\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"version\":1,\"changes\":{\"privacy\":{\"sharePlans\":true}}}'"}]}},"/api/users/viewer/profile/photo":{"put":{"tags":["Internal family social"],"x-internal":true,"summary":"Upload your photo","description":"Body is the image (image/jpeg or image/png, at most 2 MiB and 4096 px a side). It is cropped to a square and stored as a 512×512 JPEG without metadata.","requestBody":{"required":true,"content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["profile"],"properties":{"profile":{"$ref":"#/components/schemas/FamilySocialProfile"}}}}}}}},"401":{"description":"Current account required."},"413":{"description":"Larger than 2 MiB."},"415":{"description":"Not image/jpeg or image/png."},"422":{"description":"Cannot be decoded, or too large in pixels."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Upload your photo","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/profile/photo\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"delete":{"tags":["Internal family social"],"x-internal":true,"summary":"Remove your photo","description":"Removes the photo.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["profile"],"properties":{"profile":{"$ref":"#/components/schemas/FamilySocialProfile"}}}}}}}},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Remove your photo","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/profile/photo\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/people/{userId}/photo":{"get":{"tags":["Internal family social"],"x-internal":true,"summary":"Read a parent's photo","description":"The JPEG. With `v` naming the current photo it is cached immutably for a year.","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"v","in":"query","required":false,"schema":{"type":"string"},"description":"The photo's hash, from photoUrl."}],"responses":{"200":{"description":"The photo.","content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"No photo."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-auth":"Public; anyone with the link.","x-curl-examples":[{"title":"Read a parent's photo","code":"curl \"https://apis.schoolsoutapp.com/api/people/$USER_ID/photo\""}]}},"/api/users/viewer/friends":{"get":{"tags":["Internal family social"],"x-internal":true,"summary":"List friends","description":"One relation, newest first, keyset-paged. `requested` includes requests the other parent declined, so asking again stays quiet.","parameters":[{"name":"relation","in":"query","required":false,"schema":{"enum":["friends","incoming","requested","blocked"],"default":"friends"},"description":"Which list."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"nextCursor from the previous page."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Page size."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["friends","nextCursor"],"properties":{"friends":{"type":"array","items":{"$ref":"#/components/schemas/FamilySocialFriend"}},"nextCursor":{"type":["string","null"],"description":"Opaque; pass as cursor for the next page. Null on the last page."}}}}}}}},"400":{"description":"Invalid relation, cursor or limit."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"List friends","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/friends\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/friends/{userId}":{"put":{"tags":["Internal family social"],"x-internal":true,"summary":"Change a friendship","description":"request (accepts at once when they asked), accept, decline, cancel, remove, block (from any relation) or unblock (by the blocker). Repeating an action that already holds answers the current relation. A request notifies the other parent; an acceptance notifies the requester.","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"enum":["request","accept","decline","cancel","remove","block","unblock"]}},"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["relation"],"properties":{"relation":{"$ref":"#/components/schemas/FamilySocialRelation"}}}}}}}},"400":{"description":"Invalid action."},"401":{"description":"Current account required."},"404":{"description":"Unknown parent, one without a family profile, yourself, or one who blocked you."},"409":{"description":"Not allowed from the current relation, or the friend cap is reached (only while it is on)."},"429":{"description":"Daily request limit; retryAfterMs is how many milliseconds until another request is allowed. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Change a friendship","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/friends/$USER_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\":\"request\"}'"}]}},"/api/users/viewer/friends-going":{"get":{"tags":["Internal family social"],"x-internal":true,"summary":"Friends going","description":"For one page of cards or a business page: friends whose relation is friends and who share plans, signed up for these activities, sessions or organizations, one group per asked record a friend signed up for. A sign-up is a booking that is booked or waitlisted, or a registered external plan, whose session is upcoming, ongoing or ended at most 365 days before today in the viewer's household. Children's names only when that friend shows them.","parameters":[{"name":"activityIds","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated activity IDs."},{"name":"sessionIds","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated session IDs."},{"name":"organizationIds","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated organization IDs: who booked at each business, across its activities and sessions."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["going"],"properties":{"going":{"type":"array","items":{"$ref":"#/components/schemas/FamilySocialFriendsGoing"}}}}}}}}},"400":{"description":"Give exactly one of activityIds, sessionIds or organizationIds, at most 100 UUIDs."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Friends going","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/friends-going\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/reports":{"post":{"tags":["Internal family social"],"x-internal":true,"summary":"Report a person, listing or collection","description":"One report per reporter and target. The first answers 201 with `alreadyReported: false`; repeating it is idempotent: the first report stands (its reason and note too) and the answer is 200 with `alreadyReported: true`. Staff review reports at /api/family-reports.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["targetKind","targetId","reason"],"properties":{"targetKind":{"enum":["person","activity","organization","collection"]},"targetId":{"type":"string","format":"uuid"},"reason":{"enum":["fake","inappropriate","spam","other"]},"note":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"Already reported; the first report stands.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["reported","alreadyReported"],"properties":{"reported":{"const":true},"alreadyReported":{"type":"boolean","description":"False on the 201 that recorded it, true on a repeat."}},"additionalProperties":false}}}}}},"201":{"description":"Recorded.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["reported","alreadyReported"],"properties":{"reported":{"const":true},"alreadyReported":{"type":"boolean","description":"False on the 201 that recorded it, true on a repeat."}},"additionalProperties":false}}}}}},"400":{"description":"Invalid target, reason or note."},"401":{"description":"Current account required."},"404":{"description":"The target is not visible to you."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Report a person, listing or collection","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/reports\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"targetKind\":\"person\",\"targetId\":\"$USER_ID\",\"reason\":\"spam\"}'"}]}},"/api/people/{userId}":{"get":{"tags":["Internal family social"],"x-internal":true,"summary":"Read another parent","description":"Their public profile, relation, their 50 most recently changed public collections and, when you are friends and they share plans, what they are going to.","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["person"],"properties":{"person":{"$ref":"#/components/schemas/FamilySocialPerson"}}}}}}}},"401":{"description":"Current account required."},"404":{"description":"Unknown, without a family profile, yourself, or blocked either way."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Read another parent","code":"curl \"https://apis.schoolsoutapp.com/api/people/$USER_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/child-invitations/accept":{"post":{"description":"Current nondelegated account with a live verified email matching the invitation recipient. Wrong or unverified accounts receive 403 without child details; unavailable tokens return 404. POST {token,approve:true} consumes an available invitation once and grants guardian access in one transaction. The actor’s current email and verification state are locked, then the profile and invitation are locked and expiry/revocation rechecked. Returns data.child with role guardian. Guardians may read and correct current details/preferences and use owner-enabled registration reuse; they cannot delete the profile, change reuse permission, or manage other guardians. Profile or owner removal cascades invitations and memberships.","requestBody":{"content":{"application/json":{"schema":{"properties":{"approve":{"const":true,"type":"boolean"},"token":{"maxLength":43,"minLength":43,"pattern":"^[A-Za-z0-9_-]{43}$","type":"string"}},"required":["token","approve"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"child":{"$ref":"#/components/schemas/FamilyChildProfile"}},"required":["child"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid invitation input or missing explicit approval"},"401":{"description":"Current account required"},"403":{"description":"Owner-only action or current verified recipient email required"},"404":{"description":"Profile, guardian or invitation not available to this account"},"409":{"description":"Duplicate pending invitation or guardian limit"},"429":{"description":"Invitation creation rate limit. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."}},"security":[{"AccountCredential":[]}],"summary":"Explicitly accept guardian access once","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Child profiles are visible to their owner and accepted active guardians; unrelated accounts receive 404.","x-curl-examples":[{"code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/child-invitations/accept\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","title":"Explicitly accept guardian access once"}],"x-internal":true}},"/api/users/viewer/child-invitations/preview":{"post":{"description":"Current nondelegated account with a live verified email matching the invitation recipient. Wrong or unverified accounts receive 403 without child details; unavailable tokens return 404. POST {token} returns the child name, inviting owner email, recipient address and invitation dates for review. It does not return saved profile fields or grant access. Tokens travel in the JSON body, not the API URL. Invitation landing links use /family/invitations#token.","requestBody":{"content":{"application/json":{"schema":{"properties":{"token":{"maxLength":43,"minLength":43,"pattern":"^[A-Za-z0-9_-]{43}$","type":"string"}},"required":["token"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"invitation":{"$ref":"#/components/schemas/ChildInvitationPreview"}},"required":["invitation"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid invitation input or missing explicit approval"},"401":{"description":"Current account required"},"403":{"description":"Owner-only action or current verified recipient email required"},"404":{"description":"Profile, guardian or invitation not available to this account"},"409":{"description":"Duplicate pending invitation or guardian limit"},"429":{"description":"Invitation creation rate limit. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."}},"security":[{"AccountCredential":[]}],"summary":"Review an invitation addressed to your verified email","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Child profiles are visible to their owner and accepted active guardians; unrelated accounts receive 404.","x-curl-examples":[{"code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/child-invitations/preview\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","title":"Review an invitation addressed to your verified email"}],"x-internal":true}},"/api/users/viewer/children":{"get":{"description":"Current account cookie or account credential only; machine keys and delegated service requests are rejected. Lists owned child profiles and profiles with an accepted, active guardian membership, with role owner or guardian. Each profile includes its registration fields, planning details, interests and preference notes, current version and current-value provenance. Provenance records manual approval or an approved Assistant suggestion, the actor and approval time; another adult's conversation reference is omitted. No previous values are retained. Connected-service imports are not supported. With include=school each child also carries its school (E-55's shape, null when unanswered), read for every child in one query.","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"children":{"items":{"allOf":[{"$ref":"#/components/schemas/FamilyChildProfile"},{"properties":{"school":{"description":"Only with include=school: the child's school (as GET .../children/{childId}/school), or null.","oneOf":[{"$ref":"#/components/schemas/FamilyPlanChildSchool"},{"type":"null"}]}},"type":"object"}]},"type":"array"}},"required":["children"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid profile input, or an include other than school"},"401":{"description":"Current account required"},"404":{"description":"Profile not found for this owner"},"409":{"description":"Stale version or profile count limit"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"summary":"List profiles you own or have accepted guardian access to","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Child profiles are visible to their owner and accepted active guardians; unrelated accounts receive 404.","x-curl-examples":[{"code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/children\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\"","title":"List profiles you own or have accepted guardian access to"}],"x-internal":true,"parameters":[{"name":"include","in":"query","required":false,"schema":{"enum":["school"],"type":"string"},"description":"school adds each child's school (null when the parent has not answered), read for all children at once."}]},"post":{"description":"Creates one of up to 20 owned profiles from approved values. A name is optional (a child may be named later); registration reuse needs both names and defaults to false. Fields are canonical registration strings; preferences hold interests with an unknown/none/listed status and notes; planning holds structured planning details. Each current value records source, actor, approval time and version. The authenticated viewer is always the approving actor. Optional source {type:\"manual\"} or {type:\"assistant\", conversationId, turnId} names one of the viewer's own completed Assistant turns that proposed the change; the write itself is the explicit approval.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyChildInput"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"child":{"$ref":"#/components/schemas/FamilyChildProfile"}},"required":["child"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid profile input"},"401":{"description":"Current account required"},"404":{"description":"Profile not found for this owner"},"409":{"description":"Stale version or profile count limit"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not add or edit children (edit_children; a caregiver)"}},"security":[{"AccountCredential":[]}],"summary":"Create a manually approved child profile","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Child profiles are visible to their owner and accepted active guardians; unrelated accounts receive 404.","x-curl-examples":[{"code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/children\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","title":"Create a manually approved child profile"}],"x-internal":true}},"/api/users/viewer/children/{childId}":{"delete":{"description":"DELETE {version} permanently removes the profile, guardian memberships, invitations, care requirement links and child-linked remembered facts; submitted registration snapshots and receipts remain. Owner only. Stale versions return 409. When the child has a non-closed booking or plan dated today or later, answers 409 with code child-has-plans and plans: FamilyAccountUpcomingPlan[] in the error body, so the app can list them without another call.","parameters":[{"in":"path","name":"childId","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"version":{"minimum":1,"type":"integer"}},"required":["version"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"deleted":{"const":true,"type":"boolean"}},"required":["deleted"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid profile input"},"401":{"description":"Current account required"},"404":{"description":"Profile not found for this owner"},"409":{"description":"Stale version, or child-has-plans with the blocking plans"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not add or edit children (edit_children; a caregiver)"}},"security":[{"AccountCredential":[]}],"summary":"Permanently remove the reusable profile; existing registration snapshots remain","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Child profiles are visible to their owner and accepted active guardians; unrelated accounts receive 404.","x-curl-examples":[{"code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"version\":1}'","title":"Permanently remove the reusable profile; existing registration snapshots remain"}],"x-internal":true},"patch":{"description":"Applies a narrow correction to fields, preferences.notes, preferences.interests ({status, values}), planning.label, planning.age, planning.gradeSchoolYear or planning.requirements ({status, items}); null removes a value. reuseAllowed cannot be changed here. Owner or current guardian; locks the actor and profile and rechecks membership before saving. The authenticated viewer is always the approving actor. Optional source {type:\"manual\"} or {type:\"assistant\", conversationId, turnId} names one of the viewer's own completed Assistant turns that proposed the change; the write itself is the explicit approval.","parameters":[{"in":"path","name":"childId","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyCorrection"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"child":{"$ref":"#/components/schemas/FamilyChildProfile"}},"required":["child"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid input"},"401":{"description":"Current account required"},"404":{"description":"Record not found for this account"},"409":{"description":"Stale version, limit, or nothing to reconfirm"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not add or edit children (edit_children; a caregiver)"}},"security":[{"AccountCredential":[]}],"summary":"Correct named child profile values without replacing the profile","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Child profiles are visible to their owner and accepted active guardians; unrelated accounts receive 404.","x-curl-examples":[{"code":"curl -X PATCH \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"version\":3,\"changes\":{\"child.allergies\":null,\"planning.requirements\":{\"status\":\"none\",\"items\":[]}}}'","title":"Remove an allergy note and record that there are no participation requirements"}],"x-internal":true},"put":{"description":"PUT fully replaces fields and preferences with the current version; omitted or blank optional fields are removed. Omitting planning keeps the saved planning so older clients cannot clear it; sending planning replaces it. Only the owner may change reuseAllowed. Unchanged values retain provenance; changed values record the acting account, source and new version. Stale versions return 409. The authenticated viewer is always the approving actor. Optional source {type:\"manual\"} or {type:\"assistant\", conversationId, turnId} names one of the viewer's own completed Assistant turns that proposed the change; the write itself is the explicit approval.","parameters":[{"in":"path","name":"childId","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/FamilyChildInput"},{"required":["version"],"type":"object"}]}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"child":{"$ref":"#/components/schemas/FamilyChildProfile"}},"required":["child"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid profile input"},"401":{"description":"Current account required"},"404":{"description":"Profile not found for this owner"},"409":{"description":"Stale version or profile count limit"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not add or edit children (edit_children; a caregiver)"}},"security":[{"AccountCredential":[]}],"summary":"Correct an owned or shared child profile using its current version","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Child profiles are visible to their owner and accepted active guardians; unrelated accounts receive 404.","x-curl-examples":[{"code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","title":"Correct an owned or shared child profile using its current version"}],"x-internal":true}},"/api/users/viewer/children/{childId}/confirmations":{"post":{"description":"Records a fresh approval of the named current values (provenance keys such as guardian.email, preferences.interests or planning.age) without changing them. Owner or current guardian. The authenticated viewer is always the approving actor. Optional source {type:\"manual\"} or {type:\"assistant\", conversationId, turnId} names one of the viewer's own completed Assistant turns that proposed the change; the write itself is the explicit approval.","parameters":[{"in":"path","name":"childId","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyReconfirmation"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"child":{"$ref":"#/components/schemas/FamilyChildProfile"}},"required":["child"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid input"},"401":{"description":"Current account required"},"404":{"description":"Record not found for this account"},"409":{"description":"Stale version, limit, or nothing to reconfirm"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"summary":"Reconfirm unchanged child profile values","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Child profiles are visible to their owner and accepted active guardians; unrelated accounts receive 404.","x-curl-examples":[{"code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/confirmations\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"version\":4,\"keys\":[\"child.grade\",\"planning.gradeSchoolYear\"]}'","title":"Reconfirm the saved grade and its school year"}],"x-internal":true}},"/api/users/viewer/children/{childId}/guardians":{"get":{"description":"Current profile owner only; machine, service and delegated-service credentials are rejected. Cookie mutations require CSRF. GET returns active guardian accounts and unexpired pending invitations. It never returns invitation tokens, token hashes, or acceptance links. A profile has at most five active or pending guardians in addition to its owner.","parameters":[{"in":"path","name":"childId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"guardians":{"items":{"$ref":"#/components/schemas/ChildGuardian"},"type":"array"},"invitations":{"items":{"$ref":"#/components/schemas/ChildInvitation"},"type":"array"}},"required":["guardians","invitations"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid invitation input or missing explicit approval"},"401":{"description":"Current account required"},"403":{"description":"Owner-only action or current verified recipient email required"},"404":{"description":"Profile, guardian or invitation not available to this account"},"409":{"description":"Duplicate pending invitation or guardian limit"},"429":{"description":"Invitation creation rate limit. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."}},"security":[{"AccountCredential":[]}],"summary":"Review guardian access and pending invitations","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Owner-only guardian management; API_ASSISTANT_ORIGINS are not trusted on these routes.","x-curl-examples":[{"code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/guardians\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\"","title":"Review guardian access and pending invitations"}],"x-internal":true}},"/api/users/viewer/children/{childId}/guardians/{userId}":{"delete":{"description":"Current profile owner only; machine, service and delegated-service credentials are rejected. Cookie mutations require CSRF. DELETE removes this child’s guardian membership. Future profile reads, corrections and registration reuse recheck membership and refuse after revocation. Other children and existing registration receipts are unchanged.","parameters":[{"in":"path","name":"childId","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"path","name":"userId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"revoked":{"const":true,"type":"boolean"}},"required":["revoked"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid invitation input or missing explicit approval"},"401":{"description":"Current account required"},"403":{"description":"Owner-only action or current verified recipient email required"},"404":{"description":"Profile, guardian or invitation not available to this account"},"409":{"description":"Duplicate pending invitation or guardian limit"},"429":{"description":"Invitation creation rate limit. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."}},"security":[{"AccountCredential":[]}],"summary":"Remove guardian access","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Owner-only guardian management; API_ASSISTANT_ORIGINS are not trusted on these routes.","x-curl-examples":[{"code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/guardians/$USER_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","title":"Remove guardian access"}],"x-internal":true}},"/api/users/viewer/children/{childId}/invitations":{"post":{"description":"Current profile owner only; machine, service and delegated-service credentials are rejected. Cookie mutations require CSRF. POST {email} creates an invitation valid for seven days. The response reveals inviteUrl once and reports emailSent. Configured email delivery is attempted after commit; failed or unavailable email delivery leaves a usable copy link. The fragment contains 32 random bytes encoded as 43 URL-safe base64 characters; only its SHA-256 hash is stored. The invitee must verify the matching current account email, review the invitation, and explicitly accept. Creating an invitation grants no access. Duplicate pending addresses or the five-guardian limit return 409; creation is limited to 20 invitations per account per hour. An allow-listed redirect makes the link open that page (#\u003ctoken\u003e) instead of Intelligence's.","parameters":[{"in":"path","name":"childId","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"email":{"format":"email","maxLength":254,"type":"string"},"redirect":{"description":"Optional page the emailed link opens, with the token in its fragment: an absolute URL on the Auth app's or the parent site's origin (AUTH_ORIGIN, PARENT_SITE_ORIGIN, API_ASSISTANT_ORIGINS). Anything else, or none, links to Intelligence's /family/invitations.","format":"uri","type":"string"}},"required":["email"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"emailSent":{"type":"boolean"},"invitation":{"$ref":"#/components/schemas/ChildInvitation"},"inviteUrl":{"description":"Revealed once; token lives in the URL fragment.","format":"uri","type":"string"}},"required":["invitation","inviteUrl","emailSent"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid invitation input or missing explicit approval"},"401":{"description":"Current account required"},"403":{"description":"Owner-only action or current verified recipient email required"},"404":{"description":"Profile, guardian or invitation not available to this account"},"409":{"description":"Duplicate pending invitation or guardian limit"},"429":{"description":"Invitation creation rate limit. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."}},"security":[{"AccountCredential":[]}],"summary":"Invite another guardian to this child profile","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Owner-only guardian management; API_ASSISTANT_ORIGINS are not trusted on these routes.","x-curl-examples":[{"code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/invitations\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","title":"Invite another guardian to this child profile"}],"x-internal":true}},"/api/users/viewer/children/{childId}/invitations/{invitationId}":{"delete":{"description":"Current profile owner only; machine, service and delegated-service credentials are rejected. Cookie mutations require CSRF. DELETE revokes an unaccepted invitation for this child. Acceptance and revocation serialize on the profile, so a revoked invitation cannot later grant access. Use the guardian removal route after an invitation has been accepted.","parameters":[{"in":"path","name":"childId","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"path","name":"invitationId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"revoked":{"const":true,"type":"boolean"}},"required":["revoked"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid invitation input or missing explicit approval"},"401":{"description":"Current account required"},"403":{"description":"Owner-only action or current verified recipient email required"},"404":{"description":"Profile, guardian or invitation not available to this account"},"409":{"description":"Duplicate pending invitation or guardian limit"},"429":{"description":"Invitation creation rate limit. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."}},"security":[{"AccountCredential":[]}],"summary":"Revoke an invitation","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Owner-only guardian management; API_ASSISTANT_ORIGINS are not trusted on these routes.","x-curl-examples":[{"code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/invitations/$INVITATION_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","title":"Revoke an invitation"}],"x-internal":true}},"/api/users/viewer/household":{"delete":{"description":"DELETE {version} erases household preferences and provenance and advances the retained revision. Stale versions return 409. GET returns empty preferences with the new revision; resaving requires that current revision.","requestBody":{"content":{"application/json":{"schema":{"properties":{"version":{"minimum":1,"type":"integer"}},"required":["version"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"deleted":{"const":true}},"required":["deleted"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid input"},"401":{"description":"Current account required"},"404":{"description":"Record not found for this account"},"409":{"description":"Stale version, limit, or nothing to reconfirm"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"Only the family's owner changes the household (manage_household)"}},"security":[{"AccountCredential":[]}],"summary":"Remove all household preferences","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult; sharing a child never shares them.","x-curl-examples":[{"code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/household\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"version\":2}'","title":"Remove household preferences"}],"x-internal":true},"get":{"description":"Returns the signed-in adult’s private household preferences, provenance, and version. Version 0 means never saved. After deletion, preferences and provenance are empty but the incremented version remains; use this version to save again.","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"household":{"$ref":"#/components/schemas/FamilyHousehold"}},"required":["household"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not see the household (read_household; a caregiver)"}},"security":[{"AccountCredential":[]}],"summary":"Read the signed-in adult's household preferences","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult; sharing a child never shares them.","x-curl-examples":[{"code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/household\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\"","title":"Read household preferences"}],"x-internal":true},"patch":{"description":"Version 0 saves the first preferences; later changes need the current version. Each named key (searchArea, timezone, budget, travel, caregiverAvailability, emergencyContacts, planningIntent, home, places) is replaced; null removes it. The authenticated viewer is always the approving actor. Optional source {type:\"manual\"} or {type:\"assistant\", conversationId, turnId} names one of the viewer's own completed Assistant turns that proposed the change; the write itself is the explicit approval. After deletion, read and submit the retained current version; version 0 is only valid before the first save. searchArea is either {label, postalCode?, region?, radiusMiles?} or {placeId, radiusMiles?} with a placeId from GET /api/users/viewer/places, which the server replaces with the resolved label, postal code, region and point; a client-sent point is refused. A distance-only change, searchArea {radiusMiles}, keeps the stored area (label, postal code, region and point) and changes its radius; with no stored area it is 400. 404 means the placeId is unknown, expired or another viewer's. home is {placeId, label?}, which the server replaces with the lookup's label (or the label sent), address, point and precision; a label-only change, home {label}, renames the stored home. places is the whole list of at most 10 saved places: {placeId, kind, label?, childId?} adds one located from its lookup, {id, kind?, label?, childId?} keeps a saved one and its location. A client-sent point, address or precision is refused for both. kind is work, school or other; childId names one of the family's children, only on a school place, at most once. searchArea gains fromHome when its point is the home's.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyCorrection"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"household":{"$ref":"#/components/schemas/FamilyHousehold"}},"required":["household"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid input"},"401":{"description":"Current account required"},"404":{"description":"Unknown or expired placeId"},"409":{"description":"Stale version, limit, or nothing to reconfirm"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"Only the family's owner changes the household (manage_household)"}},"security":[{"AccountCredential":[]}],"summary":"Save or correct named household preferences","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult; sharing a child never shares them.","x-curl-examples":[{"code":"curl -X PATCH \"https://apis.schoolsoutapp.com/api/users/viewer/household\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"version\":0,\"changes\":{\"budget\":{\"amount\":450,\"currency\":\"USD\",\"period\":\"week\",\"scope\":\"per_child\",\"limit\":\"hard_cap\"}}}'","title":"Save a weekly per-child hard budget cap"}],"x-internal":true}},"/api/users/viewer/household/confirmations":{"post":{"description":"Records a fresh approval of the named saved preferences without changing them. The authenticated viewer is always the approving actor. Optional source {type:\"manual\"} or {type:\"assistant\", conversationId, turnId} names one of the viewer's own completed Assistant turns that proposed the change; the write itself is the explicit approval.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyReconfirmation"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"household":{"$ref":"#/components/schemas/FamilyHousehold"}},"required":["household"],"type":"object"},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Invalid input"},"401":{"description":"Current account required"},"404":{"description":"Record not found for this account"},"409":{"description":"Stale version, limit, or nothing to reconfirm"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"Only the family's owner changes the household (manage_household)"}},"security":[{"AccountCredential":[]}],"summary":"Reconfirm unchanged household preferences","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult; sharing a child never shares them.","x-curl-examples":[{"code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/household/confirmations\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"version\":2,\"keys\":[\"timezone\"]}'","title":"Reconfirm the household time zone"}],"x-internal":true}},"/api/users/viewer/registrations":{"get":{"description":"Requires an account session or account credential belonging to the parent. Machine keys, service and delegated-service credentials are rejected. Cookie mutations require the current CSRF token. Responses are private and not cached.","responses":{"200":{"description":"Latest 200 owner-scoped registrations, newest first; data.registrations carries id, organizationId, organizationName, childProfileId (nullable after removal), profileVersion, confirmationCode, status, createdAt and authorization. Authorization carries version, purpose, approvedAt, sharedFields, reusedFields, fieldLabels and consentStatements. Previously submitted registrations remain after profile removal."},"401":{"description":"Sign-in required"},"403":{"description":"Account credential required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"summary":"List your registration disclosure receipts","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Registration receipts belong to the submitting adult.","x-curl-examples":[{"code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/registrations\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\"","title":"List your registration disclosure receipts"}],"x-internal":true}},"/api/users/viewer/registrations/{id}":{"post":{"description":"Current account owns the child profile or has accepted active guardian access; cookie mutations require CSRF. Machine keys and service impersonation are rejected. Registers with camp id using an explicitly approved saved profile. Send the normal registration body plus profile {id,version}, approveSharing:true and consentStatements [{key,label,text}] matching each accepted form acknowledgement. The transaction locks the submitting account and profile, then rechecks live owner or guardian access, version and owner-controlled reuse permission before allocating capacity. The receipt belongs to the submitting account; joining a profile does not grant access to another account’s registration history. Corrections never rewrite submitted registrations. Stale profile/consent returns 409; inaccessible profiles return 404. Guest registration remains at POST /api/organizations/{id}/registrations.","parameters":[{"description":"Destination organization ID","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"description":"Canonical and custom answers follow the destination’s public registration form. Only visible requested fields are accepted. Consent statements must exactly match accepted acknowledgements in form order. The API checks the current profile version/reuse permission in the same transaction as registration and capacity.","properties":{"acknowledgements":{"type":"object"},"answers":{"type":"object"},"approveSharing":{"const":true,"type":"boolean"},"child":{"type":"object"},"consent":{"const":true,"type":"boolean"},"consentStatements":{"items":{"properties":{"key":{"type":"string"},"label":{"type":"string"},"text":{"type":"string"}},"required":["key","label","text"],"type":"object"},"type":"array"},"emergencyContact":{"type":"object"},"guardian":{"type":"object"},"profile":{"properties":{"id":{"format":"uuid","type":"string"},"version":{"maximum":9007199254740991,"minimum":1,"type":"integer"}},"required":["id","version"],"type":"object"},"secondaryGuardian":{"type":"object"},"sessionIds":{"items":{"format":"uuid","type":"string"},"maxItems":6,"type":"array"},"source":{"type":"object"}},"required":["profile","approveSharing","consentStatements","child","guardian","consent"],"type":"object"}}},"required":true},"responses":{"201":{"description":"Registration receipt in data.registration; approved disclosure persisted atomically"},"400":{"description":"Invalid reference or missing approval"},"401":{"description":"Sign-in required"},"403":{"description":"Account credential required; The role may not book (book; a caregiver)"},"404":{"description":"Camp or owned child profile not found"},"409":{"description":"Profile changed, reuse revoked, form closed, or consent wording changed; reload and review"},"422":{"description":"Registration answers or selected sessions are invalid"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"summary":"Submit an explicitly approved registration using your child profile","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Registration receipts belong to the submitting adult.","x-curl-examples":[{"code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/registrations/$ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'","title":"Submit an explicitly approved registration using your child profile"}],"x-internal":true}},"/api/users/viewer/registrations/{registrationId}/answers":{"get":{"description":"The questions the camp's current registration form requires that one of the viewer's own registrations (one they submitted, not cancelled) has not answered: visible required canonical attendee fields and the camp's own questions. Acknowledgements, consent, sessions and the how-you-heard question are never asked again here. Home's answer_form action opens it.","parameters":[{"description":"The registration (the answer_form Home action's registrationId)","in":"path","name":"registrationId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"The registration and its missing questions.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/FamilyRegistrationQuestions"}}},"example":{"success":true,"data":{"registration":{"id":"5b1f0c7e-2d4a-4e8b-9f3c-1a2b3c4d5e6f","organizationId":"8c2d4e6f-1a3b-4c5d-8e7f-9a0b1c2d3e4f","organizationName":"Lakeside Day Camp","confirmationCode":"SO-7KQ2MX","childProfileId":"3e5a7c9b-2d4f-4a6c-8e0b-1d3f5a7c9e2b","status":"enrolled"},"formVersion":"2026-10-28T17:04:11.482915+00:00","questions":[{"key":"child.allergies","label":"Allergies, medication, or medical needs","hint":"Anything staff must know to keep your camper safe.","control":"textarea","options":[]},{"key":"answers.tshirt","label":"T-shirt size","hint":"","control":"choice","options":["Youth S","Youth M","Youth L"]}]}}}}},"400":{"description":"Registration id must be a UUID"},"401":{"description":"Sign-in required"},"403":{"description":"Account credential required; The role may not book (book; a caregiver)"},"404":{"description":"Not one of your registrations, or it was cancelled"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"summary":"Read a registration's missing questions","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. A registration belongs to the adult who submitted it.","x-curl-examples":[{"code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/registrations/$REGISTRATION_ID/answers\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\"","title":"Read a registration's missing questions"}],"x-internal":true},"post":{"description":"Adds the parent's answers to questions the registration is missing, exactly as sent, validated as the public form validates them. Only questions the GET lists may be answered, and only while the camp's form is the formVersion read; a question answered since or no longer asked, or a form saved since, is 409 and nothing is written. The answers are written in the camp-correction lock order (camp, then registration), recorded in the registration's history as a correction by the parent, added to the registration's sharing record (sharedFields and fieldLabels), and emailed to the camp's registration notification address when it has one. Answer some or all of the questions; the response lists what is still missing.","parameters":[{"description":"The registration (the answer_form Home action's registrationId)","in":"path","name":"registrationId","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["formVersion","approveSharing","values"],"properties":{"formVersion":{"type":"string","description":"The formVersion the questions were read with."},"approveSharing":{"const":true,"type":"boolean","description":"The parent approved sending these answers to the camp."},"values":{"type":"object","minProperties":1,"description":"Answers keyed by question key: a string, or for multi-choice a list of the offered choices.","additionalProperties":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}}}}}}},"responses":{"200":{"description":"Answers saved; the registration and the questions still missing.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/FamilyRegistrationQuestions"}}},"example":{"success":true,"data":{"registration":{"id":"5b1f0c7e-2d4a-4e8b-9f3c-1a2b3c4d5e6f","organizationId":"8c2d4e6f-1a3b-4c5d-8e7f-9a0b1c2d3e4f","organizationName":"Lakeside Day Camp","confirmationCode":"SO-7KQ2MX","childProfileId":"3e5a7c9b-2d4f-4a6c-8e0b-1d3f5a7c9e2b","status":"enrolled"},"formVersion":"2026-10-28T17:04:11.482915+00:00","questions":[]}}}}},"400":{"description":"Missing approval or answers, or an answer the question does not accept"},"401":{"description":"Sign-in required"},"403":{"description":"Account credential required; The role may not book (book; a caregiver)"},"404":{"description":"Not one of your registrations, or it was cancelled"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"409":{"description":"The camp's form changed, or a question was answered since or is no longer asked; reload and review"}},"security":[{"AccountCredential":[]}],"summary":"Answer a registration's missing questions","tags":["Internal family profiles"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. A registration belongs to the adult who submitted it.","x-curl-examples":[{"code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/registrations/$REGISTRATION_ID/answers\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"formVersion\":\"2026-10-28T17:04:11.482915+00:00\",\"approveSharing\":true,\"values\":{\"child.allergies\":\"Peanuts\"}}'","title":"Answer a registration's missing questions"}],"x-internal":true}},"/api/users/viewer/payment-method":{"get":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Get the saved cards","description":"Returns the default and backup cards, the billing address and the receipt email, refreshed from Stripe. After setup, a card newer than both becomes the default and the previous default fills an empty backup slot. `paymentMethod` repeats the default card for older apps.","responses":{"200":{"description":"The saved cards.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/FamilyPaymentMethods"}}},"example":{"success":true,"data":{"paymentMethod":{"brand":"visa","last4":"4242"},"default":{"id":"pm_1PqDefault","brand":"visa","last4":"4242","expMonth":4,"expYear":2031,"name":"Sam Rivera"},"backup":null,"cards":[{"id":"pm_1PqDefault","brand":"visa","last4":"4242","expMonth":4,"expYear":2031,"name":"Sam Rivera"}],"billingAddress":{"line1":"12 Lake St","city":"Portland","region":"OR","postalCode":"97201"},"receiptEmail":null}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"403":{"description":"Only the family's owner manages cards, top-ups and gift cards (manage_cards)"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"Payments aren't configured on this server (no Stripe key, `STRIPE_SECRET_KEY`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Payments aren't configured on this server.","data":null}}}}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Get the saved card","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/payment-method\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"patch":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Choose cards, billing address and receipt email","description":"Chooses the default and backup among the parent’s own saved cards (checked with Stripe), saves the billing address on Stripe and here, and sets the receipt email. `makeDefault` applies first; making the backup the default swaps the two. `backupId: null` clears the backup. `receiptEmail` needs no saved card (a family that pays only with credits gets receipts too); `receiptEmail: null` sends receipts to the account email again. Send at least one property.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"backupId":{"type":["string","null"],"pattern":"^pm_"},"makeDefault":{"type":"string","pattern":"^pm_"},"billingAddress":{"$ref":"#/components/schemas/FamilyPaymentMethodBillingAddress"},"receiptEmail":{"type":["string","null"],"format":"email","maxLength":254,"description":"Where payment receipts are emailed instead of the account email; null clears it."}}}}}},"responses":{"200":{"description":"The saved cards after the change.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/FamilyPaymentMethods"}}}}}},"400":{"description":"Unknown property, no property, a malformed card id, an address without a 2-letter region and 5-digit postal code, or a receiptEmail that is not one email address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"billingAddress needs a 2-letter region and a 5-digit postal code.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"409":{"description":"No saved card yet, for a change of cards or billing address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Add a card first.","data":null}}}},"422":{"description":"A card that is not the parent’s, or a backup equal to the default. Also a card Stripe does not know, or another family’s card.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"That card is not one of your saved cards.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"Stripe did not save the address. Also when Stripe can’t be reached (or has no key locally) to look up a card in neither slot: “Card setup is unavailable. Try again shortly.” Also on a server without a Stripe key (`STRIPE_SECRET_KEY`), before anything is read or recorded: “Payments aren't configured on this server.”","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"The billing address could not be saved. Try again.","data":null}}}},"403":{"description":"Only the family's owner manages cards, top-ups and gift cards (manage_cards)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Choose cards, billing address and receipt email","code":"curl -X PATCH \"https://apis.schoolsoutapp.com/api/users/viewer/payment-method\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"makeDefault\": \"pm_1PqBackup\", \"billingAddress\": {\"line1\": \"12 Lake St\", \"city\": \"Portland\", \"region\": \"OR\", \"postalCode\": \"97201\"}}'"}]}},"/api/users/viewer/payment-method/setup":{"post":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Start card setup","description":"Returns a Stripe Checkout URL in setup mode. Send the browser there; Stripe returns the parent to `returnUrl` afterward.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["returnUrl"],"properties":{"returnUrl":{"type":"string","format":"uri","description":"Absolute URL whose origin is in the API’s `REDIRECT_ALLOWLIST`."}}},"example":{"returnUrl":"https://data.schoolsoutapp.com/family/bookings/new?session=c3a9e1f2-6d4b-4a8c-b2e7-0f5d9c1a3b48"}}}},"responses":{"200":{"description":"The Stripe-hosted setup URL.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"}}}}},"example":{"success":true,"data":{"url":"https://checkout.stripe.com/c/pay/cs_live_..."}}}}},"400":{"description":"The body isn’t exactly `{ returnUrl }`, or its origin isn’t allowed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"returnUrl must be an allowed absolute URL.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"Stripe couldn’t start setup. Nothing was saved; try again. Also on a server without a Stripe key (`STRIPE_SECRET_KEY`), before anything is read or recorded: “Payments aren't configured on this server.”","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Card setup is unavailable. Try again shortly.","data":null}}}},"403":{"description":"Only the family's owner manages cards, top-ups and gift cards (manage_cards)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Start card setup","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/payment-method/setup\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"returnUrl\": \"https://data.schoolsoutapp.com/family/bookings/new?session=c3a9e1f2-6d4b-4a8c-b2e7-0f5d9c1a3b48\"}'"}]}},"/api/users/viewer/payment-method/setup-intent":{"post":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Start card setup in the page","description":"Returns the client secret of a Stripe SetupIntent for an off-session card on the parent’s Stripe customer, for Stripe Elements to confirm in the page (`stripe.confirmCardSetup`, 3-D Secure included). The publishable key is the site’s own setting. Sending it again reuses the open SetupIntent. Once Stripe confirms, read `GET /api/users/viewer/payment-method`: it saves the new card as the default, or with `makeDefault: false` as the backup beside an existing default. The hosted `setup` route stays for a redirect.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"makeDefault":{"type":"boolean","default":true,"description":"Whether the new card becomes the default; false makes it the backup when the parent has a default card."}}},"example":{"makeDefault":true}}}},"responses":{"200":{"description":"The SetupIntent’s client secret.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["clientSecret"],"properties":{"clientSecret":{"type":"string","description":"Pass to Stripe Elements; never log or store it."}}}}},"example":{"success":true,"data":{"clientSecret":"seti_1Q2w3E4r5T6y7U8i_secret_Op9As8Df7Gh6"}}}}},"400":{"description":"The body has a property other than `makeDefault`, or it isn’t a boolean.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"The request body has an unknown or mistyped property.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"Stripe couldn’t start setup. Nothing was saved; try again. Also on a server without a Stripe key (`STRIPE_SECRET_KEY`), before anything is read or recorded: “Payments aren't configured on this server.”","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Card setup is unavailable. Try again shortly.","data":null}}}},"403":{"description":"Only the family's owner manages cards, top-ups and gift cards (manage_cards)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Start card setup in the page","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/payment-method/setup-intent\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"makeDefault\": true}'"}]}},"/api/users/viewer/booking-policy":{"get":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Get the booking policy","description":"Returns the money policy bookings use: the service fee per child and the group rate per family. Show it before the parent approves; each booking then reports the fee it carries (`payment.serviceFeeCents`).","responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/FamilyBookingPolicy"}}},"example":{"success":true,"data":{"serviceFee":{"perChild":{"amountCents":500,"currency":"USD"}},"groupRate":{"perFamily":{"amountCents":500,"currency":"USD"},"families":2}}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"$ref":"#/components/responses/BookingsDisabled"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The route is registered only when the API runs with BOOKINGS_ENABLED=true; otherwise it returns 404.","x-curl-examples":[{"title":"Get the booking policy","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/booking-policy\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/booking-quote":{"post":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Quote a booking","description":"Returns what booking a session for each child, in checkout order, would cost and what would stop it, without booking or holding anything. The spending limit is `maxSpend` when sent, else the session's listed price (most catalog sessions state no price basis, so the checkout sends the price it shows); the service fee is the policy's; `useCreditsCents` (capped by the wallet) is applied to the children in order until it runs out, each at most its limit plus fee; the card hold is what is left, raised to Stripe's minimum when small. These are the amounts the approvals then hold: send one approval per child with the child's `money.maxSpend`, `childProfileVersion` and `money.creditsApplied`. A POST because it takes a list of children; it writes nothing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyBookingQuoteInput"}}}},"responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["quote"],"properties":{"quote":{"$ref":"#/components/schemas/FamilyBookingQuote"}}}}},"example":{"success":true,"data":{"quote":{"sessionId":"c3a9e1f2-6d4b-4a8c-b2e7-0f5d9c1a3b48","children":[{"childProfileId":"1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93","childProfileVersion":4,"eligibility":{"alreadyBooked":false,"reuseConsentMissing":false,"missingFields":[]},"wouldWaitlist":false,"money":{"maxSpend":{"amountCents":42500,"currency":"USD"},"serviceFee":{"amountCents":500,"currency":"USD"},"creditsApplied":{"amountCents":30000,"currency":"USD"},"cardHold":{"amountCents":13000,"currency":"USD"},"stripeMinimumApplied":false}},{"childProfileId":"5b2e8d14-3c7a-4f91-b6d0-9e4a1c7f2b58","childProfileVersion":1,"eligibility":{"alreadyBooked":false,"reuseConsentMissing":true,"missingFields":["child.lastName"]},"wouldWaitlist":true,"money":{"maxSpend":{"amountCents":42500,"currency":"USD"},"serviceFee":{"amountCents":500,"currency":"USD"},"creditsApplied":{"amountCents":0,"currency":"USD"},"cardHold":{"amountCents":43000,"currency":"USD"},"stripeMinimumApplied":false}}],"credits":{"available":{"amountCents":30000,"currency":"USD"},"requested":{"amountCents":50000,"currency":"USD"},"applied":{"amountCents":30000,"currency":"USD"}},"paymentMethod":{"choice":"default","saved":true},"totals":{"maxSpend":{"amountCents":85000,"currency":"USD"},"serviceFees":{"amountCents":1000,"currency":"USD"},"credits":{"amountCents":30000,"currency":"USD"},"cardHolds":{"amountCents":56000,"currency":"USD"}}}}}}}},"400":{"description":"An unknown property, a `sessionId` or child that is not a UUID, no children, too many or repeated children, a `maxSpend` an approval would refuse (including another currency than the session's listed price), `useCreditsCents` below 1, or a `paymentMethod` other than `default` or `backup`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"childProfileIds must list 1 to 20 children.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"description":"Bookings are off, a child profile isn't available to this parent, or the session is no longer listed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"That session is no longer available.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not book (book; a caregiver)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The route is registered only when the API runs with BOOKINGS_ENABLED=true; otherwise it returns 404.","x-curl-examples":[{"title":"Quote a booking","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/booking-quote\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"sessionId\": \"c3a9e1f2-6d4b-4a8c-b2e7-0f5d9c1a3b48\", \"childProfileIds\": [\"1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93\", \"5b2e8d14-3c7a-4f91-b6d0-9e4a1c7f2b58\"], \"maxSpend\": {\"amountCents\": 42500, \"currency\": \"USD\"}, \"useCreditsCents\": 50000, \"paymentMethod\": \"default\"}'"}]}},"/api/users/viewer/bookings":{"get":{"tags":["Internal family bookings"],"x-internal":true,"summary":"List bookings","description":"Returns the parent’s bookings, newest first, a page at a time (`nextCursor`). Filters: `childId`, `from`/`to` (the session’s date range overlaps them; bookings without dates are left out when either is set), and `phase`. Event history is omitted; get a single booking to read it.","responses":{"200":{"description":"The parent’s bookings.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["bookings","nextCursor"],"properties":{"bookings":{"type":"array","items":{"anyOf":[{"$ref":"#/components/schemas/FamilyBooking"},{"$ref":"#/components/schemas/FamilyBookingSchedule"}]}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}},"example":{"success":true,"data":{"bookings":[{"id":"6f1d2c3b-4a5e-4f60-8b7a-1c2d3e4f5a6b","phase":"booked","status":"confirmed","paymentStatus":"captured","childProfileId":"1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93","organization":{"id":"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34","name":"Lakeside Science Camp"},"activity":{"id":"7d3b9f60-2a4c-4e8b-9f1d-6c5a0e2b8d17","name":"Summer Science Camp"},"session":{"id":"c3a9e1f2-6d4b-4a8c-b2e7-0f5d9c1a3b48","name":"Robotics Week","startDate":"2027-07-12","endDate":"2027-07-16","startTime":"09:00","endTime":"15:00","dates":["2027-07-12","2027-07-13","2027-07-14","2027-07-15","2027-07-16"],"dayTimes":[]},"approval":{"approvedAt":"2026-10-09T17:00:00Z","maxSpend":{"amountCents":45000,"currency":"USD"},"allowWaitlist":true,"autoAcceptDecisions":false,"sharedFields":["child.birthDate"]},"progressStage":null,"decision":null,"decisions":[],"receipt":{"providerReference":"LSC-20931","providerStatus":"enrolled","amount":{"amountCents":42500,"currency":"USD"},"confirmedAt":"2026-10-09T17:06:35Z","source":"hallpass"},"failure":null,"checkingWith":null,"pollAfterMs":null,"createdAt":"2026-10-09T17:00:00Z","updatedAt":"2026-10-09T17:06:41Z","waitlist":null,"canceledBy":null,"refund":null,"dueOnProviderSite":null,"note":null,"askAgainAfter":null,"replacesBookingId":null,"replacedByBookingId":null,"payment":{"paymentMethod":"default","creditsCents":0,"cardHoldCents":0,"serviceFeeCents":500},"cancellation":{"allowed":false,"action":"cancel","reason":"booked","deadline":null,"refundableCents":0,"methods":[]},"bookedBy":null,"hold":null}],"nextCursor":null}}}}},"400":{"description":"A filter, limit or cursor is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"from must be a date (YYYY-MM-DD).","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"$ref":"#/components/responses/BookingsDisabled"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"No access to the family's bookings"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The route is registered only when the API runs with BOOKINGS_ENABLED=true; otherwise it returns 404.","x-curl-examples":[{"title":"List bookings","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/bookings\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"parameters":[{"name":"childId","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Only this child’s bookings."},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date"},"description":"Sessions ending on or after this date."},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date"},"description":"Sessions starting on or before this date."},{"name":"phase","in":"query","required":false,"schema":{"$ref":"#/components/schemas/BookingPhase"},"description":"Only bookings in this phase."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Page size, at most 100."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"`nextCursor` from the previous page."}]},"post":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Approve a booking","description":"Records the parent’s approval, places a hold on their card for the spending limit plus the service fee (less any credits), and sends the booking to Hallpass. A fast checkout can finish within this request; otherwise, follow the booking until it settles. The child profile must allow reuse, as it does for registration: send `reuseAllowed: true` with the approval to record that consent on the child. `profileFields` saves new details to the child's profile first. The camp's guardian is the adult approving (the owner or a co-parent): their account's name, email and contact phone, never the child's profile.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingApprovalInput"}}}},"responses":{"200":{"description":"The existing booking for this `idempotencyKey`, returned because the request was a replay.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["booking"],"properties":{"booking":{"$ref":"#/components/schemas/FamilyBookingDetail"}}}}}}}},"201":{"description":"The new booking. Its status may already be past `authorizing`.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["booking"],"properties":{"booking":{"$ref":"#/components/schemas/FamilyBookingDetail"}}}}},"example":{"success":true,"data":{"booking":{"id":"6f1d2c3b-4a5e-4f60-8b7a-1c2d3e4f5a6b","phase":"in_progress","status":"accepted","paymentStatus":"held","childProfileId":"1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93","organization":{"id":"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34","name":"Lakeside Science Camp"},"activity":{"id":"7d3b9f60-2a4c-4e8b-9f1d-6c5a0e2b8d17","name":"Summer Science Camp"},"session":{"id":"c3a9e1f2-6d4b-4a8c-b2e7-0f5d9c1a3b48","name":"Robotics Week","startDate":"2027-07-12","endDate":"2027-07-16","startTime":"09:00","endTime":"15:00","dates":["2027-07-12","2027-07-13","2027-07-14","2027-07-15","2027-07-16"],"dayTimes":[]},"approval":{"approvedAt":"2026-10-09T17:00:00Z","maxSpend":{"amountCents":45000,"currency":"USD"},"allowWaitlist":true,"autoAcceptDecisions":false,"sharedFields":["child.birthDate"]},"progressStage":null,"decision":null,"decisions":[],"receipt":null,"failure":null,"checkingWith":null,"pollAfterMs":2000,"createdAt":"2026-10-09T17:00:00Z","updatedAt":"2026-10-09T17:06:41Z","events":[{"kind":"approval_granted","status":"authorizing","actor":"parent","summary":"Approved the booking with a spending limit of 450.00 USD.","createdAt":"2026-10-09T17:00:00Z"},{"kind":"payment_hold_placed","status":"prepared","actor":"system","summary":"Held 450.00 USD on the family card.","createdAt":"2026-10-09T17:00:01Z"},{"kind":"task_submitted","status":"submitting","actor":"system","summary":"Sent the approved booking to Hallpass.","createdAt":"2026-10-09T17:00:01Z"}],"waitlist":null,"canceledBy":null,"refund":null,"dueOnProviderSite":null,"note":null,"askAgainAfter":null,"replacesBookingId":null,"replacedByBookingId":null,"payment":{"paymentMethod":"default","creditsCents":0,"cardHoldCents":45500,"serviceFeeCents":500},"cancellation":{"allowed":true,"action":"cancel","reason":null,"deadline":null,"refundableCents":0,"methods":[]},"bookedBy":null,"hold":null}}}}}},"400":{"description":"The approval is invalid: an unknown property, `approveBooking` not `true`, a spending limit or currency that isn’t allowed, a field that can’t be shared or is missing from the profile, or too many answers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Approve the booking to continue.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"403":{"description":"`reuseAllowed` from an adult who is not the child profile's owner: only the owner grants registration reuse; The role may not book (book; a caregiver)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Only the profile owner can change registration reuse permission.","data":null}}}},"404":{"description":"Bookings are off, the child profile isn’t available to this parent, or the session is no longer listed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"That session is no longer available.","data":null}}}},"409":{"description":"The profile changed or no longer allows reuse, the `idempotencyKey` was reused with different details, the child already has an active or confirmed booking for this session, the wallet does not cover `useCreditsCents`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"This child already has a booking in progress or confirmed for this session.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The route is registered only when the API runs with BOOKINGS_ENABLED=true; otherwise it returns 404.","x-curl-examples":[{"title":"Approve a booking","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/bookings\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"idempotencyKey\": \"0b7c3f52-9a41-4e2d-8f6a-3c1d5e7b9a20\", \"childProfileId\": \"1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93\", \"childProfileVersion\": 4, \"sessionId\": \"c3a9e1f2-6d4b-4a8c-b2e7-0f5d9c1a3b48\", \"maxSpend\": {\"amountCents\": 45000, \"currency\": \"USD\"}, \"allowWaitlist\": true, \"autoAcceptDecisions\": false, \"shareFields\": [\"child.birthDate\"], \"answers\": [{\"key\": \"photo_consent\", \"label\": \"Photo consent\", \"value\": false}], \"approveBooking\": true}'"}]}},"/api/users/viewer/bookings/{bookingId}":{"get":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Get a booking","description":"Returns one booking with its full event history. Read it again after `pollAfterMs`. A booking that belongs to another account looks exactly like one that doesn’t exist.","parameters":[{"name":"bookingId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The booking and its events.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["booking","providerAccount"],"properties":{"booking":{"anyOf":[{"$ref":"#/components/schemas/FamilyBookingDetail"},{"$ref":"#/components/schemas/FamilyBookingSchedule"}]},"providerAccount":{"type":["object","null"],"description":"The provider account the booking carries when Hallpass creates or reuses one for the approving adult (BOOKING_PROVIDER_ACCOUNTS), such as a CourseStorm account; null otherwise. createdAt is null until the run creates it at the provider.","additionalProperties":false,"required":["provider","site","usernameDisplay","origin","createdAt"],"properties":{"provider":{"type":"string"},"site":{"type":"string","format":"uri"},"usernameDisplay":{"type":"string"},"origin":{"type":"string","enum":["attached","created"],"x-open-enum":true},"createdAt":{"type":["string","null"],"format":"date-time"}}}}}}},"example":{"success":true,"data":{"booking":{"id":"6f1d2c3b-4a5e-4f60-8b7a-1c2d3e4f5a6b","phase":"booked","status":"confirmed","paymentStatus":"captured","childProfileId":"1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93","organization":{"id":"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34","name":"Lakeside Science Camp"},"activity":{"id":"7d3b9f60-2a4c-4e8b-9f1d-6c5a0e2b8d17","name":"Summer Science Camp"},"session":{"id":"c3a9e1f2-6d4b-4a8c-b2e7-0f5d9c1a3b48","name":"Robotics Week","startDate":"2027-07-12","endDate":"2027-07-16","startTime":"09:00","endTime":"15:00","dates":["2027-07-12","2027-07-13","2027-07-14","2027-07-15","2027-07-16"],"dayTimes":[]},"approval":{"approvedAt":"2026-10-09T17:00:00Z","maxSpend":{"amountCents":45000,"currency":"USD"},"allowWaitlist":true,"autoAcceptDecisions":false,"sharedFields":["child.birthDate"]},"progressStage":null,"decision":null,"decisions":[],"receipt":{"providerReference":"LSC-20931","providerStatus":"enrolled","amount":{"amountCents":42500,"currency":"USD"},"confirmedAt":"2026-10-09T17:06:35Z","source":"hallpass"},"failure":null,"checkingWith":null,"pollAfterMs":null,"createdAt":"2026-10-09T17:00:00Z","updatedAt":"2026-10-09T17:06:41Z","events":[{"kind":"approval_granted","status":"authorizing","actor":"parent","summary":"Approved the booking with a spending limit of 450.00 USD.","createdAt":"2026-10-09T17:00:00Z"},{"kind":"payment_hold_placed","status":"prepared","actor":"system","summary":"Held 450.00 USD on the family card.","createdAt":"2026-10-09T17:00:01Z"},{"kind":"task_submitted","status":"submitting","actor":"system","summary":"Sent the approved booking to Hallpass.","createdAt":"2026-10-09T17:00:01Z"},{"kind":"confirmation_received","status":"confirmed","actor":"hallpass","summary":"The camp confirmed reference LSC-20931.","createdAt":"2026-10-09T17:06:40Z"},{"kind":"payment_captured","status":"confirmed","actor":"system","summary":"Charged 425.00 USD to the family card.","createdAt":"2026-10-09T17:06:41Z"}],"waitlist":null,"canceledBy":null,"refund":null,"dueOnProviderSite":null,"note":null,"askAgainAfter":null,"replacesBookingId":null,"replacedByBookingId":null,"payment":{"paymentMethod":"default","creditsCents":0,"cardHoldCents":0,"serviceFeeCents":500},"cancellation":{"allowed":false,"action":"cancel","reason":"booked","deadline":null,"refundableCents":0,"methods":[]},"bookedBy":null,"hold":null},"providerAccount":null}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"description":"No such booking for this parent, or bookings are off.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Booking not found.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"No access to the family's bookings"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The route is registered only when the API runs with BOOKINGS_ENABLED=true; otherwise it returns 404.","x-curl-examples":[{"title":"Get a booking","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/bookings/$BOOKING_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/bookings/{bookingId}/decisions/{decisionId}":{"post":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Answer a camp question","description":"Accepts one of the offered options, or declines. Declining ends the booking and releases the hold.","parameters":[{"name":"bookingId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"decisionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["outcome"],"properties":{"outcome":{"type":"string","enum":["accept","decline"]},"optionId":{"type":"string","description":"Required to accept; must be absent to decline."},"answer":{"type":["string","null"],"minLength":1,"maxLength":2000,"description":"Your typed answer. Required to accept an `answer_required` decision; omit it otherwise."}}},"examples":{"accept":{"value":{"outcome":"accept","optionId":"medium"}},"decline":{"value":{"outcome":"decline"}}}}}},"responses":{"200":{"description":"The answer was recorded and checkout resumes.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["booking"],"properties":{"booking":{"$ref":"#/components/schemas/FamilyBookingDetail"}}}}}}}},"400":{"description":"`optionId` is missing when accepting, present when declining, or not one of the offered options, or Hallpass refused a typed answer that repeats an attached detail or holds a token, card number or email address (the message says which; fix the answer and send it again). A question that asks for a secret detail can't be answered here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Choose one of the offered options.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"$ref":"#/components/responses/BookingNotFound"},"409":{"description":"The question was already answered or has expired, or the booking is briefly busy. Read the booking again instead of retrying.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"That decision is no longer waiting for an answer.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"Hallpass couldn’t be reached. Nothing was recorded; try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"The answer could not be sent. Try again.","data":null}}}},"403":{"description":"The role may not book (book; a caregiver)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The route is registered only when the API runs with BOOKINGS_ENABLED=true; otherwise it returns 404.","x-curl-examples":[{"title":"Answer a camp question","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/bookings/$BOOKING_ID/decisions/$DECISION_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"outcome\": \"accept\", \"optionId\": \"medium\"}'"}]}},"/api/users/viewer/bookings/{bookingId}/cancel":{"post":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Cancel a booking","description":"Cancels the booking and releases the hold, as long as the camp’s checkout hasn’t started payment. An application still waiting for the camp’s answer (`phase: applied`) is withdrawn the same way: it ends cancelled by the parent, with nothing charged. The body is optional: `reason` is recorded. When the camp cancelled (`canceledBy: provider`), the same route records how the parent wants their payment back: send only `refundMethod` (`card` by default). `credits` returns everything to the wallet at once; `card` refunds the card part to the card (`refund.status` stays `pending` until Stripe confirms) and any credits part to the wallet. The amount is always what the family paid. Every booking's `cancellation` says beforehand what this route does with it (`action`), whether it accepts it now (`allowed`, else `reason`), and what it refunds. Cancelling again while a cancel is on its way (`cancellation.reason: cancel_requested`) returns the booking as it is.","parameters":[{"name":"bookingId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The cancellation was recorded. The status becomes `cancelled` once Hallpass confirms.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["booking"],"properties":{"booking":{"$ref":"#/components/schemas/FamilyBookingDetail"}}}}}}}},"400":{"description":"An unknown property, `refundMethod` or `reason`, or a `reason` for a booking the camp cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"refundMethod must be credits or card.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"$ref":"#/components/responses/BookingNotFound"},"409":{"description":"The booking’s `cancellation` is not allowed (already final, checkout has started payment, being checked with the camp, a refund was already chosen, or nothing was paid), or the booking is briefly busy.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"A refund was already chosen for this booking.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not book (book; a caregiver)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The route is registered only when the API runs with BOOKINGS_ENABLED=true; otherwise it returns 404.","x-curl-examples":[{"title":"Cancel a booking","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/bookings/$BOOKING_ID/cancel\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"reason\": \"plans_changed\"}'"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyBookingCancelInput"}}}}}},"/api/users/viewer":{"get":{"description":"Returns the retained account payload with a camelCase profile beside the snake_case user.","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyAccountViewer"}}},"description":"Success. Values are top-level (retained account response)."},"401":{"description":"Current account required"}},"security":[{"AccountCredential":[]}],"summary":"Read the signed-in account and its profile","tags":["Internal family account"],"x-auth":"Any current account credential (cookie or account key). Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\"","title":"Read the signed-in account and its profile"}],"x-internal":true}},"/api/users/update-viewer":{"post":{"description":"Changes the username and/or the named profile fields; null clears a profile field. Unknown keys are refused. Email changes go through the verification flow instead. Editing the phone does not change the SMS second factor, so a new number is unverified. Answers with the GET /api/users/viewer payload.","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"username":{"pattern":"^[a-z0-9_-]{3,32}$","type":"string"},"profile":{"additionalProperties":false,"properties":{"fullName":{"maxLength":120,"minLength":1,"type":["string","null"]},"preferredName":{"maxLength":60,"minLength":1,"type":["string","null"]},"phone":{"description":"Any common format; stored as E.164. null clears it.","type":["string","null"]}},"type":"object"}},"required":[],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyAccountViewer"}}},"description":"Success. Values are top-level (retained account response)."},"400":{"description":"Invalid field, unknown key, or nothing to change"},"401":{"description":"Current account required"},"409":{"description":"Username taken"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"summary":"Edit the username or profile","tags":["Internal family account"],"x-auth":"Any current account credential (cookie or account key). Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/update-viewer\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"profile\":{\"fullName\":\"Jordan Okafor\",\"preferredName\":\"Jordan\"}}'","title":"Edit the username or profile"}],"x-internal":true}},"/api/users/viewer/sessions":{"get":{"description":"Live sessions, most recently active first (at most 100). Sessions are created by each sign-in and named by the session cookie's sid claim; cookies without a sid are not listed.","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"sessions":{"items":{"$ref":"#/components/schemas/FamilyAccountDeviceSession"},"type":"array"}},"required":["sessions"],"type":"object","additionalProperties":false},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"summary":"List signed-in devices","tags":["Internal family account"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/sessions\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\"","title":"List signed-in devices"}],"x-internal":true}},"/api/users/viewer/sessions/{sessionId}":{"delete":{"description":"Revokes the session; the next request carrying its sid answers 401. Revoking the current session signs this device out.","parameters":[{"in":"path","name":"sessionId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"revoked":{"const":true}},"required":["revoked"],"type":"object","additionalProperties":false},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"401":{"description":"Current account required"},"404":{"description":"Not the viewer's live session"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"summary":"Sign out one device","tags":["Internal family account"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/sessions/$SESSION_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\"","title":"Sign out one device"}],"x-internal":true}},"/api/users/viewer/sessions/revoke-others":{"post":{"description":"Revokes every live session except the one this request's cookie names (all of them for a header credential). A password reset and a deletion request do the same.","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"revoked":{"minimum":0,"type":"integer"}},"required":["revoked"],"type":"object","additionalProperties":false},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"summary":"Sign out every other device","tags":["Internal family account"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/sessions/revoke-others\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\"","title":"Sign out every other device"}],"x-internal":true}},"/api/users/sign-out":{"post":{"description":"Revokes the session the cookie's sid names. Cookies without a sid and header credentials name no session and answer revoked:false. The caller still clears its own cookie.","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"revoked":{"type":"boolean"}},"required":["revoked"],"type":"object","additionalProperties":false},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"summary":"Sign out this device","tags":["Internal family account"],"x-auth":"Any current account credential (cookie or account key). Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/sign-out\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\"","title":"Sign out this device"}],"x-internal":true}},"/api/users/viewer/exports":{"post":{"description":"Queues an export. The worker builds one JSON archive of the account profile and every family record the viewer owns, then emails a download link that works until it expires after a few days. Answers 202.","responses":{"202":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"export":{"$ref":"#/components/schemas/FamilyAccountExport"}},"required":["export"],"type":"object","additionalProperties":false},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"401":{"description":"Current account required"},"429":{"description":"An export is unfinished or one was requested recently. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."}},"security":[{"AccountCredential":[]}],"summary":"Request a copy of the family's data","tags":["Internal family account"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/exports\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\"","title":"Request a copy of the family's data"}],"x-internal":true}},"/api/users/viewer/exports/{exportId}":{"get":{"description":"The viewer's own export. The archive itself is only available through the emailed link.","parameters":[{"in":"path","name":"exportId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"export":{"$ref":"#/components/schemas/FamilyAccountExport"}},"required":["export"],"type":"object","additionalProperties":false},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"401":{"description":"Current account required"},"404":{"description":"Not the viewer's export"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"summary":"Read a data export's status","tags":["Internal family account"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/exports/$EXPORT_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\"","title":"Read a data export's status"}],"x-internal":true}},"/api/users/viewer/places":{"get":{"description":"Send exactly one of q (a US city, ZIP code or street address, 2 to 120 characters) or latitude and longitude (Use my location). Returns at most 5 places with whether School's Out serves each. A street address keeps its address and the geocoder's precision beside an area label; a reverse lookup's precision is device. A placeId is valid for one hour and only for this viewer, for the household's search area, home and places. Limited to 30 lookups a minute per account.","parameters":[{"in":"query","name":"q","schema":{"maxLength":120,"minLength":2,"type":"string"}},{"in":"query","name":"latitude","schema":{"maximum":90,"minimum":-90,"type":"number"}},{"in":"query","name":"longitude","schema":{"maximum":180,"minimum":-180,"type":"number"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"places":{"items":{"$ref":"#/components/schemas/FamilyAccountPlace"},"maxItems":5,"type":"array"}},"required":["places"],"type":"object","additionalProperties":false},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Neither or both input forms, or an invalid value"},"401":{"description":"Current account required"},"429":{"description":"Lookup rate limit. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."},"503":{"description":"Place search is unavailable"}},"security":[{"AccountCredential":[]}],"summary":"Find a place","tags":["Internal family account"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/places?q=97202\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\"","title":"Find a home area"}],"x-internal":true}},"/api/global-settings":{"get":{"description":"Every global on/off setting by name, each its stored value or else its default. signups_enabled (default true): false refuses every route that creates an account with code signups_disabled, while existing accounts still sign in. referral_only (default false): true makes a new account need a referralCode (referral_code on the legacy app's routes), else the route refuses with code referral_required. Settings are stored by snake_case name in global_settings and answered as camelCase fields; new settings add fields, which clients ignore until they know them. The sign-up routes enforce the settings themselves; this read is for clients deciding what to show.","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"settings":{"properties":{"referralOnly":{"description":"Setting referral_only (default false): a new account needs a referral code.","type":"boolean"},"signupsEnabled":{"description":"Setting signups_enabled (default true): new accounts may be created.","type":"boolean"}},"required":["signupsEnabled","referralOnly"],"type":"object","additionalProperties":false}},"required":["settings"],"type":"object","additionalProperties":false},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"summary":"Read the global settings","tags":["Internal family account"],"x-auth":"Public.","x-curl-examples":[{"code":"curl \"https://apis.schoolsoutapp.com/api/global-settings\"","title":"Read the global settings"}],"x-internal":true}},"/api/users/viewer/feature-flags":{"get":{"description":"The signed-in account's own feature flags, each its override or else its default. agentic_booking_preview (default false): the site runs agentic booking on mock data for this account. It grants no booking access.","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"flags":{"properties":{"agenticBookingPreview":{"description":"Flag agentic_booking_preview (default false): agentic booking runs on mock data.","type":"boolean"}},"required":["agenticBookingPreview"],"type":"object","additionalProperties":false}},"required":["flags"],"type":"object","additionalProperties":false},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"401":{"description":"Not signed in"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"summary":"Read the viewer's feature flags","tags":["Internal family account"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level.","x-curl-examples":[{"code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/feature-flags\" -H \"x-api-key: $API_KEY\"","title":"Read the viewer's feature flags"}],"x-internal":true}},"/api/users/viewer/referral-redemptions":{"post":{"description":"For new accounts (created within the last 30 days) at sign-up or onboarding. Each account redeems one code, never its own household's. The referring household's credit is granted through the wallet, possibly later. Limited to 10 attempts an hour.","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":false,"properties":{"code":{"type":"string"}},"required":["code"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"redemption":{"$ref":"#/components/schemas/FamilyAccountReferralRedemption"}},"required":["redemption"],"type":"object","additionalProperties":false},"success":{"const":true,"type":"boolean"}},"required":["success","data"],"type":"object"}}},"description":"Success"},"400":{"description":"Malformed code or the household's own code"},"401":{"description":"Current account required"},"404":{"description":"Unknown code"},"409":{"description":"Already redeemed, the account is not new, or the code has admitted as many families as it can (code referral_limit_reached)"},"429":{"description":"Attempt limit. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."}},"security":[{"AccountCredential":[]}],"summary":"Redeem another household's referral code","tags":["Internal family account"],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/referral-redemptions\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"code\":\"OKAFOR-24\"}'","title":"Redeem another household's referral code"}],"x-internal":true}},"/api/users/viewer/google-calendar":{"get":{"summary":"Read the first Google Calendar connection","description":"Returns the viewer's first Google connection (in connection order) in the single-connection shape, without its token; without one, connected: false, nulls and the default sync choices. For app releases before multiple accounts; GET /api/users/viewer/calendars lists every connection. Personal: every adult of a family (owner, co-parent or caregiver) reaches only their own connection; nobody manages another adult's.","tags":["Internal family account"],"x-internal":true,"security":[{"AccountCredential":[]}],"x-auth":"Signed-in adult only; account cookie or account credential, never delegated service or machine keys. Cookie requests require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"$ref":"#/components/schemas/FamilyAccountGoogleCalendarConnection"}}}}}},"401":{"description":"Current adult account required; service delegation and machine keys are refused."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-curl-examples":[{"title":"Read the connection","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/google-calendar\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"patch":{"summary":"Change the first Google Calendar connection's choices","description":"As PATCH /api/users/viewer/calendars/{connectionId}, for the viewer's first Google connection, answering in the single-connection shape; 404 when the viewer has none. For app releases before multiple accounts. Personal: every adult of a family (owner, co-parent or caregiver) reaches only their own connection; nobody manages another adult's.","tags":["Internal family account"],"x-internal":true,"security":[{"AccountCredential":[]}],"x-auth":"Signed-in adult only; account cookie or account credential, never delegated service or machine keys. Cookie requests require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"$ref":"#/components/schemas/FamilyAccountGoogleCalendarConnection"}}}}}},"400":{"description":"Invalid input."},"401":{"description":"Current adult account required; service delegation and machine keys are refused."},"404":{"description":"The viewer has no Google connection."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-curl-examples":[{"title":"Also sync waitlisted sessions","code":"curl -X PATCH \"https://apis.schoolsoutapp.com/api/users/viewer/google-calendar\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"sync\":{\"waitlisted\":true}}'"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"sync":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"confirmed":{"type":"boolean"},"waitlisted":{"type":"boolean"}}},"calendars":{"type":"array","maxItems":50,"items":{"type":"string"},"description":"Replaces the reading selection with these calendar IDs, all from calendars. The cache refreshes on the next read."}},"minProperties":1}}}}},"delete":{"summary":"Disconnect the first Google Calendar connection","description":"As DELETE /api/users/viewer/calendars/{connectionId}, for the viewer's first Google connection; without one it answers {connected: false, revoked: false}. For app releases before multiple accounts. Personal: every adult of a family (owner, co-parent or caregiver) reaches only their own connection; nobody manages another adult's.","tags":["Internal family account"],"x-internal":true,"security":[{"AccountCredential":[]}],"x-auth":"Signed-in adult only; account cookie or account credential, never delegated service or machine keys. Cookie requests require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"$ref":"#/components/schemas/FamilyAccountCalendarDisconnection"}}}}}},"401":{"description":"Current adult account required; service delegation and machine keys are refused."},"409":{"description":"A grant was reconnected during revocation; retry against current state."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"501":{"description":"Google Calendar OAuth and token-sealing settings are not configured."},"502":{"description":"Google revocation failed; the stored connection is retained."}},"x-curl-examples":[{"title":"Disconnect Google Calendar","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/google-calendar\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/google-calendar/authorization":{"post":{"summary":"Start a browser or app Google Calendar connection","description":"Starts Google Calendar access (calendar.events.readonly and calendar.app.created, required, plus calendar.calendarlist.readonly, which the parent may decline; offline, with include_granted_scopes so an earlier grant is extended rather than replaced) and returns {url}. Web requests use an optional parent-site redirect and receive the browser nonce cookie. Proof requests send {redirect, challenge}: exactly schoolsout://google-calendar-callback (or exp://host:port/--/google-calendar-callback outside production), or a parent-site page when the site's server (a BFF) calls the API, and an unpadded base64url SHA-256 proof of a verifier the client keeps; their URL is a one-use 10-minute API browser bootstrap that sets the nonce in the browser, and the client completes with the verifier (POST .../google-calendar/complete). As for Gmail. Each Google account the parent grants becomes a connection of its own; granting an account already connected replaces that connection's grant. Personal: every adult of a family (owner, co-parent or caregiver) reaches only their own connection; nobody manages another adult's.","tags":["Internal family account"],"x-internal":true,"security":[{"AccountCredential":[]}],"x-auth":"Signed-in adult only; account cookie or account credential, never delegated service or machine keys. Cookie requests require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"$ref":"#/components/schemas/FamilyAccountGoogleCalendarAuthorization"}}}}}},"400":{"description":"Redirect not allowed, malformed proof, or unknown property."},"401":{"description":"Current adult account required; service delegation and machine keys are refused."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"501":{"description":"Google Calendar OAuth and token-sealing settings are not configured."}},"x-curl-examples":[{"title":"Start a Google Calendar connection from the installed app","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/google-calendar/authorization\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"redirect\":\"schoolsout://google-calendar-callback\",\"challenge\":\"$CHALLENGE\"}'"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":[],"properties":{"redirect":{"type":"string","maxLength":2048,"description":"Allowed parent page for web, schoolsout://google-calendar-callback for installed apps, or the narrow Expo Go callback in development."},"challenge":{"type":"string","pattern":"^[A-Za-z0-9_-]{43}$","description":"Unpadded base64url SHA-256 of the app-held verifier. Required for native callbacks and forbidden for web callbacks."}}}}}}}},"/api/users/viewer/google-calendar/complete":{"post":{"summary":"Complete a native Google Calendar connection","description":"Native completion accepts {code, verifier}. Consumes the five-minute ticket only for its starting adult; the original S256 proof must match a 43–128 character verifier. Exchanges the sealed Google authorization code, requires both scopes, and saves the grant as the viewer's connection to that Google account (a new connection, or the existing one whose grant it replaces; 409 when a new account would pass the 10-connection limit). It makes School's Out's own calendar in that account (bookings or not; a reconnection keeps it), refreshes its event cache at once and returns that connection in the single-connection shape (lastSyncedAt set; a failed first refresh, or a calendar Google will not make (push_failed), is recorded as failure, the grant is kept and the worker retries it). The family's bookings are pushed by the worker's next pass. Missing or wrong proof, expiry, replay and a partial grant return 400. Personal: every adult of a family (owner, co-parent or caregiver) reaches only their own connection; nobody manages another adult's.","tags":["Internal family account"],"x-internal":true,"security":[{"AccountCredential":[]}],"x-auth":"Signed-in adult only; account cookie or account credential, never delegated service or machine keys. Cookie requests require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"$ref":"#/components/schemas/FamilyAccountGoogleCalendarConnection"}}}}}},"400":{"description":"Invalid, expired or reused flow, wrong proof, or a rejected Google grant."},"401":{"description":"Current adult account required; service delegation and machine keys are refused."},"409":{"description":"Connecting a new account would pass the 10-connection limit."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"501":{"description":"Google Calendar OAuth and token-sealing settings are not configured."}},"x-curl-examples":[{"title":"Complete the app connection","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/google-calendar/complete\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"code\":\"$COMPLETION\",\"verifier\":\"$VERIFIER\"}'"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["code","verifier"],"properties":{"code":{"type":"string","format":"uuid"},"verifier":{"type":"string","minLength":43,"maxLength":128}}}}}}}},"/api/users/viewer/google-calendar/events":{"get":{"summary":"Read the family's cached calendar events (earlier path)","description":"The same read as GET /api/users/viewer/calendars/events, at the path app releases before multiple accounts use. Every role may read it.","tags":["Internal family account"],"x-internal":true,"security":[{"AccountCredential":[]}],"x-auth":"Signed-in adult only; account cookie or account credential, never delegated service or machine keys. Cookie requests require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"$ref":"#/components/schemas/FamilyAccountCalendarEvents"}}}}}},"400":{"description":"Missing or invalid dates, to before from, or a range over 400 days."},"401":{"description":"Current adult account required; service delegation and machine keys are refused."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-curl-examples":[{"title":"Read October's events","code":"curl --get \"https://apis.schoolsoutapp.com/api/users/viewer/google-calendar/events\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  --data-urlencode from=2026-10-01 --data-urlencode to=2026-10-31"}],"parameters":[{"in":"query","name":"from","required":true,"schema":{"type":"string","format":"date"},"description":"Inclusive local date (YYYY-MM-DD)."},{"in":"query","name":"to","required":true,"schema":{"type":"string","format":"date"},"description":"Inclusive local date (YYYY-MM-DD)."}]}},"/api/users/viewer/calendars":{"get":{"summary":"List the calendar connections","description":"Returns the viewer's own calendar connections, without their secrets, in the order they were first connected: every Google account (connect each through POST .../google-calendar/authorization; connecting an account already connected replaces its grant) and Apple iCloud account. Each connection has its own sync choices, calendars and failure. An adult has at most 10 connections. Personal: every adult of a family (owner, co-parent or caregiver) reaches only their own connections; nobody manages another adult's.","tags":["Internal family account"],"x-internal":true,"security":[{"AccountCredential":[]}],"x-auth":"Signed-in adult only; account cookie or account credential, never delegated service or machine keys. Cookie requests require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"$ref":"#/components/schemas/FamilyAccountCalendarConnections"}}}}}},"401":{"description":"Current adult account required; service delegation and machine keys are refused."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-curl-examples":[{"title":"List the connections","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/calendars\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/calendars/apple":{"post":{"summary":"Connect an Apple iCloud calendar","description":"Connects an Apple iCloud account with POST {appleId, password}: the Apple ID and an app-specific password the parent makes at account.apple.com (xxxx-xxxx-xxxx-xxxx; an Apple ID's own password never works and is refused before asking Apple). Apple checks them first over CalDAV (discovering the account's calendar home); a refused Apple ID or password returns 400 and stores nothing, and iCloud being unreachable returns 502. The password is sealed for the viewer (APPLE_CALENDAR_TOKEN_KEY) and saved as the viewer's connection to that Apple ID: a new one, or the existing one whose password it replaces; 409 when a new account would pass the 10-connection limit. As for a Google account, School's Out makes its own calendar in the account (the only one it writes; bookings are pushed by the worker's next pass), every other calendar is listed and read, the cache is refreshed at once, and the connection is returned (a failed first refresh is recorded as failure and retried by the worker). A password the parent later revokes reads as failure authorization_revoked until they connect again. 501 without APPLE_CALENDAR_TOKEN_KEY. Personal: every adult of a family (owner, co-parent or caregiver) reaches only their own connections; nobody manages another adult's.","tags":["Internal family account"],"x-internal":true,"security":[{"AccountCredential":[]}],"x-auth":"Signed-in adult only; account cookie or account credential, never delegated service or machine keys. Cookie requests require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"$ref":"#/components/schemas/FamilyAccountCalendarConnection"}}}}}},"400":{"description":"Invalid input, or Apple refused the Apple ID or app-specific password."},"401":{"description":"Current adult account required; service delegation and machine keys are refused."},"409":{"description":"Connecting a new account would pass the 10-connection limit."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"501":{"description":"Apple iCloud calendars are not configured."},"502":{"description":"iCloud could not be reached."}},"x-curl-examples":[{"title":"Connect an Apple iCloud account","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/calendars/apple\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"appleId\":\"parent@icloud.com\",\"password\":\"abcd-efgh-ijkl-mnop\"}'"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["appleId","password"],"properties":{"appleId":{"type":"string","format":"email","maxLength":320,"description":"The Apple ID's email address."},"password":{"type":"string","pattern":"^[A-Za-z]{4}-?[A-Za-z]{4}-?[A-Za-z]{4}-?[A-Za-z]{4}$","description":"An app-specific password from account.apple.com."}}}}}}}},"/api/users/viewer/calendars/{connectionId}":{"patch":{"summary":"Change a calendar connection's choices","description":"PATCH {sync?: {confirmed?, waitlisted?}, calendars?: string[]} changes which of the family's bookings the worker writes to School's Out's own calendar in this account, and which of the account's calendars are read. Unknown properties, non-boolean sync values and calendar IDs that are not in calendars return 400; 404 when the viewer has no such connection. Personal: every adult of a family (owner, co-parent or caregiver) reaches only their own connections; nobody manages another adult's.","tags":["Internal family account"],"x-internal":true,"security":[{"AccountCredential":[]}],"x-auth":"Signed-in adult only; account cookie or account credential, never delegated service or machine keys. Cookie requests require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"$ref":"#/components/schemas/FamilyAccountCalendarConnection"}}}}}},"400":{"description":"Invalid input."},"401":{"description":"Current adult account required; service delegation and machine keys are refused."},"404":{"description":"The viewer has no such connection."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-curl-examples":[{"title":"Also sync waitlisted sessions","code":"curl -X PATCH \"https://apis.schoolsoutapp.com/api/users/viewer/calendars/$CONNECTION_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"sync\":{\"waitlisted\":true}}'"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"sync":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"confirmed":{"type":"boolean"},"waitlisted":{"type":"boolean"}}},"calendars":{"type":"array","maxItems":50,"items":{"type":"string"},"description":"Replaces the reading selection with these calendar IDs, all from calendars. The cache refreshes on the next read."}},"minProperties":1}}}},"parameters":[{"in":"path","name":"connectionId","required":true,"schema":{"type":"string","format":"uuid"},"description":"A connection ID from GET /api/users/viewer/calendars."}]},"delete":{"summary":"Disconnect a calendar connection","description":"Disconnects one of the viewer's connections: deletes School's Out's own calendar from the account (and so every booking event School's Out wrote there; best effort, it never blocks the disconnect), revokes a Google grant at Google, then deletes the connection and its cached events and returns {connected: false, revoked}. A token Google already considers invalid counts as revoked. A Google revocation failure returns 502 and keeps the connection; a connection reconnected during the request returns 409. Apple cannot revoke an app-specific password for the parent, so an Apple disconnect answers revoked: false; the parent revokes the password at account.apple.com. 404 when the viewer has no such connection. Personal: every adult of a family (owner, co-parent or caregiver) reaches only their own connections; nobody manages another adult's.","tags":["Internal family account"],"x-internal":true,"security":[{"AccountCredential":[]}],"x-auth":"Signed-in adult only; account cookie or account credential, never delegated service or machine keys. Cookie requests require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"$ref":"#/components/schemas/FamilyAccountCalendarDisconnection"}}}}}},"401":{"description":"Current adult account required; service delegation and machine keys are refused."},"404":{"description":"The viewer has no such connection."},"409":{"description":"The account was reconnected while disconnecting; try again."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"502":{"description":"The provider did not confirm the revocation; the connection is kept."}},"x-curl-examples":[{"title":"Disconnect an account","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/calendars/$CONNECTION_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"parameters":[{"in":"path","name":"connectionId","required":true,"schema":{"type":"string","format":"uuid"},"description":"A connection ID from GET /api/users/viewer/calendars."}]}},"/api/users/viewer/calendars/events":{"get":{"summary":"Read the family's cached calendar events","description":"Returns the family's merged calendar: the cached events, overlapping the inclusive local date range in the household time zone, of every connection of every adult of the family the viewer acts in (its owner, co-parents and caregivers). The viewer's own events are whole; each other adult's event is a busy block with only its time span and the adult's name (busy: true; no title, calendar, link or children), enforced in this response. An event an adult has in several calendars or accounts (one event ID) appears once, and another adult's copy of an event the viewer has is left out. When one of the viewer's own caches is older than the refresh interval, the request refreshes it from its provider first; other adults' caches are refreshed by the worker. A failed refresh serves cached events with stale: true and is recorded on its connection. A family with no connected adult answers events: []. from and to are YYYY-MM-DD, to on or after from, covering at most 400 days. Each account's events come from the calendars chosen on its connection; a Google grant without the calendar-list scope reads only the primary calendar. Every role may read it.","tags":["Internal family account"],"x-internal":true,"security":[{"AccountCredential":[]}],"x-auth":"Signed-in adult only; account cookie or account credential, never delegated service or machine keys. Cookie requests require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"$ref":"#/components/schemas/FamilyAccountCalendarEvents"}}}}}},"400":{"description":"Missing or invalid dates, to before from, or a range over 400 days."},"401":{"description":"Current adult account required; service delegation and machine keys are refused."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-curl-examples":[{"title":"Read October's events","code":"curl --get \"https://apis.schoolsoutapp.com/api/users/viewer/calendars/events\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  --data-urlencode from=2026-10-01 --data-urlencode to=2026-10-31"}],"parameters":[{"in":"query","name":"from","required":true,"schema":{"type":"string","format":"date"},"description":"Inclusive local date (YYYY-MM-DD)."},{"in":"query","name":"to","required":true,"schema":{"type":"string","format":"date"},"description":"Inclusive local date (YYYY-MM-DD)."}]}},"/api/schools":{"get":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Search schools","description":"Finds schools by name (q, trigram word similarity), by nearness (near), or both; at least one is required. Name matches come first by similarity, then nearest; near alone orders nearest first. near=home is the family's household's saved search-area point (a co-parent's or caregiver's home is the family owner's), else the centre of its search-area ZIP code; with no home known and no q it finds nothing. distanceMiles is always from home. limit is 1 to 20 (default 10).","responses":{"200":{"description":"Matching schools.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["schools"],"properties":{"schools":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlanSchool"}}}}}},"example":{"success":true,"data":{"schools":[{"id":"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34","name":"Lincoln Elementary School","kind":"public","city":"Portland","region":"OR","postalCode":"97202","distanceMiles":0.7,"calendarStatus":"published"}]}}}}},"400":{"description":"Neither q nor near, q outside 2 to 120 characters, near not home or latitude,longitude, or limit outside 1 to 20."},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted origin.","x-curl-examples":[{"title":"Search schools","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/schools?q=lincoln\u0026near=home\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"parameters":[{"in":"query","name":"q","schema":{"type":"string","minLength":2,"maxLength":120}},{"in":"query","name":"near","schema":{"type":"string"},"description":"home, or latitude,longitude."},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":20}}]}},"/api/users/viewer/children/{childId}/school":{"get":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Read a child's school","description":"The school the child attends, or the parent's other answer; null when the parent has not answered. The child's owner and guardians may read it.","responses":{"200":{"description":"The child's school.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["school"],"properties":{"school":{"oneOf":[{"$ref":"#/components/schemas/FamilyPlanChildSchool"},{"type":"null"}]}}}}},"example":{"success":true,"data":{"school":{"schoolId":"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34","name":"Lincoln Elementary School","kind":"public"}}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"description":"Child not available to this account."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted origin.","x-curl-examples":[{"title":"Read a child's school","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/school\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"parameters":[{"in":"path","name":"childId","required":true,"schema":{"type":"string","format":"uuid"}}]},"put":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Set a child's school","description":"Records the school a child attends ({schoolId}) or another answer ({other: private|homeschool|not_found, name?}). Only the child's owner may change it; a guardian gets 403. Unknown properties are rejected.","responses":{"200":{"description":"The child's school.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["school"],"properties":{"school":{"$ref":"#/components/schemas/FamilyPlanChildSchool"}}}}},"example":{"success":true,"data":{"school":{"schoolId":"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34","name":"Lincoln Elementary School","kind":"public"}}}}}},"400":{"description":"Neither or both of schoolId and other, an unknown property, a malformed schoolId, an unknown other answer, or a name outside 1 to 120 characters."},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"403":{"description":"The viewer is the child's guardian, not its owner; The role may not add or edit children (edit_children; a caregiver)"},"404":{"description":"Child or school not found."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted origin.","x-curl-examples":[{"title":"Set a child's school","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/school\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"schoolId\": \"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34\"}'"}],"parameters":[{"in":"path","name":"childId","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","additionalProperties":false,"required":["schoolId"],"properties":{"schoolId":{"type":"string","format":"uuid"}}},{"type":"object","additionalProperties":false,"required":["other"],"properties":{"other":{"enum":["private","homeschool","not_found"]},"name":{"type":"string","minLength":1,"maxLength":120}}}]},"example":{"schoolId":"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34"}}}}},"delete":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Forget a child's school","description":"Removes the child's school answer. Only the child's owner may.","responses":{"200":{"description":"The answer was removed.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["school"],"properties":{"school":{"type":"null"}}}}},"example":{"success":true,"data":{"school":null}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"403":{"description":"The viewer is the child's guardian, not its owner; The role may not add or edit children (edit_children; a caregiver)"},"404":{"description":"Child not available to this account."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted origin.","x-curl-examples":[{"title":"Forget a child's school","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/school\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"parameters":[{"in":"path","name":"childId","required":true,"schema":{"type":"string","format":"uuid"}}]}},"/api/users/viewer/school-calendar":{"get":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Read the school calendar","description":"Each child's school with its days out and early releases between from and to (inclusive household dates; default today to 120 days ahead; at most 400 days), after the family's overrides. Children at one school share an entry unless their own child-specific overrides in the range differ. A school-wide override applies to every child at the school; a child's own override beats it; in_session removes a published day. A child whose answer names no school is an entry of its own with schoolId null and status none; children without an answer are left out.","responses":{"200":{"description":"The school calendar.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/FamilyPlanSchoolCalendar"}}},"example":{"success":true,"data":{"schools":[{"schoolId":"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34","name":"Lincoln Elementary School","childIds":["1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93"],"status":"published","term":{"start":"2026-09-01","end":"2027-06-15"},"weekdays":[0,1,2,3,4],"hours":{"start":"08:00","end":"14:40"},"days":[{"date":"2026-10-21","kind":"early_release","label":"Early Release Day","breakId":null,"breakName":null,"endsAt":"12:30","source":"school","updateId":null},{"date":"2026-11-25","kind":"break","label":"Fall break","breakId":"2026-11-25","breakName":"Fall break","endsAt":null,"source":"school","updateId":null},{"date":"2026-12-02","kind":"no_school","label":"Snow day","breakId":null,"breakName":null,"endsAt":null,"source":"email","updateId":"6f1d2c3b-4a5e-4f60-8b7a-1c2d3e4f5a6b"}]}]}}}}},"400":{"description":"from or to not a date, to before from, or a range over 400 days."},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted origin.","x-curl-examples":[{"title":"Read the school calendar","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/school-calendar?from=2026-10-16\u0026to=2027-02-13\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"parameters":[{"in":"query","name":"from","schema":{"type":"string","format":"date"}},{"in":"query","name":"to","schema":{"type":"string","format":"date"}}]}},"/api/users/viewer/plans":{"get":{"tags":["Internal family calendar"],"x-internal":true,"summary":"List plans","description":"Merges the viewer's Hallpass bookings (read through the booking service, never copied) with plans made here, including plans another adult made for a child the viewer shares (childIds then lists only the children the viewer can see), ordered by start date and then id, a page at a time. A group filter may return a short page with a cursor; keep reading until nextCursor is null. The source, activityId and sessionId filters combine with the others.","parameters":[{"name":"childId","in":"query","required":false,"description":"Only this child's plans.","schema":{"type":"string","format":"uuid"}},{"name":"from","in":"query","required":false,"description":"Plans that end on or after this date; also bounds dates.","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"Plans that start on or before this date; also bounds dates.","schema":{"type":"string","format":"date"}},{"name":"group","in":"query","required":false,"description":"Only plans in this group.","schema":{"enum":["active","pending","waitlist","closed","past"]}},{"name":"source","in":"query","required":false,"description":"Only plans from this source. booking reads only Hallpass bookings; external and covered read no bookings.","schema":{"enum":["booking","external","covered"]}},{"name":"activityId","in":"query","required":false,"description":"Only plans of this catalog activity.","schema":{"type":"string","format":"uuid"}},{"name":"sessionId","in":"query","required":false,"description":"Only plans of this catalog session. With source=external and a childId, this is how a client checks whether a child already has a plan of the session before adding one.","schema":{"type":"string","format":"uuid"}},{"name":"cursor","in":"query","required":false,"description":"nextCursor of the previous page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}}],"responses":{"200":{"description":"A page of plans.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["plans","nextCursor"],"properties":{"plans":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlan"}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}]}}}}},"example":{"success":true,"data":{"plans":[{"id":"plan:3b1e6f0a-2c4d-4e8f-9a1b-5c6d7e8f9a0b","source":"external","childIds":["1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93"],"title":"Saturday swim","organizationId":null,"activityId":null,"sessionId":null,"dates":["2026-10-17","2026-10-24"],"startTime":"09:00","endTime":"10:00","recurrence":{"weekdays":[5],"from":"2026-10-17","until":"2026-10-24"},"status":"registered","askAgainAfter":null,"group":"active","coveredBy":null,"note":null,"bookingId":null,"editable":true}],"nextCursor":null}}}}},"400":{"description":"A parameter is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"group must be active, pending, waitlist, closed or past.","data":null}}}},"404":{"description":"childId is not one of the viewer's children.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Child not found.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"List active plans","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/plans?group=active\u0026from=2026-10-01\u0026to=2026-12-31\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"post":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Add a plan","description":"Records a registration made on a provider's own site (external) or days the family covers itself (covered). A sessionId or activityId stores the catalog's organization and activity and, without a title, its name. An external plan of a session may leave out dates and recurrence: the plan then takes the session's days (the attendance dates its schedule lists, weekly rules on their weekdays without excluded dates, and every day of a date range from its start to its end, or only its start when it states no end; at most 400, earliest first) and, when startTime and endTime are left out too, the session's one daily clock if it has one. A session that states no dates answers 400. Explicit dates or a recurrence always win. An external plan of a catalog session (or, without a sessionId, of an activity) is recorded at most once per child: a child that already has an open (not canceled) external plan of the same session (or of the same activity with no session), made by any adult of the family, is not planned again. That plan keeps its dates, times and children, and moves to the requested status only when the status is further along unconfirmed \u003c needs_parent \u003c applied \u003c registered and the viewer may change the plan (editable); it never moves back. The other children get one new plan. The answer is 201 with created true and the new plan when a plan was made, else 200 with created false and plan the oldest of existingPlans. existingPlans lists the open plans that already covered some of the children, as they read after any status change. Title-only external plans and covered plans are never matched and always answer 201 with an empty existingPlans.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanInput"}}}},"responses":{"200":{"description":"Every child already had an open external plan of the session (or activity): nothing new was made. plan is the oldest of existingPlans.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["plan","created","existingPlans"],"properties":{"plan":{"$ref":"#/components/schemas/FamilyPlan"},"created":{"type":"boolean","description":"true when this request made a new plan; false when every child already had an open external plan of the target."},"existingPlans":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlan"},"description":"Open external plans of the same session (or activity) that already covered some of the requested children, after any status advance, oldest first."}}}}},"example":{"success":true,"data":{"plan":{"id":"plan:3b1e6f0a-2c4d-4e8f-9a1b-5c6d7e8f9a0b","source":"external","childIds":["1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93"],"title":"Robotics week","organizationId":"7c2d9e41-5a3b-4f6c-8d1e-2b4a6c8e0f13","activityId":"9e4f1a2b-3c5d-4e6f-8a7b-1c2d3e4f5a6b","sessionId":"5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d","dates":["2027-06-21","2027-06-22","2027-06-23","2027-06-24","2027-06-25"],"startTime":"09:00","endTime":"15:00","recurrence":null,"status":"registered","askAgainAfter":null,"group":"active","coveredBy":null,"note":null,"bookingId":null,"editable":true},"created":false,"existingPlans":[{"id":"plan:3b1e6f0a-2c4d-4e8f-9a1b-5c6d7e8f9a0b","source":"external","childIds":["1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93"],"title":"Robotics week","organizationId":"7c2d9e41-5a3b-4f6c-8d1e-2b4a6c8e0f13","activityId":"9e4f1a2b-3c5d-4e6f-8a7b-1c2d3e4f5a6b","sessionId":"5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d","dates":["2027-06-21","2027-06-22","2027-06-23","2027-06-24","2027-06-25"],"startTime":"09:00","endTime":"15:00","recurrence":null,"status":"registered","askAgainAfter":null,"group":"active","coveredBy":null,"note":null,"bookingId":null,"editable":true}]}}}}},"201":{"description":"A new plan was made (for the children that had no open external plan of the target).","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["plan","created","existingPlans"],"properties":{"plan":{"$ref":"#/components/schemas/FamilyPlan"},"created":{"type":"boolean","description":"true when this request made a new plan; false when every child already had an open external plan of the target."},"existingPlans":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlan"},"description":"Open external plans of the same session (or activity) that already covered some of the requested children, after any status advance, oldest first."}}}}},"example":{"success":true,"data":{"plan":{"id":"plan:3b1e6f0a-2c4d-4e8f-9a1b-5c6d7e8f9a0b","source":"external","childIds":["1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93"],"title":"Saturday swim","organizationId":null,"activityId":null,"sessionId":null,"dates":["2026-10-17","2026-10-24"],"startTime":"09:00","endTime":"10:00","recurrence":{"weekdays":[5],"from":"2026-10-17","until":"2026-10-24"},"status":"registered","askAgainAfter":null,"group":"active","coveredBy":null,"note":null,"bookingId":null,"editable":true},"created":true,"existingPlans":[]}}}}},"400":{"description":"The body is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Send either dates or recurrence.","data":null}}}},"404":{"description":"A child is not the viewer's, or the session or activity does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Session not found.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not add or change plans (write_plans; a caregiver), or the plan is another adult's and the viewer is not the owner"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Add a weekly class","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/plans\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"childIds\": [\"1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93\"], \"source\": \"external\", \"title\": \"Saturday swim\", \"recurrence\": {\"weekdays\": [5], \"from\": \"2026-10-17\", \"until\": null}, \"startTime\": \"09:00\", \"endTime\": \"10:00\"}'"}]}},"/api/users/viewer/plans/email-check":{"post":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Check email for registrations","description":"Runs only when the parent asks. Reads the viewer's own connected Gmail (gmail.readonly) for the last 60 days of mail whose subject reads like a registration confirmation, at most 20 messages, headers and Gmail's snippet only: no body or attachment is read or stored. Promotions and subjects that announce or chase a registration (\"now open\", \"reminder\") are skipped. Each message is matched to one organization the viewer saved or whose camp, activity or session page they viewed in the last 90 days: by the sender's domain (the organization's website domain or a subdomain of it, or its contact address's domain, never a public mailbox provider), else by the organization's name in the subject or the sender's name. It is then tied to one upcoming session of that organization (a session or activity name in the subject or snippet, a session the viewer viewed, or the organization's only upcoming session) and to the children the subject or snippet names by first name (or the only child the viewer sees). Each fully matched message becomes, once per Gmail message, a plan update (kind question, source email: the message id, the matched ids and the subject, at most 200 characters) and an unconfirmed external plan of the session with the session's days and daily clock, for the children without an open external plan of it already. Home then asks \"From your email: did you register?\" and the parent answers through PATCH /api/users/viewer/plans/{planId} (registered or canceled); nothing is ever confirmed without that answer. The update is undoable (deleting the plan) when the viewer is the family's owner. Messages from an organization the check could not tie to one session or to the children are listed in unresolved and recorded nowhere; they are listed again on the next check.","responses":{"200":{"description":"What the check found.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["checked","added","unresolved"],"properties":{"checked":{"type":"integer","minimum":0,"description":"Messages read."},"added":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPlan"},"description":"The unconfirmed plans this check made."},"unresolved":{"type":"array","items":{"$ref":"#/components/schemas/FamilyEmailCheckUnresolved"}}}}}},"example":{"success":true,"data":{"checked":7,"added":[{"id":"plan:3b1e6f0a-2c4d-4e8f-9a1b-5c6d7e8f9a0b","source":"external","childIds":["1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93"],"title":"Robotics week","organizationId":"7c2d9e41-5a3b-4f6c-8d1e-2b4a6c8e0f13","activityId":"9e4f1a2b-3c5d-4e6f-8a7b-1c2d3e4f5a6b","sessionId":"5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d","dates":["2027-06-21","2027-06-22","2027-06-23","2027-06-24","2027-06-25"],"startTime":"09:00","endTime":"15:00","recurrence":null,"status":"unconfirmed","askAgainAfter":null,"group":"pending","coveredBy":null,"note":null,"bookingId":null,"editable":true}],"unresolved":[{"messageId":"18c2f0a9b7d3e4f1","organizationId":"7c2d9e41-5a3b-4f6c-8d1e-2b4a6c8e0f13","organizationName":"Portland Robotics","sessionId":null,"activityId":null,"reason":"session"}]}}}}},"403":{"description":"The role may not add plans (write_plans), or the request is not the viewer's own account."},"409":{"description":"Gmail is not connected, lacks the read permission, or its grant expired or was revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Connect Gmail in Connected accounts so Mellow can help with your email.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"501":{"description":"The API has no Gmail settings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Email checks are not available.","data":null}}}},"502":{"description":"Gmail could not be reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Gmail could not be reached. Try again in a moment.","data":null}}}}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Check email for registrations","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/plans/email-check\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/plans/{planId}":{"patch":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Change a plan","description":"Marks an external plan registered, cancels a plan, replaces its dates, or sets its note.","parameters":[{"name":"planId","in":"path","required":true,"description":"\"plan:\u003cuuid\u003e\"; a \"booking:\u003cuuid\u003e\" answers 409.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanChangeInput"}}}},"responses":{"200":{"description":"The changed plan.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["plan"],"properties":{"plan":{"$ref":"#/components/schemas/FamilyPlan"}}}}}}}},"400":{"description":"The body is invalid, or registered was sent for a covered plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Only an external plan can be marked registered.","data":null}}}},"403":{"description":"The plan is for a shared child but another adult made it; The role may not add or change plans (write_plans; a caregiver), or the plan is another adult's and the viewer is not the owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Only the parent who made this plan can change it.","data":null}}}},"404":{"description":"No such plan of the viewer's.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Plan not found.","data":null}}}},"409":{"description":"A booking plan changes through its booking.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"A booking changes through the booking, not as a plan.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Mark registered","code":"curl -X PATCH \"https://apis.schoolsoutapp.com/api/users/viewer/plans/plan:$PLAN_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"status\": \"registered\"}'"}]},"delete":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Delete a plan","description":"Deletes a plan made here. Bookings are cancelled with the booking cancel route instead.","parameters":[{"name":"planId","in":"path","required":true,"description":"\"plan:\u003cuuid\u003e\"; a \"booking:\u003cuuid\u003e\" answers 409.","schema":{"type":"string"}}],"responses":{"200":{"description":"The plan is gone.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["deleted"],"properties":{"deleted":{"const":true}}}}},"example":{"success":true,"data":{"deleted":true}}}}},"403":{"description":"The plan is for a shared child but another adult made it; The role may not add or change plans (write_plans; a caregiver), or the plan is another adult's and the viewer is not the owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Only the parent who made this plan can change it.","data":null}}}},"404":{"description":"No such plan of the viewer's.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Plan not found.","data":null}}}},"409":{"description":"A booking plan changes through its booking.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"A booking changes through the booking, not as a plan.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Delete a plan","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/plans/plan:$PLAN_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/day-marks/{date}":{"put":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Mark a day","description":"Merges the family's answers for one day: each present key replaces the stored one and null clears it. A day left with no marks is removed. The date must be from 30 days before to 400 days after today in the household's time zone.","parameters":[{"name":"date","in":"path","required":true,"description":"Household date.","schema":{"type":"string","format":"date"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanDayMarks"}}}},"responses":{"200":{"description":"The day's marks after the merge.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["date","marks"],"properties":{"date":{"type":"string","format":"date"},"marks":{"$ref":"#/components/schemas/FamilyPlanDayMarks"}}}}},"example":{"success":true,"data":{"date":"2026-11-25","marks":{"open":"open","children":{"1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93":{"need":"not_needed"}}}}}}}},"400":{"description":"The date or a mark is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Unknown day mark: color.","data":null}}}},"404":{"description":"A child is not the viewer's.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Child not found.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not add or change plans (write_plans; a caregiver), or the plan is another adult's and the viewer is not the owner"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Share an open day","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/day-marks/2026-11-25\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"open\": \"open\"}'"}]}},"/api/users/viewer/calendar":{"get":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Read the calendar","description":"Composes, per day and child: the school calendar, bookings and plans, the family's day marks, registration openings and friends' open days; with detail=day also the cached Google events and each child's entries. A gap is a no-school or break day on a usual school day, with no planned cover, during which the household's caregiver (when set) is not available for the school's hours.","parameters":[{"name":"from","in":"query","required":false,"description":"First day; default today.","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"Last day; at most 400 days from from. Default about five weeks from from.","schema":{"type":"string","format":"date"}},{"name":"childId","in":"query","required":false,"description":"Only this child.","schema":{"type":"string","format":"uuid"}},{"name":"detail","in":"query","required":false,"description":"day adds entries and events and covers at most 45 days.","schema":{"enum":["day","summary"],"default":"summary"}}],"responses":{"200":{"description":"The days of the range and per-month counts.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/FamilyPlanCalendar"}}}}}},"400":{"description":"The range or detail is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"detail=day covers at most 45 days.","data":null}}}},"404":{"description":"childId is not one of the viewer's children.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Child not found.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Read a month","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/calendar?from=2026-11-01\u0026to=2026-11-30\u0026detail=day\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/home":{"get":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Read Home","description":"Home's cards: what needs the parent (provider cancellations awaiting a refund choice, spots that opened, applications to confirm, unfinished bookings, tasks on a provider's site, missing child or form details, unconfirmed external plans, conflicts and email questions), upcoming days off without cover grouped by break (state idea, with activity ids, when saved or followed options are available), recent updates, registration openings, waitlist positions and the next booked session. Dismissed items are left out. Ranking and counts are not part of the contract.","responses":{"200":{"description":"Home.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/FamilyPlanHome"}}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Read Home","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/home\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/home/dismissals/{itemId}":{"post":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Dismiss a Home item","description":"Hides a Home item for good, or until a future time. Dismissing again replaces the earlier dismissal. An update is dismissed as \"update:\u003cid\u003e\".","parameters":[{"name":"itemId","in":"path","required":true,"description":"The item's id.","schema":{"type":"string","pattern":"^[a-z_]+:[A-Za-z0-9-]{1,80}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"until":{"type":"string","format":"date-time","description":"Omitted hides the item for good."}}}}}},"responses":{"200":{"description":"The item is hidden.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["dismissed"],"properties":{"dismissed":{"const":true}}}}},"example":{"success":true,"data":{"dismissed":true}}}}},"400":{"description":"The item id or until is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"until must be a future RFC 3339 time.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Ask again in three days","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/home/dismissals/confirm_registration:$BOOKING_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"until\": \"2026-10-19T18:00:00Z\"}'"}]},"delete":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Restore a Home item","description":"Shows a dismissed item again. Restoring an item that is not dismissed succeeds.","parameters":[{"name":"itemId","in":"path","required":true,"description":"The item's id.","schema":{"type":"string","pattern":"^[a-z_]+:[A-Za-z0-9-]{1,80}$"}}],"responses":{"200":{"description":"The item shows again.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["dismissed"],"properties":{"dismissed":{"const":false}}}}},"example":{"success":true,"data":{"dismissed":false}}}}},"400":{"description":"The item id is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"itemId must look like \u003ckind\u003e:\u003csubjectId\u003e.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Undo a dismissal","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/home/dismissals/confirm_registration:$BOOKING_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/home/updates/{updateId}/undo":{"post":{"tags":["Internal family calendar"],"x-internal":true,"summary":"Undo an update","description":"Reverts what an update applied, such as school days or a plan learned from email, and marks it undone, in one transaction.","parameters":[{"name":"updateId","in":"path","required":true,"description":"The update's id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The update, now undone.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["update"],"properties":{"update":{"$ref":"#/components/schemas/FamilyPlanHomeUpdate"}}}}}}}},"404":{"description":"No such update of the viewer's.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"Update not found.","data":null}}}},"409":{"description":"The update was already undone or cannot be undone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyPlanError"},"example":{"error":true,"message":"This update was already undone.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not add or change plans (write_plans; a caregiver), or the plan is another adult's and the viewer is not the owner"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Undo an update","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/home/updates/$UPDATE_ID/undo\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/bookings/{bookingId}/change":{"post":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Change a booking","description":"Moves a booked booking to another open session of the same activity. It starts a new, linked booking with the same child, answers and shared fields (no waitlist); the original stays booked and is cancelled, and its payment refunded by `refundMethod`, only once the new one is booked. If the new one gets no place the original is untouched. Staff cancel the original registration with the camp. `warnings` contains `overlap` when the new dates overlap another booking of the child.","parameters":[{"name":"bookingId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyBookingChangeInput"}}}},"responses":{"200":{"description":"The existing new booking for this `idempotencyKey` (a replay).","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["booking","replaces","warnings"],"properties":{"booking":{"$ref":"#/components/schemas/FamilyBookingDetail"},"replaces":{"type":"string","format":"uuid"},"warnings":{"type":"array","items":{"type":"string"}}}}}}}}},"202":{"description":"The new booking.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["booking","replaces","warnings"],"properties":{"booking":{"$ref":"#/components/schemas/FamilyBookingDetail"},"replaces":{"type":"string","format":"uuid"},"warnings":{"type":"array","items":{"type":"string","enum":["overlap"],"x-open-enum":true}}}}}},"example":{"success":true,"data":{"booking":{"id":"9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d","phase":"in_progress","status":"accepted","paymentStatus":"held","childProfileId":"1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93","organization":{"id":"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34","name":"Lakeside Science Camp"},"activity":{"id":"7d3b9f60-2a4c-4e8b-9f1d-6c5a0e2b8d17","name":"Summer Science Camp"},"session":{"id":"d4b8f2a1-7c3e-4f9d-8a6b-1e2f3c4d5a6b","name":"Robotics Week 2","startDate":"2027-07-19","endDate":"2027-07-23","startTime":"09:00","endTime":"15:00","dates":[],"dayTimes":[]},"approval":{"approvedAt":"2026-10-09T17:00:00Z","maxSpend":{"amountCents":45000,"currency":"USD"},"allowWaitlist":false,"autoAcceptDecisions":false,"sharedFields":["child.birthDate"]},"progressStage":null,"hold":null,"decision":null,"decisions":[],"receipt":null,"failure":null,"checkingWith":null,"pollAfterMs":2000,"createdAt":"2026-10-09T17:00:00Z","updatedAt":"2026-10-09T17:06:41Z","events":[{"kind":"approval_granted","status":"authorizing","actor":"parent","summary":"Approved the booking with a spending limit of 450.00 USD.","createdAt":"2026-10-09T17:00:00Z"}],"waitlist":null,"canceledBy":null,"refund":null,"dueOnProviderSite":null,"note":null,"askAgainAfter":null,"replacesBookingId":"6f1d2c3b-4a5e-4f60-8b7a-1c2d3e4f5a6b","replacedByBookingId":null,"payment":{"paymentMethod":"default","creditsCents":0,"cardHoldCents":45500,"serviceFeeCents":500},"cancellation":{"allowed":true,"action":"cancel","reason":null,"deadline":null,"refundableCents":0,"methods":[]},"bookedBy":null},"replaces":"6f1d2c3b-4a5e-4f60-8b7a-1c2d3e4f5a6b","warnings":[]}}}}},"400":{"description":"An unknown property or an invalid id or amount.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"sessionId must be a UUID.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"$ref":"#/components/responses/BookingNotFound"},"409":{"description":"The booking is not booked, is already being changed, or the profile can no longer be used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Only a booked booking can be changed.","data":null}}}},"422":{"description":"The session is in another activity, is the same session, or is waitlist-only or closed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Choose another open session of the same activity.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not book (book; a caregiver)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The route is registered only when the API runs with BOOKINGS_ENABLED=true; otherwise it returns 404.","x-curl-examples":[{"title":"Change a booking","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/bookings/$BOOKING_ID/change\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"idempotencyKey\": \"3e9a1c7b-5d2f-4b8e-a6c4-0f1e2d3c4b5a\", \"sessionId\": \"d4b8f2a1-7c3e-4f9d-8a6b-1e2f3c4d5a6b\", \"refundMethod\": \"credits\"}'"}]}},"/api/users/viewer/bookings/{bookingId}/outcome":{"post":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Report a booking outcome","description":"Records the parent’s answer about what happened on the camp’s side. Each answer is an event with actor `parent`.","parameters":[{"name":"bookingId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyBookingOutcomeInput"}}}},"responses":{"200":{"description":"The updated booking.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["booking"],"properties":{"booking":{"$ref":"#/components/schemas/FamilyBookingDetail"}}}}}}}},"400":{"description":"Unknown property or outcome.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"outcome must be registered, not_registered, not_yet or provider_task_done.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"$ref":"#/components/responses/BookingNotFound"},"409":{"description":"The outcome does not apply to the booking now: the first three need an applied booking, `provider_task_done` needs `dueOnProviderSite`; or the booking is briefly busy.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Only an application waiting for the camp's answer can be marked registered.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not book (book; a caregiver)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The route is registered only when the API runs with BOOKINGS_ENABLED=true; otherwise it returns 404.","x-curl-examples":[{"title":"Report a booking outcome","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/bookings/$BOOKING_ID/outcome\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"outcome\": \"registered\"}'"}]}},"/api/sessions/{sessionId}/booking-form":{"get":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Get a session's booking form","description":"Returns what an approval for the session shares of the child's profile, for the checklist before Book. Send `shareFields` from it with the approval.","parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The route is registered only when the API runs with BOOKINGS_ENABLED=true; otherwise it returns 404.","x-curl-examples":[{"title":"Get a session's booking form","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/sessions/$SESSION_ID/booking-form\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"responses":{"200":{"description":"The booking form.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["form"],"properties":{"form":{"$ref":"#/components/schemas/FamilyBookingForm"}}}}},"example":{"success":true,"data":{"form":{"sessionId":"c3a9e1f2-6d4b-4a8c-b2e7-0f5d9c1a3b48","alwaysShared":["child.firstName","child.lastName","guardian.email","guardian.name"],"shareFields":["child.allergies","child.birthDate","child.grade","guardian.phone"]}}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"description":"No such live session, or bookings are off.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Session not found.","data":null}}}},"403":{"description":"The role may not book (book; a caregiver)"}}}},"/api/users/viewer/wallet":{"get":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Get the wallet","description":"Returns the wallet balance: credits the family added or was refunded, less what is spent or held for bookings.","responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["wallet"],"properties":{"wallet":{"$ref":"#/components/schemas/FamilyWallet"}}}}},"example":{"success":true,"data":{"wallet":{"balanceCents":2500,"currency":"USD"}}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not see money (read_money; a caregiver)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Get the wallet","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/wallet\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/provider-accounts":{"get":{"tags":["Internal family bookings"],"x-internal":true,"summary":"List provider accounts","description":"Lists the provider accounts Hallpass created for the signed-in adult and keeps for their later bookings, such as a CourseStorm account made during a booking. Each shows its site and masked username, never a password. They are the adult's own, whatever their family role.","responses":{"200":{"description":"The adult's provider accounts.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["providerAccounts"],"properties":{"providerAccounts":{"type":"array","items":{"$ref":"#/components/schemas/FamilyProviderAccount"}}}}}},"example":{"success":true,"data":{"providerAccounts":[{"platform":"coursestorm","provider":"CourseStorm","site":"https://coursestorm.com/","usernameDisplay":"r…@example.com","createdAt":"2026-11-14T17:02:11Z","lastUsedAt":null}]}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"$ref":"#/components/responses/BookingsDisabled"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The route is registered only when the API runs with BOOKINGS_ENABLED=true; otherwise it returns 404.","x-curl-examples":[{"title":"List provider accounts","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/provider-accounts\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/provider-accounts/{platform}":{"delete":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Remove a provider account","description":"Erases the adult's provider account for a platform from School's Out. The account itself stays at the provider; a later booking there creates a new one, or stops when one already exists for the adult's email.","parameters":[{"name":"platform","in":"path","required":true,"schema":{"type":"string","enum":["coursestorm"]}}],"responses":{"200":{"description":"The adult's remaining provider accounts.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["providerAccounts"],"properties":{"providerAccounts":{"type":"array","items":{"$ref":"#/components/schemas/FamilyProviderAccount"}}}}}},"example":{"success":true,"data":{"providerAccounts":[{"platform":"coursestorm","provider":"CourseStorm","site":"https://coursestorm.com/","usernameDisplay":"r…@example.com","createdAt":"2026-11-14T17:02:11Z","lastUsedAt":null}]}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"description":"Bookings are off, or the adult has no account for that platform.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Provider account not found.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The route is registered only when the API runs with BOOKINGS_ENABLED=true; otherwise it returns 404.","x-curl-examples":[{"title":"Remove a provider account","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/provider-accounts/coursestorm\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/wallet/entries":{"get":{"tags":["Internal family bookings"],"x-internal":true,"summary":"List wallet entries","description":"Returns wallet history newest first, a page at a time. These entries also back the Payments history.","parameters":[{"name":"kind","in":"query","required":false,"schema":{"type":"string","enum":["top_up","refund","booking_payment","booking_hold","hold_released","adjustment","gift_card","referral","group_rate"],"x-open-enum":true},"description":"Only entries of this kind."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Page size, at most 100."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"`nextCursor` from the previous page."}],"responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["entries","nextCursor"],"properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/FamilyWalletEntry"}},"nextCursor":{"type":["string","null"]}}}}},"example":{"success":true,"data":{"entries":[{"id":"1042","kind":"top_up","label":"Credits added","detail":null,"amountCents":2500,"method":{"brand":"visa","last4":"4242"},"bookingId":null,"occurredAt":"2026-10-16T09:00:00Z"}],"nextCursor":null}}}}},"400":{"description":"Unknown kind, or an invalid limit or cursor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"kind is not a wallet entry kind.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not see money (read_money; a caregiver)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"List wallet entries","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/wallet/entries\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/payments":{"get":{"tags":["Internal family bookings"],"x-internal":true,"summary":"List payments","description":"Returns the family’s payments newest first, a page at a time, for Settings › Payments: each ledger transaction that moved the family’s money once, with what it took from or returned to the card and the wallet. Booking payments, refunds, wallet top-ups, gift card purchases and redemptions, referral credits and group rate credits are listed; credits held for a booking are not payments until the booking is paid.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Page size, at most 100."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"`nextCursor` from the previous page."}],"responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["payments","nextCursor"],"properties":{"payments":{"type":"array","items":{"$ref":"#/components/schemas/FamilyPayment"}},"nextCursor":{"type":["string","null"]}}}}},"example":{"success":true,"data":{"payments":[{"id":"7f1c2e4a-5b6d-4c8e-9f0a-1b2c3d4e5f60","kind":"booking_payment","label":"Paid · STEAM Discovery Camp","detail":"Experiment PDX · Maya","cardCents":-36500,"walletCents":-6000,"currency":"USD","card":null,"serviceFeeCents":500,"bookingId":"0b6a1f9e-3c2d-4e5f-8a7b-9c0d1e2f3a4b","giftCardId":null,"occurredAt":"2026-10-16T09:00:00Z"},{"id":"2a3b4c5d-6e7f-4809-8a1b-2c3d4e5f6a7b","kind":"top_up","label":"Credits added","detail":null,"cardCents":-10000,"walletCents":10000,"currency":"USD","card":{"brand":"visa","last4":"4242"},"serviceFeeCents":null,"bookingId":null,"giftCardId":null,"occurredAt":"2026-10-15T18:30:00Z"}],"nextCursor":null}}}}},"400":{"description":"An invalid limit or cursor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"cursor is not a cursor this route returned.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not see money (read_money; a caregiver)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"List payments","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/payments\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/payments/{paymentId}/receipt":{"post":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Email a payment's receipt","description":"Emails a fresh copy of one of the family’s payments’ receipts to the receipt email (else the account email), whatever kind of payment it is and whether automatic receipts are on. Receipts are School’s Out’s own: the camp’s organization and address, the child, the session dates, the camp price, service fee and credits applied, how it was paid, and the references. Every booking payment, refund, top-up and gift card purchase is also emailed its receipt when it is posted, unless the family turned `receipts` off in its notification preferences. The email is queued in the outbox and sent by the worker.","parameters":[{"name":"paymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"A payment’s `id` from GET /api/users/viewer/payments."}],"responses":{"202":{"description":"Queued.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["sentTo"],"properties":{"sentTo":{"type":"string","format":"email","description":"Where the receipt goes."}}}}},"example":{"success":true,"data":{"sentTo":"sam@example.com"}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"description":"Not one of the family’s payments.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Payment not found.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"This deployment cannot seal emails (no API_AES_KEY).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"We can’t email receipts right now. Try again later.","data":null}}}},"403":{"description":"The role may not see money (read_money; a caregiver)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Email a payment's receipt","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/payments/7f1c2e4a-5b6d-4c8e-9f0a-1b2c3d4e5f60/receipt\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/admin/family-bookings/follow-ups":{"get":{"tags":["Internal family bookings"],"x-internal":true,"summary":"List bookings waiting on staff","description":"For a staff or ADMIN person on their own account (an ADMIN machine key, a service call or a preview gets 403): bookings whose card refund or capture Stripe refused, most recently changed first. The system does not retry these; a refused refund stays `pending` for the parent until staff move the money and resolve the booking with `PATCH /api/admin/family-bookings/{bookingId}/follow-up`, which takes it off this list. The first page (no cursor) also lists, as `charges`, the wallet top-ups and gift card purchases closed as unresolved because Stripe never named their PaymentIntent in time (newest first, at most `limit`); bookings whose camp deferred payment are listed with reason `payment_deferred` until staff release or capture their kept hold.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Page size, at most 100."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"`nextCursor` from the previous page."}],"responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["followUps","charges","nextCursor"],"properties":{"followUps":{"type":"array","items":{"$ref":"#/components/schemas/FamilyBookingStaffFollowUp"}},"charges":{"type":"array","items":{"$ref":"#/components/schemas/FamilyStaffChargeFollowUp"},"description":"Only on the first page; empty on later pages."},"nextCursor":{"type":["string","null"]}}}}},"example":{"success":true,"data":{"followUps":[{"bookingId":"0b6a1f9e-3c2d-4e5f-8a7b-9c0d1e2f3a4b","ownerUserId":"4d2c1b0a-9e8f-4a7b-8c6d-5e4f3a2b1c0d","reason":"card_refund_refused","status":"cancelled","paymentStatus":"captured","paymentIntentId":"pi_3QxExample","refund":{"method":"card","amountCents":42500,"walletCents":0,"status":"pending"},"cardPaidCents":42500,"currency":"USD","organizationName":"Experiment PDX","sessionName":"STEAM Discovery Camp","updatedAt":"2026-10-16T09:00:00Z"}],"nextCursor":null,"charges":[]}}}}},"400":{"description":"An invalid limit or cursor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"cursor is not a cursor this route returned.","data":null}}}},"401":{"description":"Sign-in required."},"403":{"description":"The viewer is not ADMIN or staff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"An ADMIN or staff user is required.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"ADMIN or staff accounts only (cookie or account credential).","x-curl-examples":[{"title":"List bookings waiting on staff","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/admin/family-bookings/follow-ups\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/admin/family-bookings/{bookingId}/follow-up":{"patch":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Resolve a booking waiting on staff","description":"For a staff or ADMIN person on their own account (an ADMIN machine key, a service call or a preview gets 403, because the `reconciled` event names the person): records how staff settled a booking's money by hand and takes it off the follow-up list. A `card_refund_refused` booking is resolved as `refunded`: staff refunded the card part through Stripe, so the parent's refund becomes `done`, the ledger records the card refund as the automatic refund would, and the parent is notified. A `card_capture_refused` booking is resolved as `collected` or `written_off`; either way any wallet credits still held for it go back to the wallet. A `payment_deferred` booking is resolved as `hold_released` (nothing is owed to the camp: only the booking's service fee is charged, and the rest of its card hold and held credits go back) or `hold_captured` with `amountCents`, what School's Out paid the camp (at most the spending limit): the payment and the service fee are recorded and charged like a capture, held credits first, then the card hold. A `card_capture_refused` booking's collected or written-off amount includes its service fee. Each resolution is recorded as a staff `reconciled` event with the note.","parameters":[{"name":"bookingId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["resolution","note"],"properties":{"resolution":{"type":"string","enum":["refunded","collected","written_off","hold_released","hold_captured"],"description":"`refunded` answers `card_refund_refused`; `collected` and `written_off` answer `card_capture_refused`; `hold_released` and `hold_captured` answer `payment_deferred`."},"note":{"type":"string","minLength":1,"maxLength":1000,"description":"What staff did, kept on the booking's staff event. Surrounding whitespace is trimmed."},"amountCents":{"type":"integer","minimum":1,"description":"With `hold_captured` only: what School's Out paid the camp, in the spending limit's currency, at most the limit."}}},"example":{"resolution":"refunded","note":"Refunded $425.00 in the Stripe dashboard, re_3QxExample."}}}},"responses":{"200":{"description":"The recorded resolution.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["resolved"],"properties":{"resolved":{"$ref":"#/components/schemas/FamilyBookingStaffResolution"}}}}},"example":{"success":true,"data":{"resolved":{"bookingId":"0b6a1f9e-3c2d-4e5f-8a7b-9c0d1e2f3a4b","reason":"card_refund_refused","resolution":"refunded","note":"Refunded $425.00 in the Stripe dashboard, re_3QxExample."}}}}}},"400":{"description":"Unknown property, an unknown resolution, a note that is empty or longer than 1000 characters, or amountCents missing from `hold_captured` or sent with another resolution.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"note must be 1 to 1000 characters.","data":null}}}},"401":{"description":"Sign-in required."},"403":{"description":"The viewer is not ADMIN or staff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"An ADMIN or staff user is required.","data":null}}}},"404":{"description":"No such booking, or bookings are disabled (BOOKINGS_ENABLED is not true).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Booking not found.","data":null}}}},"409":{"description":"Nothing on the booking waits on staff (already resolved), another update holds the booking (retry), or Stripe refused to capture a deferred payment's hold (it may have expired; resolve it as `hold_released` instead).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Nothing on this booking waits on staff.","data":null}}}},"422":{"description":"The resolution does not answer the booking's follow-up reason, or amountCents exceeds the spending limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"A card_capture_refused follow-up cannot be resolved as refunded.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"Stripe did not confirm releasing or capturing a deferred payment's hold; nothing was recorded. Send the same resolution again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Stripe did not confirm. Send the same resolution again.","data":null}}}}},"security":[{"AccountCredential":[]}],"x-auth":"ADMIN or staff accounts only (cookie or account credential). Cookie mutations require CSRF. The route is registered only when the API runs with BOOKINGS_ENABLED=true; otherwise it returns 404.","x-curl-examples":[{"title":"Resolve a booking waiting on staff","code":"curl -X PATCH \"https://apis.schoolsoutapp.com/api/admin/family-bookings/0b6a1f9e-3c2d-4e5f-8a7b-9c0d1e2f3a4b/follow-up\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"resolution\": \"refunded\", \"note\": \"Refunded $425.00 in the Stripe dashboard, re_3QxExample.\"}'"}]}},"/api/admin/family-bookings/charges/{kind}/{chargeId}/follow-up":{"patch":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Resolve an unresolved saved-card charge","description":"For a staff or ADMIN person on their own account (an ADMIN machine key, a service call or a preview gets 403, because the resolution names the person): records how staff closed a wallet top-up or gift card purchase listed under `charges` on the follow-up list, after looking it up in Stripe by its `stripeIdempotencyKey`, and takes it off the list. `refunded`: Stripe charged the card and staff refunded it there. `written_off`: staff closed it without moving money. `resolved_in_stripe`: staff settled it in Stripe by hand (for example Stripe never charged the card, or the PaymentIntent was cancelled there). The route moves no money and posts nothing: an unresolved charge never credited the wallet or activated a gift card, and stays unresolved for the parent (its idempotency key keeps answering 409). The body has the booking follow-up's shape.","parameters":[{"name":"kind","in":"path","required":true,"schema":{"type":"string","enum":["top_up","gift_card"]},"description":"The charge's `kind` from the follow-up list."},{"name":"chargeId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The charge's `id` from the follow-up list."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["resolution","note"],"properties":{"resolution":{"type":"string","enum":["refunded","written_off","resolved_in_stripe"]},"note":{"type":"string","minLength":1,"maxLength":1000,"description":"What staff found and did in Stripe, kept on the charge. Surrounding whitespace is trimmed."}}},"example":{"resolution":"refunded","note":"Charged once (pi_3QxExample); refunded in the Stripe dashboard."}}}},"responses":{"200":{"description":"The recorded resolution.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["resolved"],"properties":{"resolved":{"$ref":"#/components/schemas/FamilyStaffChargeResolution"}}}}},"example":{"success":true,"data":{"resolved":{"kind":"top_up","id":"5d1c2b3a-4e5f-4a6b-8c7d-9e0f1a2b3c4d","resolution":"refunded","note":"Charged once (pi_3QxExample); refunded in the Stripe dashboard."}}}}}},"400":{"description":"Unknown property (amountCents included), a resolution other than refunded, written_off or resolved_in_stripe, or a note that is empty or longer than 1000 characters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"resolution must be refunded, written_off or resolved_in_stripe, without amountCents.","data":null}}}},"401":{"description":"Sign-in required."},"403":{"description":"The viewer is not ADMIN or staff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"An ADMIN or staff user is required.","data":null}}}},"404":{"description":"No unresolved charge of that kind and id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Charge not found.","data":null}}}},"409":{"description":"Staff already resolved this charge.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Staff already resolved this charge.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"ADMIN or staff accounts only (cookie or account credential). Cookie mutations require CSRF.","x-curl-examples":[{"title":"Resolve an unresolved saved-card charge","code":"curl -X PATCH \"https://apis.schoolsoutapp.com/api/admin/family-bookings/charges/top_up/5d1c2b3a-4e5f-4a6b-8c7d-9e0f1a2b3c4d/follow-up\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"resolution\": \"refunded\", \"note\": \"Charged once (pi_3QxExample); refunded in the Stripe dashboard.\"}'"}]}},"/api/users/viewer/wallet/top-ups":{"post":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Add credits","description":"Charges a saved card now and adds the amount to the wallet once the payment succeeds. Nothing is added for a payment that needs authentication or is declined (`402`) or whose outcome is unknown (`503`; resend the same request).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyWalletTopUpInput"}}}},"responses":{"200":{"description":"The same top-up, already completed (a replay).","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["wallet","entry"],"properties":{"wallet":{"$ref":"#/components/schemas/FamilyWallet"},"entry":{"$ref":"#/components/schemas/FamilyWalletEntry"}}}}}}}},"201":{"description":"The top-up succeeded and its credits are in the wallet.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["wallet","entry"],"properties":{"wallet":{"$ref":"#/components/schemas/FamilyWallet"},"entry":{"$ref":"#/components/schemas/FamilyWalletEntry"}}}}},"example":{"success":true,"data":{"wallet":{"balanceCents":2500,"currency":"USD"},"entry":{"id":"1042","kind":"top_up","label":"Credits added","detail":null,"amountCents":2500,"method":{"brand":"visa","last4":"4242"},"bookingId":null,"occurredAt":"2026-10-16T09:00:00Z"}}}}}},"400":{"description":"Unknown property, or an invalid key, amount or card choice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"amountCents must be between 1000 and 50000.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"402":{"description":"The card needs the parent (`requiresAction.clientSecret`, confirm it with Stripe.js, then resend the same request) or was declined (`declined.code`).","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/BookingError"},{"type":"object","properties":{"requiresAction":{"type":"object","required":["clientSecret"],"properties":{"clientSecret":{"type":"string"}}},"declined":{"type":"object","required":["code"],"properties":{"code":{"type":"string"}}}}}]},"example":{"error":true,"message":"Your card was declined. Try another card.","data":null,"declined":{"code":"insufficient_funds"}}}}},"409":{"description":"No such saved card, or the key was reused with different details. A charge whose PaymentIntent Stripe never named within its idempotency window is closed as unresolved for staff, and its key answers 409 from then on: use a new idempotencyKey.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Add a card first.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"The payment outcome is unknown; resend the same request. Also on a server without a Stripe key (`STRIPE_SECRET_KEY`), before anything is read or recorded: “Payments aren't configured on this server.”","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"The payment could not be confirmed. Send the same request again.","data":null}}}},"403":{"description":"Only the family's owner manages cards, top-ups and gift cards (manage_cards)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Add credits","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/wallet/top-ups\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"idempotencyKey\": \"6c5b4a39-2817-4f6e-9d5c-4b3a29180f7e\", \"amountCents\": 2500, \"paymentMethod\": \"default\"}'"}]}},"/api/users/viewer/payment-method/{paymentMethodId}":{"delete":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Remove a card","description":"Removes a saved card and detaches it in Stripe: the default (the backup is promoted), the backup, or any other card in the response's `cards`, which leaves both slots as they are. A booking whose hold is on the card keeps its money held: the hold first moves to the parent's remaining card (the other slot, or the default for a card in neither; a new hold of the same amount there, then the old one is cancelled), and the booking's events record the move. Without another card, or while a hold on the card is still being placed, the card cannot be removed. When Stripe does not confirm the detach at once the card is still removed, and School's Out finishes the detach in the background, so a removed card never stays attached.","parameters":[{"name":"paymentMethodId","in":"path","required":true,"schema":{"type":"string","pattern":"^pm_"}}],"responses":{"200":{"description":"The saved cards after the change.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/FamilyPaymentMethods"}}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"description":"Not one of the parent’s saved cards.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Card not found.","data":null}}}},"409":{"description":"A booking's hold is on the card and there is no other card to move it to, the other card declined the moved hold (the card stays), or a hold on the card is still being placed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"A booking is holding money on this card. Add another card before removing it.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"Stripe's answer to moving a booking's hold was unknown, or Stripe could not be asked whether a card outside the slots is the parent's; the card stays. Send the same request again. Also on a server without a Stripe key (`STRIPE_SECRET_KEY`), before anything is read or recorded: “Payments aren't configured on this server.”","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"The card could not be removed. Try again.","data":null}}}},"403":{"description":"Only the family's owner manages cards, top-ups and gift cards (manage_cards)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Remove a card","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/payment-method/$PAYMENT_METHOD_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/gift-cards":{"get":{"tags":["Internal family bookings"],"x-internal":true,"summary":"List gift cards you bought","description":"The viewer's purchased gift cards, newest first (at most 100). Purchases whose payment did not succeed are not listed. Lists show only each code's last four characters; GET /api/users/viewer/gift-cards/{giftCardId}/code shows a code.","responses":{"200":{"description":"The viewer's gift cards.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["giftCards"],"properties":{"giftCards":{"type":"array","items":{"$ref":"#/components/schemas/FamilyGiftCard"}}}}}},"example":{"success":true,"data":{"giftCards":[{"id":"5b0f8d2e-3c41-4b8a-9e6f-2d7a1c9b4e30","amountCents":5000,"currency":"USD","status":"active","codeLast4":"Q7XM","recipientName":"Jordan","recipientEmail":"jordan@example.com","emailStatus":"pending","message":"Happy summer!","card":{"brand":"visa","last4":"4242"},"purchasedAt":"2026-10-16T17:04:11Z","redeemedAt":null}]}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"Only the family's owner manages cards, top-ups and gift cards (manage_cards)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"List gift cards","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/gift-cards\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"post":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Buy a gift card","description":"Charges the viewer's saved card and, once the payment succeeds, returns the gift card with its code. The code is stored sealed, so a replay of the same request, or GET /api/users/viewer/gift-cards/{giftCardId}/code, shows it again, even after it is redeemed. With `recipientEmail`, School's Out also emails the code to the recipient in the background (see `emailStatus`); otherwise the purchaser shares it. Gift cards do not expire. Without the stored-secrets key the API refuses every purchase with 503 before any charge.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyGiftCardPurchaseInput"},"example":{"idempotencyKey":"0d6f1c9a-2b7e-4f3d-8a1c-5e9b7d2f4a60","amountCents":5000,"recipientName":"Jordan","recipientEmail":"jordan@example.com","message":"Happy summer!","paymentMethod":"default"}}}},"responses":{"201":{"description":"Paid. The response carries the code.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/FamilyGiftCardIssued"}}},"example":{"success":true,"data":{"giftCard":{"id":"5b0f8d2e-3c41-4b8a-9e6f-2d7a1c9b4e30","amountCents":5000,"currency":"USD","status":"active","codeLast4":"Q7XM","recipientName":"Jordan","recipientEmail":"jordan@example.com","emailStatus":"pending","message":"Happy summer!","card":{"brand":"visa","last4":"4242"},"purchasedAt":"2026-10-16T17:04:11Z","redeemedAt":null},"code":"K7MD-3QPA-WX9H-Q7XM"}}}}},"200":{"description":"A replay of a key whose card is already sold (active or redeemed): the same card and code, without a new charge.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/FamilyGiftCardIssued"}}},"example":{"success":true,"data":{"giftCard":{"id":"5b0f8d2e-3c41-4b8a-9e6f-2d7a1c9b4e30","amountCents":5000,"currency":"USD","status":"active","codeLast4":"Q7XM","recipientName":"Jordan","recipientEmail":"jordan@example.com","emailStatus":"pending","message":"Happy summer!","card":{"brand":"visa","last4":"4242"},"purchasedAt":"2026-10-16T17:04:11Z","redeemedAt":null},"code":"K7MD-3QPA-WX9H-Q7XM"}}}}},"400":{"description":"The body isn't a valid purchase: unknown property, bad key, amount out of bounds, too-long text or a bad email.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"amountCents must be between 1000 and 50000.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"402":{"description":"The card needs the parent's confirmation (`requiresAction.clientSecret`: confirm it with Stripe.js, then replay the same request) or was declined (`declined.code`, Stripe's decline code; use a new idempotencyKey to try another card). Same shape as wallet top-ups.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/BookingError"},{"type":"object","oneOf":[{"type":"object","required":["requiresAction"],"properties":{"requiresAction":{"type":"object","additionalProperties":false,"required":["clientSecret"],"properties":{"clientSecret":{"type":"string"}}}}},{"type":"object","required":["declined"],"properties":{"declined":{"type":"object","additionalProperties":false,"required":["code"],"properties":{"code":{"type":"string"}}}}}]}]},"examples":{"requiresAction":{"value":{"error":true,"message":"Your bank needs you to confirm this payment.","data":null,"requiresAction":{"clientSecret":"pi_3Q..._secret_..."}}},"declined":{"value":{"error":true,"message":"Your card was declined. Try another card.","data":null,"declined":{"code":"insufficient_funds"}}}}}}},"409":{"description":"The chosen saved card slot is empty, the key was used with different details, or another request with this key is completing it. A charge whose PaymentIntent Stripe never named within its idempotency window is closed as unresolved for staff, and its key answers 409 from then on: use a new idempotencyKey.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"This purchase was already sent with different details. Use a new idempotencyKey.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"Stripe's answer is unknown (nothing is charged twice; retry with the same key), or gift cards are unavailable because the API runs without its stored-secrets key (nothing was charged). Also on a server without a Stripe key (`STRIPE_SECRET_KEY`), before anything is read or recorded: “Payments aren't configured on this server.”","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"The payment could not be confirmed. Send the same request again.","data":null}}}},"403":{"description":"Only the family's owner manages cards, top-ups and gift cards (manage_cards)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Buy a $50 gift card","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/gift-cards\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"idempotencyKey\":\"0d6f1c9a-2b7e-4f3d-8a1c-5e9b7d2f4a60\",\"amountCents\":5000,\"recipientName\":\"Jordan\"}'"}]}},"/api/users/viewer/gift-cards/{giftCardId}/code":{"get":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Show a gift card's code","description":"Shows the code of a gift card the viewer bought, active or redeemed, so the purchaser can share it again. Only the purchaser sees it: another account's card, or a purchase that was never paid, is not found. The answer is never cached (Cache-Control: private, no-store).","parameters":[{"name":"giftCardId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The gift card and its code.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/FamilyGiftCardIssued"}}},"example":{"success":true,"data":{"giftCard":{"id":"5b0f8d2e-3c41-4b8a-9e6f-2d7a1c9b4e30","amountCents":5000,"currency":"USD","status":"active","codeLast4":"Q7XM","recipientName":"Jordan","recipientEmail":"jordan@example.com","emailStatus":"pending","message":"Happy summer!","card":{"brand":"visa","last4":"4242"},"purchasedAt":"2026-10-16T17:04:11Z","redeemedAt":null},"code":"K7MD-3QPA-WX9H-Q7XM"}}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"description":"No gift card of the viewer's with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Gift card not found.","data":null}}}},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"Gift card codes are unavailable: the API runs without its stored-secrets key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Gift cards are unavailable right now. Try again later.","data":null}}}},"403":{"description":"Only the family's owner manages cards, top-ups and gift cards (manage_cards)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Show a gift card's code","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/gift-cards/5b0f8d2e-3c41-4b8a-9e6f-2d7a1c9b4e30/code\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/wallet/redeem":{"post":{"tags":["Internal family bookings"],"x-internal":true,"summary":"Redeem a gift card","description":"Adds a gift card's value to the viewer's wallet. A code can be redeemed once, by any account. Unknown codes count against a per-account limit; past it the route answers 429 until the window ends.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyGiftCardRedeemInput"},"example":{"code":"k7md-3qpa-wx9h-q7xm"}}}},"responses":{"201":{"description":"Redeemed. The wallet's new balance and the credit entry.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["wallet","entry"],"properties":{"wallet":{"$ref":"#/components/schemas/FamilyWallet"},"entry":{"$ref":"#/components/schemas/FamilyWalletEntry"}}}}},"example":{"success":true,"data":{"wallet":{"balanceCents":5000,"currency":"USD"},"entry":{"id":"9182","kind":"gift_card","label":"Gift card redeemed","detail":null,"amountCents":5000,"method":null,"bookingId":null,"occurredAt":"2026-10-16T18:20:03Z"}}}}}},"400":{"description":"The body isn't exactly `{ code }`, or the code is not 16 characters of the gift card alphabet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"Enter the 16-character code from the gift card email.","data":null}}}},"401":{"description":"Current account required. A request without a session or credential answers {\"authorized\": false}; a machine key or delegated service request answers the error envelope with message \"unauthorized.\", because these routes act only as the signed-in parent."},"404":{"description":"No gift card has this code. Counts against the limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"That gift card code was not recognized.","data":null}}}},"409":{"description":"The gift card was already redeemed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingError"},"example":{"error":true,"message":"This gift card was already redeemed.","data":null}}}},"429":{"description":"Too many unrecognized codes from this account. `Retry-After` gives the seconds to wait. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/BookingError"},{"type":"object","required":["retryAfterMs"],"properties":{"retryAfterMs":{"type":"integer","minimum":0}}}]},"example":{"error":true,"message":"Too many gift card codes were not recognized. Try again later.","data":null,"retryAfterMs":1800000}}}},"403":{"description":"Only the family's owner manages cards, top-ups and gift cards (manage_cards)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Redeem a gift card","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/wallet/redeem\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"code\":\"K7MD-3QPA-WX9H-Q7XM\"}'"}]}},"/api/users/viewer/feed":{"get":{"tags":["Internal family feed"],"x-internal":true,"summary":"Read the family feed","description":"Returns the current edition's day: edition is the current edition, and editions is the day's morning edition then its evening edition, both published on the day's first read, so the evening is out before it becomes current. It is the only day a family can read: an earlier one is replaced, not kept, and no other day is ever generated. The route takes no query parameters. Post bodies are paragraphs of runs; a camp run only ever names the post's own camp. note is never null: the adult's daily note, as GET /api/users/viewer/feed/note answers it.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"$ref":"#/components/schemas/FamilyFeed"}}},"example":{"success":true,"data":{"edition":{"id":"7c1e2d3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f","date":"2026-10-19","slot":"morning","label":"Monday morning","publishedAt":"2026-10-19T14:00:03Z","posts":[{"id":"0d9c8b7a-6f5e-4d3c-2b1a-0f9e8d7c6b5a","kind":"camp","topic":"science","headline":"Registration is open for Junior Explorers","body":[[{"camp":{"organizationId":"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34","name":"Riverside Camps","logoUrl":null}},{"text":" opened registration. For ages 6 to 12. Next session: Mon, Oct 26 to Fri, Oct 30. About 2 miles away."}]],"image":{"url":"https://riverside.example/explorers.jpg","alt":"Junior Explorers","position":null},"until":"2026-10-26","reason":"Registration opened at Riverside Camps, which you follow.","signal":null,"organizationId":"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34","activityId":"c3a9e1f2-6d4b-4a8c-b2e7-0f5d9c1a3b48"}]},"editions":[{"id":"7c1e2d3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f","date":"2026-10-19","slot":"morning","label":"Monday morning","publishedAt":"2026-10-19T14:00:03Z","posts":[{"id":"0d9c8b7a-6f5e-4d3c-2b1a-0f9e8d7c6b5a","kind":"camp","topic":"science","headline":"Registration is open for Junior Explorers","body":[[{"camp":{"organizationId":"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34","name":"Riverside Camps","logoUrl":null}},{"text":" opened registration. For ages 6 to 12. Next session: Mon, Oct 26 to Fri, Oct 30. About 2 miles away."}]],"image":{"url":"https://riverside.example/explorers.jpg","alt":"Junior Explorers","position":null},"until":"2026-10-26","reason":"Registration opened at Riverside Camps, which you follow.","signal":null,"organizationId":"9e4f7a21-3b6c-4d8e-a1f5-2c7b0d9e6f34","activityId":"c3a9e1f2-6d4b-4a8c-b2e7-0f5d9c1a3b48"}]},{"id":"5b2a1c0d-9e8f-4a7b-8c6d-3e2f1a0b9c8d","date":"2026-10-19","slot":"evening","label":"Monday evening","publishedAt":"2026-10-19T14:00:05Z","posts":[]}],"note":{"date":"2026-10-19","id":"quote-001","type":"quote","greeting":"Good morning","text":"Why, sometimes I’ve believed as many as six impossible things before breakfast.","credit":"Lewis Carroll, Through the Looking-Glass","doodle":"library","pose":{"time":"morning","season":"autumn"},"seenAt":null,"savedAt":null}}}}}},"400":{"description":"date and slot do not name an edition"},"401":{"description":"Current account required"},"404":{"description":"No edition was published for that date and slot"},"409":{"description":"The household has no area to measure from"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"The feed took longer than its request deadline; nothing was published. Retry after a short delay."}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Read the current edition","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/feed\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/feed/camp-views":{"post":{"tags":["Internal family feed"],"x-internal":true,"summary":"Record time on a camp page","description":"Records that the parent spent dwellSeconds on an organization's, activity's or session's page: evidence of interest the feed learns from.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"targetKind":{"type":"string","enum":["organization","activity","session"]},"targetId":{"type":"string","format":"uuid"},"dwellSeconds":{"type":"integer","minimum":0,"maximum":86400}},"required":["targetKind","targetId","dwellSeconds"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"recorded":{"const":true,"type":"boolean"}},"required":["recorded"]}}}}}},"400":{"description":"Invalid target or dwell time"},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"The feed took longer than its request deadline; nothing was published. Retry after a short delay."}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Record a look at an activity","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/feed/camp-views\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"targetKind\":\"activity\",\"targetId\":\"c3a9e1f2-6d4b-4a8c-b2e7-0f5d9c1a3b48\",\"dwellSeconds\":42}'"}]}},"/api/users/viewer/feed/instructions":{"get":{"tags":["Internal family feed"],"x-internal":true,"summary":"Read your feed instructions","description":"The signed-in adult's own feed instructions (null while they read the default feed) and the feed's default wording for them. Every role reads their own; nobody reads another adult's.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"required":["feedInstructions"],"properties":{"feedInstructions":{"$ref":"#/components/schemas/FamilyFeedInstructions"}}}}}}}},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"The feed took longer than its request deadline; nothing was published. Retry after a short delay."}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Read your feed instructions","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/feed/instructions\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"put":{"tags":["Internal family feed"],"x-internal":true,"summary":"Save your feed instructions","description":"Replaces the signed-in adult's own feed instructions, or with null removes them (Reset to default). Every role writes their own; no household version is needed, and no other adult's feed changes. The next edition follows them.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"required":["feedInstructions"],"properties":{"feedInstructions":{"$ref":"#/components/schemas/FamilyFeedInstructions"}}}}}}}},"400":{"description":"Not {instructions: string | null}, or the text is empty after trimming or longer than 4000 characters"},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"The feed took longer than its request deadline; nothing was published. Retry after a short delay."}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Save your feed instructions","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/feed/instructions\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"instructions\": \"Soccer news and weather that changes plans, please.\"}'"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FamilyFeedInstructionsInput"}}}}}},"/api/users/viewer/feed/note":{"get":{"tags":["Internal family feed"],"x-internal":true,"summary":"Read the daily note","description":"Returns the adult's daily note for today in the family's time zone, picking it on the date's first read: a line staff wrote for the family's area and date, else a line from the pool of credited quotes (books first published before 1930) and true facts. The pool is gated by the part of the day, the season's dates (event windows such as Halloween or a clock change first), and the day's weather at home; a line is not shown again within 45 days; quotes and facts are drawn by equal weight; an adult with no children set up gets a welcome line; when nothing fits, the untagged line shown longest ago. Unlike the feed it needs no home area. The route takes no query parameters.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"note":{"$ref":"#/components/schemas/FamilyFeedNote"}},"required":["note"]}}},"example":{"success":true,"data":{"note":{"date":"2026-10-19","id":"quote-001","type":"quote","greeting":"Good morning","text":"Why, sometimes I’ve believed as many as six impossible things before breakfast.","credit":"Lewis Carroll, Through the Looking-Glass","doodle":"library","pose":{"time":"morning","season":"autumn"},"seenAt":null,"savedAt":null}}}}}},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"The feed took longer than its request deadline; nothing was published. Retry after a short delay."}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Read the daily note","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/feed/note\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/feed/notes/{date}/seen":{"put":{"tags":["Internal family feed"],"x-internal":true,"summary":"Mark the daily note seen","description":"Records that the adult saw their daily note for date, so its first-view mark draws once per account. Any date the adult was given a note for can be marked. Idempotent: the first time is kept.","parameters":[{"name":"date","in":"path","required":true,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"seenAt":{"type":"string","format":"date-time"}},"required":["seenAt"]}}}}}},"400":{"description":"date is not YYYY-MM-DD"},"401":{"description":"Current account required"},"404":{"description":"The adult was given no note on that date"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"The feed took longer than its request deadline; nothing was published. Retry after a short delay."}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Mark a note seen","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/feed/notes/2026-10-19/seen\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/feed/notes/{date}/saved":{"put":{"tags":["Internal family feed"],"x-internal":true,"summary":"Save the daily note to Notes","description":"Saves the adult's line for date to their Notes, which Collections lists under Notes. Idempotent: saving again keeps the first time.","parameters":[{"name":"date","in":"path","required":true,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"date":{"type":"string","format":"date"},"savedAt":{"type":["string","null"],"format":"date-time"}},"required":["date","savedAt"]}}},"example":{"success":true,"data":{"date":"2026-10-19","savedAt":"2026-10-19T15:04:05Z"}}}}},"400":{"description":"date is not YYYY-MM-DD"},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"The feed took longer than its request deadline; nothing was published. Retry after a short delay."},"404":{"description":"The adult was given no note on that date"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Save the day's note","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/feed/notes/2026-10-19/saved\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"delete":{"tags":["Internal family feed"],"x-internal":true,"summary":"Remove the daily note from Notes","description":"Removes the adult's line for date from their Notes. Idempotent: removing an unsaved line answers savedAt null.","parameters":[{"name":"date","in":"path","required":true,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"date":{"type":"string","format":"date"},"savedAt":{"type":["string","null"],"format":"date-time"}},"required":["date","savedAt"]}}},"example":{"success":true,"data":{"date":"2026-10-19","savedAt":null}}}}},"400":{"description":"date is not YYYY-MM-DD"},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"The feed took longer than its request deadline; nothing was published. Retry after a short delay."},"404":{"description":"The adult was given no note on that date"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Remove a saved note","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/feed/notes/2026-10-19/saved\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/feed/notes/saved":{"get":{"tags":["Internal family feed"],"x-internal":true,"summary":"List saved notes","description":"Returns the adult's Notes, the lines they saved, the most recently saved first, at most 200. The route takes no query parameters.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"notes":{"type":"array","items":{"$ref":"#/components/schemas/FamilyFeedSavedNote"}}},"required":["notes"]}}},"example":{"success":true,"data":{"notes":[{"date":"2026-10-19","id":"quote-001","type":"quote","text":"Why, sometimes I’ve believed as many as six impossible things before breakfast.","credit":"Lewis Carroll, Through the Looking-Glass","doodle":"library","savedAt":"2026-10-19T15:04:05Z"}]}}}}},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"The feed took longer than its request deadline; nothing was published. Retry after a short delay."}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"List saved notes","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/feed/notes/saved\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/feed/posts/{postId}/signal":{"put":{"tags":["Internal family feed"],"x-internal":true,"summary":"Set a post's signal","description":"Sets, changes or (with null) clears the one signal a post carries; setting the current signal again or clearing a post without one succeeds unchanged, and only a post that is not the viewer's is 404. Like and dislike teach the feed about the post's camp and interests; dont_show_again resets its interests as of that moment.","parameters":[{"name":"postId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"signal":{"type":["string","null"],"enum":["like","dislike","dont_show_again",null]}},"required":["signal"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"postId":{"type":"string","format":"uuid"},"signal":{"type":["string","null"],"enum":["like","dislike","dont_show_again",null]}},"required":["postId","signal"]}}}}}},"400":{"description":"Invalid signal"},"401":{"description":"Current account required"},"404":{"description":"Post not found for this account"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"The feed took longer than its request deadline; nothing was published. Retry after a short delay."}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Like a post","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/feed/posts/0d9c8b7a-6f5e-4d3c-2b1a-0f9e8d7c6b5a/signal\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"signal\":\"like\"}'"}]}},"/api/users/viewer/feed/posts/{postId}/done":{"put":{"tags":["Internal family feed"],"x-internal":true,"summary":"Mark a post done","description":"Marks a post done or not done in the viewer's To do; the feed's posts carry it as done. Marking a post as it already is succeeds unchanged, a post marked done again keeps its first time, and only a post that is not the viewer's is 404.","parameters":[{"name":"postId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"done":{"type":"boolean"}},"required":["done"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"postId":{"type":"string","format":"uuid"},"done":{"type":"boolean"}},"required":["postId","done"]}}}}}},"400":{"description":"done is missing or not a boolean"},"401":{"description":"Current account required"},"404":{"description":"Post not found for this account"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"503":{"description":"The feed took longer than its request deadline; nothing was published. Retry after a short delay."}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Mark a post done","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/feed/posts/0d9c8b7a-6f5e-4d3c-2b1a-0f9e8d7c6b5a/done\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"done\":true}'"}]}},"/api/users/viewer/notification-preferences":{"get":{"tags":["Internal family notifications"],"x-internal":true,"summary":"Read notification preferences","description":"Returns the family's email, push and SMS choices per row (bookings, waitlist, schedule, groups, friends, assistant), whether payment receipts are emailed (`receipts`, email only), its quiet hours and its product news switch. A family that never saved gets the defaults at version 0, and a saved record without a row or setting reads it at its default; the defaults are policy and may change.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"preferences":{"$ref":"#/components/schemas/FamilyNotificationPreferences"}},"required":["preferences"]}}}}}},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Read notification preferences","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/notification-preferences\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"put":{"tags":["Internal family notifications"],"x-internal":true,"summary":"Save notification preferences","description":"Replaces the family's preferences. Every channel of bookings, waitlist and schedule must be present; `groups`, `friends`, `assistant` and `receipts` may be left out, as may `quietHours` and `productNews`, each keeping its saved value, so a client that predates them never changes them. Any other key is 400. Send the current version (0 before the first save); a stale version returns 409. Turning SMS on for a row where the saved matrix has it off needs a verified phone: otherwise 409 with code phone-unverified. SMS already on is kept without one, and no text is sent until a phone is verified.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"version":{"type":"integer","minimum":0},"preferences":{"$ref":"#/components/schemas/FamilyNotificationPreferenceValues"}},"required":["version","preferences"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"preferences":{"$ref":"#/components/schemas/FamilyNotificationPreferences"}},"required":["preferences"]}}}}}},"400":{"description":"Invalid or incomplete preferences"},"401":{"description":"Current account required"},"409":{"description":"Stale version, or SMS turned on without a verified phone (code phone-unverified)"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Save preferences","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/notification-preferences\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"version\":0,\"preferences\":{\"channels\":{\"bookings\":{\"email\":true,\"push\":true,\"sms\":false},\"waitlist\":{\"email\":true,\"push\":true,\"sms\":false},\"schedule\":{\"email\":false,\"push\":true,\"sms\":false},\"groups\":{\"email\":false,\"push\":true,\"sms\":false},\"friends\":{\"email\":false,\"push\":true,\"sms\":false},\"assistant\":{\"email\":false,\"push\":true,\"sms\":false}},\"quietHours\":{\"on\":true,\"from\":21,\"to\":7},\"productNews\":false}}'"}]}},"/api/users/viewer/notifications":{"get":{"tags":["Internal family notifications"],"x-internal":true,"summary":"List notifications","description":"Returns the family's notifications newest first with its unread count. filter=bookings keeps booking and waitlist notifications; filter=unread keeps unread ones. The badge reads unreadCount with limit=1. Pass nextCursor back as cursor, with the same filter, for the next page.","parameters":[{"name":"filter","in":"query","required":false,"description":"Which notifications; all by default.","schema":{"type":"string","enum":["all","bookings","unread"]}},{"name":"limit","in":"query","required":false,"description":"Page size, 50 by default.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"nextCursor from the previous page of the same filter.","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"notifications":{"type":"array","items":{"$ref":"#/components/schemas/FamilyNotification"}},"unreadCount":{"type":"integer","minimum":0},"nextCursor":{"type":["string","null"]}},"required":["notifications","unreadCount","nextCursor"]}}}}}},"400":{"description":"Invalid filter, limit or cursor"},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"List unread notifications","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/notifications?filter=unread\u0026limit=20\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/notifications/read":{"post":{"tags":["Internal family notifications"],"x-internal":true,"summary":"Mark notifications read","description":"Marks the named notifications read, or with {all: true, before} every unread notification created at or before that moment, and returns the unread count after. Ids that are not the family's own change nothing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","additionalProperties":false,"properties":{"ids":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","format":"uuid"}}},"required":["ids"]},{"type":"object","additionalProperties":false,"properties":{"all":{"const":true,"type":"boolean"},"before":{"type":"string","format":"date-time"}},"required":["all","before"]}]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"unreadCount":{"type":"integer","minimum":0}},"required":["unreadCount"]}}}}}},"400":{"description":"Neither or both forms, or more than 100 ids"},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Mark everything read","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/notifications/read\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"all\":true,\"before\":\"2026-10-19T15:04:05Z\"}'"}]}},"/api/users/viewer/push-subscriptions":{"put":{"tags":["Internal family notifications"],"x-internal":true,"summary":"Register a browser or app for push","description":"Subscribes the browser or native app of the caller's sign-in session to the family's push notifications. A browser sends PushSubscription.toJSON() (expirationTime is accepted and not kept); the app sends {kind: \"expo\", token, platform, deviceName?} with its Expo push token. A sign-in session has one browser subscription and one app token: subscribing again replaces it, keeping its id, and registering the same token again only refreshes it. An endpoint or token another session registered, of this family or another, moves to this one. Signing a session out (or revoking it) deletes its subscriptions. Push is sent only for types whose push preference is on. An app's notification carries data {notificationId, target} ({notificationId: uuid, target}), target being the notification's target as the inbox returns it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/FamilyPushSubscriptionInput"},{"$ref":"#/components/schemas/FamilyExpoPushSubscriptionInput"}]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"subscription":{"oneOf":[{"$ref":"#/components/schemas/FamilyPushSubscription"},{"$ref":"#/components/schemas/FamilyExpoPushSubscription"}]}},"required":["subscription"]}}}}}},"400":{"description":"Invalid endpoint, keys, token or platform"},"401":{"description":"Current account required, or its sign-in session has ended"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Register a browser","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/push-subscriptions\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"endpoint\":\"https://fcm.googleapis.com/fcm/send/example\",\"keys\":{\"p256dh\":\"BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4\",\"auth\":\"BTBZMqHH6r4Tts7J_aSIgg\"}}'"},{"title":"Register the app","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/push-subscriptions\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"X-Session-Id: $SCHOOLS_OUT_SESSION_ID\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"kind\":\"expo\",\"token\":\"ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]\",\"platform\":\"ios\",\"deviceName\":\"iPhone\"}'"}]},"delete":{"tags":["Internal family notifications"],"x-internal":true,"summary":"Unregister a browser or app from push","description":"Removes the family's own subscription at the endpoint, as the browser does when the parent turns push off there, or the app's own Expo push token ({token}), as the app does when push is turned off or before signing out (signing out also deletes it). An endpoint or token the family has not registered, including one another family holds, returns 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","additionalProperties":false,"properties":{"endpoint":{"type":"string","format":"uri","maxLength":2048,"description":"The browser's push service URL (https, at a known browser push service)."}},"required":["endpoint"]},{"type":"object","additionalProperties":false,"properties":{"token":{"type":"string","description":"The app's Expo push token."}},"required":["token"]}]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"deleted":{"const":true,"type":"boolean"}},"required":["deleted"]}}}}}},"400":{"description":"Neither one endpoint nor one token"},"401":{"description":"Current account required"},"404":{"description":"The family has no subscription at that endpoint or with that token"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Unregister a browser","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/push-subscriptions\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"endpoint\":\"https://fcm.googleapis.com/fcm/send/example\"}'"},{"title":"Unregister the app","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/push-subscriptions\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"X-Session-Id: $SCHOOLS_OUT_SESSION_ID\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"token\":\"ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]\"}'"}]}},"/api/users/viewer/push-subscriptions/vapid-key":{"get":{"tags":["Internal family notifications"],"x-internal":true,"summary":"Read the push public key","description":"Returns the VAPID public key (base64url, uncompressed P-256) to pass as applicationServerKey to PushManager.subscribe(). null when this deployment sends no push; the app then offers no push.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","additionalProperties":false,"properties":{"publicKey":{"type":["string","null"],"description":"base64url uncompressed P-256 public key; null when push is off."}},"required":["publicKey"]}}}}}},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Records are private to the signed-in adult.","x-curl-examples":[{"title":"Read the push public key","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/push-subscriptions/vapid-key\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/children/{childId}/photo":{"get":{"tags":["Internal family profiles"],"x-internal":true,"summary":"Read a child's photo","description":"The JPEG, to the child's owner and guardians only. With `v` naming the current photo it may be cached privately until it changes; never in a shared cache.","parameters":[{"name":"childId","in":"path","required":true,"schema":{"format":"uuid","type":"string"}},{"name":"v","in":"query","required":false,"schema":{"type":"string"},"description":"The photo's hash, from photoUrl."}],"responses":{"200":{"description":"The photo.","content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Current account required."},"404":{"description":"Child not found for this account, or no photo."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Child profiles are visible to their owner and accepted active guardians; unrelated accounts receive 404.","x-curl-examples":[{"title":"Read a child's photo","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/photo\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"security":[{"AccountCredential":[]}]},"put":{"tags":["Internal family profiles"],"x-internal":true,"summary":"Upload a child's photo","description":"Body is the image (image/jpeg or image/png, at most 2 MiB and 4096 px a side), through the parent photo's pipeline: cropped to a square and stored as a 512×512 JPEG without metadata. Owners and guardians may set it. Only the child's family reads it.","requestBody":{"required":true,"content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["photoUrl"],"properties":{"photoUrl":{"type":"string"}}}}}}}},"401":{"description":"Current account required."},"413":{"description":"Larger than 2 MiB."},"415":{"description":"Not image/jpeg or image/png."},"422":{"description":"Cannot be decoded, or too large in pixels."},"404":{"description":"Child not found for this account."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not add or edit children (edit_children; a caregiver)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Child profiles are visible to their owner and accepted active guardians; unrelated accounts receive 404.","x-curl-examples":[{"title":"Upload a child's photo","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/photo\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: image/jpeg\" --data-binary @photo.jpg"}],"parameters":[{"name":"childId","in":"path","required":true,"schema":{"format":"uuid","type":"string"}}]},"delete":{"tags":["Internal family profiles"],"x-internal":true,"summary":"Remove a child's photo","description":"Removes the child's photo, if it has one.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["photoUrl"],"properties":{"photoUrl":{"type":"null"}}}}}}}},"401":{"description":"Current account required."},"404":{"description":"Child not found for this account."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"403":{"description":"The role may not add or edit children (edit_children; a caregiver)"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Child profiles are visible to their owner and accepted active guardians; unrelated accounts receive 404.","x-curl-examples":[{"title":"Remove a child's photo","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/photo\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"parameters":[{"name":"childId","in":"path","required":true,"schema":{"format":"uuid","type":"string"}}]}},"/api/users/viewer/email-change":{"post":{"tags":["Internal family account"],"x-internal":true,"summary":"Change your email","description":"Emails a link to the new address; the account keeps its email (profile.pendingEmail shows the new one) until that link is confirmed with POST /api/users/viewer/email-change/confirm. Asking again replaces the pending change and its link. The link opens redirect?code=\u003ccode\u003e when redirect is an absolute URL on an allow-listed origin (AUTH_ORIGIN, PARENT_SITE_ORIGIN, API_ASSISTANT_ORIGINS), else the Auth app's /email-change?code=\u003ccode\u003e. It expires in 30 minutes. Outside production, when no email is sent, data.devLink is the link.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["email"],"properties":{"email":{"format":"email","maxLength":254,"type":"string"},"redirect":{"description":"The client's page for the link, e.g. https://next.schoolsoutapp.com/consumer/email-change.","format":"uri","type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["pending"],"properties":{"pending":{"type":"object","additionalProperties":false,"required":["email","expiresAt"],"properties":{"email":{"format":"email","type":"string"},"expiresAt":{"format":"date-time","type":"string"}}},"devLink":{"description":"Development only.","type":"string"}}}}}}}},"400":{"description":"Not an email address, or already the account's email."},"401":{"description":"A signed-in device is required."},"409":{"description":"Another account holds this address."},"429":{"description":"Too many email changes; retry later."}},"security":[{"AccountCredential":[]}],"x-auth":"A signed-in device only: the account credential with its live session (cookie sid or X-Session-Id); a bare account key, machine keys and delegated service requests are rejected.","x-curl-examples":[{"title":"Change your email","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/email-change\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" -H \"X-Session-Id: $SESSION_ID\" \\\n  -H \"Content-Type: application/json\" -d '{\"email\":\"new@example.com\"}'"}]}},"/api/users/viewer/email-change/confirm":{"post":{"tags":["Internal family account"],"x-internal":true,"summary":"Confirm your new email","description":"Confirms the pending change with the code from its link, on the account that asked. In one transaction the account moves to the new address (verified), its key rotates, every session is signed out (revoked_reason email_change) and this device gets a new session. The answer is a sign-in's: {user, key, session_id}, so the client re-signs its cookie from it. The old address is told of the change.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["code"],"properties":{"code":{"format":"uuid","type":"string"}}}}}},"responses":{"200":{"description":"The new credential.","content":{"application/json":{"schema":{"type":"object","required":["user","key","session_id"],"properties":{"user":{"type":"object","description":"The users row without its key and password hash; email is the new address."},"key":{"type":"string"},"session_id":{"format":"uuid","type":"string"}}}}}},"401":{"description":"A signed-in device is required, or this device's key was rotated meanwhile."},"409":{"description":"Another account took this address meanwhile."},"410":{"description":"The link expired, was replaced by a newer change, or is another account's."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"A signed-in device only: the account credential with its live session (cookie sid or X-Session-Id); a bare account key, machine keys and delegated service requests are rejected.","x-curl-examples":[{"title":"Confirm your new email","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/email-change/confirm\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" -H \"X-Session-Id: $SESSION_ID\" \\\n  -H \"Content-Type: application/json\" -d '{\"code\":\"CODE\"}'"}]}},"/api/users/viewer/phone":{"delete":{"tags":["Internal family account"],"x-internal":true,"summary":"Remove your phone","description":"Clears the verified phone and the contact phone. SMS two-step login turns off with it, so while it is on this needs the same proof as turning it off. Text notifications stop: they go only to a verified phone. To replace the phone, verify the new one (POST /api/auth/phone/check), which answers the same proof while SMS two-step login is on.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"currentPassword":{"description":"The account's password, when it has one (profile.hasPassword).","type":"string"},"currentPhoneCode":{"description":"A code texted to the phone two-step login uses, by POST /api/users/viewer/two-step/sms/code.","type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["profile"],"properties":{"profile":{"$ref":"#/components/schemas/FamilyAccountViewerProfile"}}}}}}}},"400":{"description":"Proof is missing (code proof-required), or the password or texted code is wrong or expired (code proof-wrong). Asked only while SMS two-step login is on."},"401":{"description":"A signed-in device is required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"A signed-in device only: the account credential with its live session (cookie sid or X-Session-Id); a bare account key, machine keys and delegated service requests are rejected.","x-curl-examples":[{"title":"Remove your phone","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/phone\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" -H \"X-Session-Id: $SESSION_ID\" \\\n  -H \"Content-Type: application/json\" -d '{}'"}]}},"/api/users/viewer/two-step/sms":{"post":{"tags":["Internal family account"],"x-internal":true,"summary":"Turn on SMS two-step login","description":"Signing in then asks for a code texted to the account's verified phone. Already on is a success.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["profile"],"properties":{"profile":{"$ref":"#/components/schemas/FamilyAccountViewerProfile"}}}}}}}},"401":{"description":"A signed-in device is required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"409":{"description":"The account has no verified phone (code phone-unverified). Verify one with POST /api/auth/phone/start and /api/auth/phone/check first."}},"security":[{"AccountCredential":[]}],"x-auth":"A signed-in device only: the account credential with its live session (cookie sid or X-Session-Id); a bare account key, machine keys and delegated service requests are rejected.","x-curl-examples":[{"title":"Turn on SMS two-step login","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/two-step/sms\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" -H \"X-Session-Id: $SESSION_ID\" \\\n  -H \"Content-Type: application/json\" -d '{}'"}]},"delete":{"tags":["Internal family account"],"x-internal":true,"summary":"Turn off SMS two-step login","description":"Needs fresh proof that it is the parent: currentPassword or currentPhoneCode. The verified phone stays, and keeps getting the texts notifications send. Already off is a success and needs no proof.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"currentPassword":{"description":"The account's password, when it has one (profile.hasPassword).","type":"string"},"currentPhoneCode":{"description":"A code texted to the phone two-step login uses, by POST /api/users/viewer/two-step/sms/code.","type":"string"}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["profile"],"properties":{"profile":{"$ref":"#/components/schemas/FamilyAccountViewerProfile"}}}}}}}},"400":{"description":"Proof is missing (code proof-required), or the password or texted code is wrong or expired (code proof-wrong)."},"401":{"description":"A signed-in device is required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"A signed-in device only: the account credential with its live session (cookie sid or X-Session-Id); a bare account key, machine keys and delegated service requests are rejected.","x-curl-examples":[{"title":"Turn off SMS two-step login","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/two-step/sms\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" -H \"X-Session-Id: $SESSION_ID\" \\\n  -H \"Content-Type: application/json\" -d '{\"currentPassword\":\"...\"}'"}]}},"/api/users/viewer/two-step/sms/code":{"post":{"tags":["Internal family account"],"x-internal":true,"summary":"Text a code to confirm it’s you","description":"Texts a code to the phone SMS two-step login uses. Send it back as currentPhoneCode to turn SMS two-step login off, remove the phone, or verify a different phone. Outside production, when no text is sent, data.devCode is the code.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","additionalProperties":false,"required":["sent"],"properties":{"sent":{"const":true},"devCode":{"description":"Development only.","type":"string"}}}}}}}},"400":{"description":"SMS two-step login is off, so no change needs a texted code."},"401":{"description":"A signed-in device is required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"},"502":{"description":"The text could not be sent."}},"security":[{"AccountCredential":[]}],"x-auth":"A signed-in device only: the account credential with its live session (cookie sid or X-Session-Id); a bare account key, machine keys and delegated service requests are rejected.","x-curl-examples":[{"title":"Text a code to confirm it’s you","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/two-step/sms/code\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" -H \"X-Session-Id: $SESSION_ID\""}]}},"/api/users/viewer/family/members":{"get":{"summary":"List the family's members and invitations","description":"The family the viewer acts in: its owner, members (any adult of the family may read it) and, for its parents, its open and recently declined invitations. A caregiver sees names only. An adult in no other family is the owner of their own, possibly with no members.","tags":["Internal family account"],"x-internal":true,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"$ref":"#/components/schemas/FamilyMembers"}}}}}},"401":{"description":"Current account required"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The viewer acts in one family: the family they are a member of, else their own (FamilyAccess); each route requires an action of the role permission matrix and answers 403 with that action's refusal otherwise.","x-curl-examples":[{"title":"List the family's members and invitations","code":"curl -X GET \"https://apis.schoolsoutapp.com/api/users/viewer/family/members\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"security":[{"AccountCredential":[]}]}},"/api/users/viewer/family/invitations":{"post":{"summary":"Invite an adult into the family","description":"Owner only (manage_members). Creates an invitation valid for seven days with a role and children, and queues its email in the outbox in the same transaction. The response reveals inviteUrl once. The token is 32 random bytes (43 URL-safe base64 characters) in the fragment; only its SHA-256 is stored. Accepting needs the invited address. An expired open invitation to the address is replaced. Issuing links (create, resend, copy link, and the per-child invite) is limited to 20 an hour per owner.","tags":["Internal family account"],"x-internal":true,"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","required":["invitation","inviteUrl","emailSent"],"properties":{"invitation":{"$ref":"#/components/schemas/FamilyInvitation"},"inviteUrl":{"type":"string","format":"uri","description":"Revealed once; the token lives in the URL fragment."},"emailSent":{"type":"boolean","description":"The invitation email was queued in the outbox (false when the deployment has no API_AES_KEY to seal it; copy the link instead)."}}}}}}}},"400":{"description":"Invalid invitation, own address"},"401":{"description":"Current account required"},"403":{"description":"Not the family's owner"},"404":{"description":"A child that is not the family's"},"409":{"description":"An open invitation for the address, an adult already in the family, or a child at the five-adult limit"},"429":{"description":"Invitation rate limit. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."}},"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The viewer acts in one family: the family they are a member of, else their own (FamilyAccess); each route requires an action of the role permission matrix and answers 403 with that action's refusal otherwise.","x-curl-examples":[{"title":"Invite an adult into the family","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/family/invitations\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\": \"ruth@example.com\", \"name\": \"Ruth Bello\", \"role\": \"caregiver\", \"relation\": \"Grandparent\", \"childIds\": [\"1f8a3c5e-7b2d-4e9f-a6c1-4d0b8e2f7a93\"]}'"}],"security":[{"AccountCredential":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","role"],"properties":{"email":{"type":"string","format":"email","maxLength":254},"name":{"type":["string","null"],"maxLength":120},"role":{"$ref":"#/components/schemas/FamilyMemberRole"},"relation":{"type":["string","null"],"maxLength":60},"childIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":20,"uniqueItems":true},"allChildren":{"const":true,"type":"boolean"},"redirect":{"type":"string","format":"uri","description":"Optional page the link opens, with the token in its fragment (e.g. \u003csite\u003e/consumer/invitation): an absolute URL on AUTH_ORIGIN, PARENT_SITE_ORIGIN or API_ASSISTANT_ORIGINS. Anything else, or none, links to the parent site's accept page, PARENT_SITE_ORIGIN/consumer/invitation."}},"additionalProperties":false,"description":"childIds or allChildren, not both. A co-parent invitation without either sees every child; a caregiver invitation must name them."}}}}}},"/api/users/viewer/family/invitations/{invitationId}/resend":{"post":{"summary":"Resend an invitation","description":"Owner only. Gives an open invitation (pending or expired) a new token, sends it now and expires it seven days from now, and queues the email again. The previous link stops working.","tags":["Internal family account"],"x-internal":true,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","required":["invitation","inviteUrl","emailSent"],"properties":{"invitation":{"$ref":"#/components/schemas/FamilyInvitation"},"inviteUrl":{"type":"string","format":"uri","description":"Revealed once; the token lives in the URL fragment."},"emailSent":{"type":"boolean","description":"The invitation email was queued in the outbox (false when the deployment has no API_AES_KEY to seal it; copy the link instead)."}}}}}}}},"401":{"description":"Current account required"},"403":{"description":"Not the family's owner"},"404":{"description":"No such invitation"},"409":{"description":"Accepted, declined or canceled"},"429":{"description":"Invitation rate limit. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."}},"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The viewer acts in one family: the family they are a member of, else their own (FamilyAccess); each route requires an action of the role permission matrix and answers 403 with that action's refusal otherwise.","x-curl-examples":[{"title":"Resend an invitation","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/family/invitations/{invitationId}/resend\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"security":[{"AccountCredential":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":[],"properties":{"redirect":{"type":"string","format":"uri","description":"Optional page the link opens, with the token in its fragment (e.g. \u003csite\u003e/consumer/invitation): an absolute URL on AUTH_ORIGIN, PARENT_SITE_ORIGIN or API_ASSISTANT_ORIGINS. Anything else, or none, links to the parent site's accept page, PARENT_SITE_ORIGIN/consumer/invitation."}},"additionalProperties":false}}}},"parameters":[{"in":"path","name":"invitationId","required":true,"schema":{"type":"string","format":"uuid"}}]}},"/api/users/viewer/family/invitations/{invitationId}/link":{"post":{"summary":"Copy an invitation link","description":"Owner only. As resend, without an email: a link for the owner to share themselves. The previous link stops working; accepting still needs the invited address.","tags":["Internal family account"],"x-internal":true,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","required":["invitation","inviteUrl"],"properties":{"invitation":{"$ref":"#/components/schemas/FamilyInvitation"},"inviteUrl":{"type":"string","format":"uri","description":"Revealed once; the token lives in the URL fragment."}}}}}}}},"401":{"description":"Current account required"},"403":{"description":"Not the family's owner"},"404":{"description":"No such invitation"},"409":{"description":"Accepted, declined or canceled"},"429":{"description":"Invitation rate limit. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."}},"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The viewer acts in one family: the family they are a member of, else their own (FamilyAccess); each route requires an action of the role permission matrix and answers 403 with that action's refusal otherwise.","x-curl-examples":[{"title":"Copy an invitation link","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/family/invitations/{invitationId}/link\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"security":[{"AccountCredential":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":[],"properties":{"redirect":{"type":"string","format":"uri","description":"Optional page the link opens, with the token in its fragment (e.g. \u003csite\u003e/consumer/invitation): an absolute URL on AUTH_ORIGIN, PARENT_SITE_ORIGIN or API_ASSISTANT_ORIGINS. Anything else, or none, links to the parent site's accept page, PARENT_SITE_ORIGIN/consumer/invitation."}},"additionalProperties":false}}}},"parameters":[{"in":"path","name":"invitationId","required":true,"schema":{"type":"string","format":"uuid"}}]}},"/api/users/viewer/family/invitations/{invitationId}":{"delete":{"summary":"Cancel an invitation","description":"Owner only. Cancels an open invitation; its link then previews as canceled.","tags":["Internal family account"],"x-internal":true,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","required":["invitation","canceled"],"properties":{"invitation":{"$ref":"#/components/schemas/FamilyInvitation"},"canceled":{"const":true,"type":"boolean"}}}}}}}},"401":{"description":"Current account required"},"403":{"description":"Not the family's owner"},"404":{"description":"No such invitation"},"409":{"description":"Already accepted, declined or canceled"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The viewer acts in one family: the family they are a member of, else their own (FamilyAccess); each route requires an action of the role permission matrix and answers 403 with that action's refusal otherwise.","x-curl-examples":[{"title":"Cancel an invitation","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/family/invitations/{invitationId}\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"security":[{"AccountCredential":[]}],"parameters":[{"in":"path","name":"invitationId","required":true,"schema":{"type":"string","format":"uuid"}}]}},"/api/users/viewer/family/members/{userId}":{"patch":{"summary":"Change a member's role or children","description":"Owner only. A new role, relation (null clears it), and childIds or allChildren. Children the member no longer sees lose the member's private notes and care requirements about them. The member is told (role_changed).","tags":["Internal family account"],"x-internal":true,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","required":["member"],"properties":{"member":{"$ref":"#/components/schemas/FamilyMember"}}}}}}}},"400":{"description":"Nothing to change, or an invalid value"},"401":{"description":"Current account required"},"403":{"description":"Not the family's owner"},"404":{"description":"Not a member of the family, or a child that is not the family's"},"409":{"description":"The owner, or a child at the five-adult limit"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The viewer acts in one family: the family they are a member of, else their own (FamilyAccess); each route requires an action of the role permission matrix and answers 403 with that action's refusal otherwise.","x-curl-examples":[{"title":"Change a member's role or children","code":"curl -X PATCH \"https://apis.schoolsoutapp.com/api/users/viewer/family/members/{userId}\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"role\": \"co_parent\", \"allChildren\": true}'"}],"security":[{"AccountCredential":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":[],"properties":{"role":{"$ref":"#/components/schemas/FamilyMemberRole"},"relation":{"type":["string","null"],"maxLength":60},"childIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":20,"uniqueItems":true},"allChildren":{"const":true,"type":"boolean"}},"additionalProperties":false,"minProperties":1}}}},"parameters":[{"in":"path","name":"userId","required":true,"schema":{"type":"string","format":"uuid"}}]},"delete":{"summary":"Remove a member, or leave","description":"The owner removes a member; a member names themselves to leave. The member's grants and private context about the family's children go with the membership. The other side is told (member_removed).","tags":["Internal family account"],"x-internal":true,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","required":["removed"],"properties":{"removed":{"const":true,"type":"boolean"}}}}}}}},"401":{"description":"Current account required"},"403":{"description":"A member removing someone else"},"404":{"description":"Not a member of the family"},"409":{"description":"The owner"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The viewer acts in one family: the family they are a member of, else their own (FamilyAccess); each route requires an action of the role permission matrix and answers 403 with that action's refusal otherwise.","x-curl-examples":[{"title":"Remove a member, or leave","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/family/members/{userId}\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"security":[{"AccountCredential":[]}],"parameters":[{"in":"path","name":"userId","required":true,"schema":{"type":"string","format":"uuid"}}]}},"/api/users/viewer/family/leave":{"post":{"summary":"Leave the family","description":"A member leaves the family they help with (as DELETE on their own id). The owner is told (member_removed). 409 for the owner.","tags":["Internal family account"],"x-internal":true,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","required":["left"],"properties":{"left":{"const":true,"type":"boolean"}}}}}}}},"401":{"description":"Current account required"},"404":{"description":"Not a member"},"409":{"description":"The owner cannot leave their own family"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The viewer acts in one family: the family they are a member of, else their own (FamilyAccess); each route requires an action of the role permission matrix and answers 403 with that action's refusal otherwise.","x-curl-examples":[{"title":"Leave the family","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/family/leave\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"security":[{"AccountCredential":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":[],"properties":{},"additionalProperties":false}}}}}},"/api/users/viewer/family-invitations/accept":{"post":{"summary":"Accept a family invitation","description":"The signed-in account whose email is the invited address joins the family with the invitation's role and children, and is told the family as they now see it. Holding the token proves the inbox, so accepting verifies an unverified matching email. One family per adult: a member of another family, an owner with members, or an account with its own child profiles gets 409. An adult already in this family takes the invitation's role and gains its children. The owner is told (member_joined).","tags":["Internal family account"],"x-internal":true,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","required":["member","family"],"properties":{"member":{"$ref":"#/components/schemas/FamilyMember"},"family":{"$ref":"#/components/schemas/FamilyMembers"}}}}}}}},"400":{"description":"approve must be true"},"401":{"description":"Current account required"},"403":{"description":"Signed in with another address, or the family's own owner"},"404":{"description":"Unknown, accepted, declined, canceled or expired invitation"},"409":{"description":"One family per adult, or a child at the five-adult limit"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The viewer acts in one family: the family they are a member of, else their own (FamilyAccess); each route requires an action of the role permission matrix and answers 403 with that action's refusal otherwise.","x-curl-examples":[{"title":"Accept a family invitation","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/family-invitations/accept\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"security":[{"AccountCredential":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","approve"],"properties":{"token":{"type":"string","minLength":43,"maxLength":43,"pattern":"^[A-Za-z0-9_-]{43}$"},"approve":{"const":true,"type":"boolean"}},"additionalProperties":false}}}}}},"/api/users/viewer/family-invitations/decline":{"post":{"summary":"Decline a family invitation (signed in)","description":"As the public decline, for a signed-in invitee.","tags":["Internal family account"],"x-internal":true,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","required":["status"],"properties":{"status":{"const":"declined","type":"string"}}}}}}}},"401":{"description":"Current account required"},"404":{"description":"Unknown token"},"409":{"description":"No longer pending"},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. The viewer acts in one family: the family they are a member of, else their own (FamilyAccess); each route requires an action of the role permission matrix and answers 403 with that action's refusal otherwise.","x-curl-examples":[{"title":"Decline a family invitation (signed in)","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/family-invitations/decline\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}],"security":[{"AccountCredential":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string","minLength":43,"maxLength":43,"pattern":"^[A-Za-z0-9_-]{43}$"}},"additionalProperties":false}}}}}},"/api/family-invitations/preview":{"post":{"summary":"Preview a family invitation","description":"Public. What the invitation is for while pending (who invited, the role, the children's first names, the masked address), and only its status and inviter otherwise, so the page can say expired, canceled, declined or accepted. Unknown tokens are 404.","tags":["Internal family account"],"x-internal":true,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","required":["invitation"],"properties":{"invitation":{"$ref":"#/components/schemas/FamilyInvitationPreview"}}}}}}}},"404":{"description":"Unknown token"},"429":{"description":"Too many failed lookups from this requester, or too many requests for this invitation"}},"x-auth":"Public: the invitation token in the body is the only credential. The parent site may call it with its catalog-only key (x-api-key), adding the visitor's address to X-Forwarded-For, which only that key is trusted to forward. Limited per requester (the signed-in account, else the client address) on lookups that find no invitation, checked before looking, and per invitation on links that do (hourly policy limits).","x-curl-examples":[{"title":"Preview a family invitation","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/family-invitations/preview\""}],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string","minLength":43,"maxLength":43,"pattern":"^[A-Za-z0-9_-]{43}$"}},"additionalProperties":false}}}}}},"/api/family-invitations/decline":{"post":{"summary":"Decline a family invitation","description":"Public (the invitee may have no account). Declines a pending invitation and tells the owner (invitation_declined) once; declining again answers the same.","tags":["Internal family account"],"x-internal":true,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","required":["status"],"properties":{"status":{"const":"declined","type":"string"}}}}}}}},"404":{"description":"Unknown token"},"409":{"description":"No longer pending"},"429":{"description":"Too many failed lookups from this requester, or too many requests for this invitation"}},"x-auth":"Public: the invitation token in the body is the only credential. The parent site may call it with its catalog-only key (x-api-key), adding the visitor's address to X-Forwarded-For, which only that key is trusted to forward. Limited per requester (the signed-in account, else the client address) on lookups that find no invitation, checked before looking, and per invitation on links that do (hourly policy limits).","x-curl-examples":[{"title":"Decline a family invitation","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/family-invitations/decline\""}],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string","minLength":43,"maxLength":43,"pattern":"^[A-Za-z0-9_-]{43}$"}},"additionalProperties":false}}}}}},"/api/family-invitations/request-new":{"post":{"summary":"Ask for a new invitation","description":"Public. For an expired, canceled or declined invitation: tells the owner (new_invite_requested) once per invitation; asking again answers the first requestedAt.","tags":["Internal family account"],"x-internal":true,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true,"type":"boolean"},"data":{"type":"object","required":["requestedAt"],"properties":{"requestedAt":{"type":"string","format":"date-time"}}}}}}}},"404":{"description":"Unknown token"},"409":{"description":"Still pending, or accepted"},"429":{"description":"Too many failed lookups from this requester, or too many requests for this invitation"}},"x-auth":"Public: the invitation token in the body is the only credential. The parent site may call it with its catalog-only key (x-api-key), adding the visitor's address to X-Forwarded-For, which only that key is trusted to forward. Limited per requester (the signed-in account, else the client address) on lookups that find no invitation, checked before looking, and per invitation on links that do (hourly policy limits).","x-curl-examples":[{"title":"Ask for a new invitation","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/family-invitations/request-new\""}],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string","minLength":43,"maxLength":43,"pattern":"^[A-Za-z0-9_-]{43}$"}},"additionalProperties":false}}}}}},"/api/users/viewer/groups":{"get":{"tags":["Internal family groups"],"x-internal":true,"summary":"List your groups","description":"Your groups, most recent activity first, each with its unread count and the newest message you can read.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["groups"],"properties":{"groups":{"type":"array","items":{"$ref":"#/components/schemas/FamilyGroup"}}}}}}}}},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"List your groups","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/groups\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"post":{"tags":["Internal family groups"],"x-internal":true,"summary":"Start a group","description":"Creates a group with you as its first member and invites the named friends. Only accepted friends can be invited, and a friend with a block with any member cannot; either answers 404 without saying which.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":80},"userIds":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":20,"description":"Accepted friends to invite; yourself and repeats are ignored."}},"additionalProperties":false}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["group"],"properties":{"group":{"$ref":"#/components/schemas/FamilyGroupDetail"}}}}}}}},"400":{"description":"Invalid name or userIds."},"404":{"description":"One of userIds is not your accepted friend."},"409":{"description":"You are in the most groups allowed (100), or the invitations exceed the group's 50 seats."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Start a group","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/groups\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"Maya's 2nd grade class\",\"userIds\":[\"$FRIEND_ID\"]}'"}]}},"/api/users/viewer/groups/{groupId}":{"get":{"tags":["Internal family groups"],"x-internal":true,"summary":"Read a group","description":"The group with its members and open invitations.","parameters":[{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["group"],"properties":{"group":{"$ref":"#/components/schemas/FamilyGroupDetail"}}}}}}}},"404":{"description":"Group not found, or you are not a member."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the group's members see it; anyone else receives 404.","x-curl-examples":[{"title":"Read a group","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/groups/$GROUP_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"patch":{"tags":["Internal family groups"],"x-internal":true,"summary":"Rename a group","description":"Names the group again. Only the group's owner may rename it.","parameters":[{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":80}},"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["group"],"properties":{"group":{"$ref":"#/components/schemas/FamilyGroupDetail"}}}}}}}},"400":{"description":"name is not one line of 1 to 80 characters."},"403":{"description":"You are a member but not the group's owner."},"404":{"description":"Group not found, or you are not a member."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the group's members see it; anyone else receives 404. Only its owner may make this change; another member receives 403.","x-curl-examples":[{"title":"Rename a group","code":"curl -X PATCH \"https://apis.schoolsoutapp.com/api/users/viewer/groups/$GROUP_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"Sellwood neighbors\"}'"}]},"delete":{"tags":["Internal family groups"],"x-internal":true,"summary":"Delete a group","description":"Deletes the group for every member, with its messages, invitations and plans. Only the group's owner may delete it; any member may leave instead.","parameters":[{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["deleted"],"properties":{"deleted":{"const":true}}}}}}}},"403":{"description":"You are a member but not the group's owner."},"404":{"description":"Group not found, or you are not a member."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the group's members see it; anyone else receives 404. Only its owner may make this change; another member receives 403.","x-curl-examples":[{"title":"Delete a group","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/groups/$GROUP_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/groups/{groupId}/invitations":{"post":{"tags":["Internal family groups"],"x-internal":true,"summary":"Invite friends to a group","description":"Invites your accepted friends into a group you are a member of. A friend already a member or already invited is left as they are. Members and open invitations together take at most 50 seats.","parameters":[{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userIds"],"properties":{"userIds":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":20,"description":"Accepted friends to invite; yourself and repeats are ignored.","minItems":1}},"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["group"],"properties":{"group":{"$ref":"#/components/schemas/FamilyGroupDetail"}}}}}}}},"400":{"description":"No userIds, or invalid ones."},"404":{"description":"Group not found, or one of userIds is not your accepted friend."},"409":{"description":"The group has no seat left."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the group's members see it; anyone else receives 404.","x-curl-examples":[{"title":"Invite friends to a group","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/groups/$GROUP_ID/invitations\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"userIds\":[\"$FRIEND_ID\"]}'"}]}},"/api/users/viewer/groups/{groupId}/membership":{"delete":{"tags":["Internal family groups"],"x-internal":true,"summary":"Leave a group","description":"Removes you from the group. The last member to leave deletes it with its messages and invitations.","parameters":[{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["left"],"properties":{"left":{"const":true}}}}}}}},"404":{"description":"Group not found, or you are not a member."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the group's members see it; anyone else receives 404.","x-curl-examples":[{"title":"Leave a group","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/groups/$GROUP_ID/membership\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/groups/{groupId}/messages":{"get":{"tags":["Internal family groups"],"x-internal":true,"summary":"Read a group's messages","description":"A page of the messages you can read (those sent after you joined), oldest first. With after, the messages since that seq: poll it with latestSeq to see new messages and Assistant's replies. With before, the page before that seq. With neither, the newest page.","parameters":[{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"after","in":"query","required":false,"schema":{"type":"integer","minimum":0},"description":"Return messages after this seq, oldest first."},{"name":"before","in":"query","required":false,"schema":{"type":"integer","minimum":0},"description":"Return the page before this seq."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Page size."}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/FamilyGroupMessages"}}}}}},"400":{"description":"Invalid after, before or limit, or both after and before."},"404":{"description":"Group not found, or you are not a member."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the group's members see it; anyone else receives 404.","x-curl-examples":[{"title":"Read a group's messages","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/groups/$GROUP_ID/messages?after=$LATEST_SEQ\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"post":{"tags":["Internal family groups"],"x-internal":true,"summary":"Send a message to a group","description":"Stores the message as the group's next seq; sending the same clientMessageId again returns the stored message (200) instead of a second one. With asksAssistant (the app's mention of Assistant in the message), Assistant answers in the thread from the group's messages you can read and your own family context, which it never repeats to the group; its reply arrives as a later message with replyToMessageId. Asking Assistant requires your own session-backed account and is admitted before the message is stored. Verified parents share the private-chat daily account limit and all-account question budget; staff and ADMIN retain their exemption. Only one question per parent per group may be pending. Failed or duplicate sends and failed answers release the original admission once, including when the UTC day has changed. With shared, the message shares a collection you own or may open, or an activity the catalog shows, as a card; text is then an optional note. Members see the card only while they could open the item themselves.","parameters":[{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["clientMessageId"],"properties":{"clientMessageId":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,64}$","description":"Your idempotency key for this message; keep it when retrying."},"text":{"type":"string","maxLength":4000,"description":"Required unless shared is given; then an optional note (and required when asksAssistant)."},"asksAssistant":{"type":"boolean","default":false,"description":"The message mentions Assistant, which answers in the thread."},"shared":{"type":"object","required":["kind","id"],"properties":{"kind":{"enum":["collection","activity"]},"id":{"type":"string","format":"uuid"}},"additionalProperties":false,"description":"An item to share as a card. Plans are shared by proposing them (POST .../plans)."}},"additionalProperties":false}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["message"],"properties":{"message":{"$ref":"#/components/schemas/FamilyGroupMessage"}}}}}}}},"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["message"],"properties":{"message":{"$ref":"#/components/schemas/FamilyGroupMessage"}}}}}}}},"400":{"description":"Invalid clientMessageId, text or shared."},"402":{"description":"The Ask allowance is spent (asksAssistant)."},"403":{"description":"Assistant requires the viewer’s own session-backed account and a verified email for consumer questions."},"404":{"description":"Group not found, or you are not a member; or the shared collection or activity is not one you may share."},"409":{"description":"clientMessageId was used for a different message, or Assistant is still answering your previous question in this group."},"429":{"description":"Daily message limit; retryAfterMs is how many milliseconds until another message is allowed. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs. Assistant’s daily account question limit returns Retry-After in seconds until the next UTC day."},"503":{"description":"Assistant is not configured, or the shared daily question budget is exhausted. Budget refusals include Retry-After in seconds until the next UTC day."},"401":{"description":"Current account required."}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the group's members see it; anyone else receives 404.","x-curl-examples":[{"title":"Send a message to a group","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/groups/$GROUP_ID/messages\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"clientMessageId\":\"m-8f3a2c1d\",\"text\":\"Anyone free on the 16th?\",\"asksAssistant\":false}'"}]}},"/api/users/viewer/groups/{groupId}/read":{"put":{"tags":["Internal family groups"],"x-internal":true,"summary":"Mark a group read","description":"Moves your read position up to seq, never past the newest message and never back.","parameters":[{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["seq"],"properties":{"seq":{"type":"integer","minimum":0}},"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["lastReadSeq"],"properties":{"lastReadSeq":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"seq is missing or negative."},"404":{"description":"Group not found, or you are not a member."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the group's members see it; anyone else receives 404.","x-curl-examples":[{"title":"Mark a group read","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/groups/$GROUP_ID/read\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"seq\":42}'"}]}},"/api/users/viewer/group-invitations":{"get":{"tags":["Internal family groups"],"x-internal":true,"summary":"List your group invitations","description":"Your open invitations, newest first.","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["invitations"],"properties":{"invitations":{"type":"array","items":{"$ref":"#/components/schemas/FamilyGroupInvitation"}}}}}}}}},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"List your group invitations","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/group-invitations\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/group-invitations/{invitationId}":{"put":{"tags":["Internal family groups"],"x-internal":true,"summary":"Answer a group invitation","description":"accept joins the group (you read the messages sent after you join); decline closes the invitation. Accepting needs you to still be the inviter's friend with no block with any member. Repeating an answer already given is a no-op.","parameters":[{"name":"invitationId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"enum":["accept","decline"]}},"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["group"],"properties":{"group":{"oneOf":[{"$ref":"#/components/schemas/FamilyGroupDetail"},{"type":"null"}],"description":"The group after accepting; null after declining."}}}}}}}},"400":{"description":"Invalid action."},"404":{"description":"Invitation not found for this account."},"409":{"description":"The invitation was already answered the other way or can no longer be accepted, or you are in the most groups allowed."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin.","x-curl-examples":[{"title":"Answer a group invitation","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/group-invitations/$INVITATION_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\":\"accept\"}'"}]}},"/api/users/viewer/children/{childId}/documents":{"get":{"tags":["Internal family profiles"],"x-internal":true,"summary":"List a child's documents","description":"The documents on a child's profile, newest first, without their files.","parameters":[{"name":"childId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["documents"],"properties":{"documents":{"type":"array","items":{"$ref":"#/components/schemas/FamilyChildDocument"}}}}}}}}},"403":{"description":"Only the family's owner may manage a child's documents (manage_child_documents)."},"404":{"description":"Child not found for this account."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the family's owner (the parent admin) reads or changes a child's documents; co-parents and caregivers receive 403, other accounts 404.","x-curl-examples":[{"title":"List a child's documents","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/documents\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"post":{"tags":["Internal family profiles"],"x-internal":true,"summary":"Add a document to a child's profile","description":"Body is the file: application/pdf, image/jpeg or image/png, at most 10 MiB, and its bytes must be that type. Images are re-encoded without their metadata (EXIF, including location); a PDF is kept as sent. The file is encrypted at rest, kept until you delete it, the child or the account, and never exported or shared. A child has at most 50 documents; the same file added again answers the stored document (200).","parameters":[{"name":"childId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"fileName","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"The file's name; a path keeps its last part."},{"name":"title","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":120},"description":"What the document is; defaults to fileName."}],"requestBody":{"required":true,"content":{"application/pdf":{"schema":{"type":"string","format":"binary"}},"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["document"],"properties":{"document":{"$ref":"#/components/schemas/FamilyChildDocument"}}}}}}}},"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["document"],"properties":{"document":{"$ref":"#/components/schemas/FamilyChildDocument"}}}}}}}},"400":{"description":"fileName is missing or invalid, or title is invalid."},"403":{"description":"Only the family's owner may manage a child's documents (manage_child_documents)."},"404":{"description":"Child not found for this account."},"409":{"description":"The child has 50 documents."},"413":{"description":"Larger than 10 MiB."},"415":{"description":"Not application/pdf, image/jpeg or image/png."},"422":{"description":"The bytes are not the declared type, or the image cannot be decoded (or is over 4096 px a side)."},"503":{"description":"Document storage is not configured (API_AES_KEY)."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the family's owner (the parent admin) reads or changes a child's documents; co-parents and caregivers receive 403, other accounts 404.","x-curl-examples":[{"title":"Add a document to a child's profile","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/documents?fileName=immunizations.pdf\u0026title=Immunization%20record\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/pdf\" \\\n  --data-binary @immunizations.pdf"}]}},"/api/users/viewer/children/{childId}/documents/{documentId}":{"get":{"tags":["Internal family profiles"],"x-internal":true,"summary":"Download a child's document","description":"The file as an attachment, never cached.","parameters":[{"name":"childId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"documentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The file.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}},"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}}}},"403":{"description":"Only the family's owner may manage a child's documents (manage_child_documents)."},"404":{"description":"Child or document not found for this account."},"503":{"description":"Document storage is not configured (API_AES_KEY)."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the family's owner (the parent admin) reads or changes a child's documents; co-parents and caregivers receive 403, other accounts 404.","x-curl-examples":[{"title":"Download a child's document","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/documents/$DOCUMENT_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -o document"}]},"delete":{"tags":["Internal family profiles"],"x-internal":true,"summary":"Delete a child's document","description":"Removes the document and its file for good.","parameters":[{"name":"childId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"documentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["deleted"],"properties":{"deleted":{"const":true}}}}}}}},"403":{"description":"Only the family's owner may manage a child's documents (manage_child_documents)."},"404":{"description":"Child or document not found for this account."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the family's owner (the parent admin) reads or changes a child's documents; co-parents and caregivers receive 403, other accounts 404.","x-curl-examples":[{"title":"Delete a child's document","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/children/$CHILD_ID/documents/$DOCUMENT_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/groups/{groupId}/plans":{"get":{"tags":["Internal family groups"],"x-internal":true,"summary":"Read a group's plans","description":"The group's plans, soonest first, with each member's answer.","parameters":[{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["plans"],"properties":{"plans":{"type":"array","items":{"$ref":"#/components/schemas/FamilyGroupPlan"}}}}}}}}},"404":{"description":"Group not found, or you are not a member."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the group's members see it; anyone else receives 404.","x-curl-examples":[{"title":"Read a group's plans","code":"curl \"https://apis.schoolsoutapp.com/api/users/viewer/groups/$GROUP_ID/plans\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]},"post":{"tags":["Internal family groups"],"x-internal":true,"summary":"Propose a plan to a group","description":"Stores the plan and sends a message that shares it, as a send does: once per clientMessageId (a repeat returns the stored message, 200) and within the daily message limit. A group keeps at most 200 plans.","parameters":[{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["clientMessageId","title","date"],"properties":{"clientMessageId":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,64}$","description":"Your idempotency key for the message that shares the plan; keep it when retrying."},"title":{"type":"string","minLength":1,"maxLength":120},"date":{"type":"string","format":"date"},"time":{"type":["string","null"],"pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","description":"Local time on date, HH:MM on a 24-hour clock."},"note":{"type":["string","null"],"maxLength":1000},"activityId":{"type":["string","null"],"format":"uuid","description":"A catalog activity the plan is for."}},"additionalProperties":false}}}},"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["message"],"properties":{"message":{"$ref":"#/components/schemas/FamilyGroupMessage"}}}}}}}},"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["message"],"properties":{"message":{"$ref":"#/components/schemas/FamilyGroupMessage"}}}}}}}},"400":{"description":"Invalid clientMessageId, title, date, time, note or activityId."},"404":{"description":"Group not found, or you are not a member; or the activity is not in the catalog."},"409":{"description":"clientMessageId was used for a different message, or the group has the most plans allowed."},"429":{"description":"Daily message limit; retryAfterMs is how many milliseconds until another message is allowed. A session-backed request can also be refused by the family request limit (FamilyRequestLimit), with retryAfterMs and windowMs."},"401":{"description":"Current account required."}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the group's members see it; anyone else receives 404.","x-curl-examples":[{"title":"Propose a plan to a group","code":"curl -X POST \"https://apis.schoolsoutapp.com/api/users/viewer/groups/$GROUP_ID/plans\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"clientMessageId\":\"p-8f3a2c1d\",\"title\":\"Oaks Bottom walk\",\"date\":\"2026-11-07\",\"time\":\"10:00\"}'"}]}},"/api/users/viewer/groups/{groupId}/plans/{planId}":{"delete":{"tags":["Internal family groups"],"x-internal":true,"summary":"Delete a plan","description":"Deletes the plan with its answers. Whoever proposed it or the group's owner may; the message that shared it stays, with no plan.","parameters":[{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"planId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["deleted"],"properties":{"deleted":{"const":true}}}}}}}},"403":{"description":"You neither proposed the plan nor own the group."},"404":{"description":"Group or plan not found, or you are not a member."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the group's members see it; anyone else receives 404.","x-curl-examples":[{"title":"Delete a plan","code":"curl -X DELETE \"https://apis.schoolsoutapp.com/api/users/viewer/groups/$GROUP_ID/plans/$PLAN_ID\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\""}]}},"/api/users/viewer/groups/{groupId}/plans/{planId}/answer":{"put":{"tags":["Internal family groups"],"x-internal":true,"summary":"Answer a plan","description":"Records whether you are going (yes, maybe or no); null takes your answer back.","parameters":[{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"planId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["answer"],"properties":{"answer":{"enum":["yes","maybe","no",null]}},"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"const":true},"data":{"type":"object","required":["plan"],"properties":{"plan":{"$ref":"#/components/schemas/FamilyGroupPlan"}}}}}}}},"400":{"description":"answer is not yes, maybe, no or null."},"404":{"description":"Group or plan not found, or you are not a member."},"401":{"description":"Current account required."},"429":{"$ref":"#/components/responses/FamilyRequestLimit"}},"security":[{"AccountCredential":[]}],"x-auth":"Current account cookie or account credential only. Machine keys, investor previews and delegated service requests are rejected regardless of plan, billing team, staff or ADMIN level. Cookie mutations require CSRF from a trusted or API_ASSISTANT_ORIGINS origin. Only the group's members see it; anyone else receives 404.","x-curl-examples":[{"title":"Answer a plan","code":"curl -X PUT \"https://apis.schoolsoutapp.com/api/users/viewer/groups/$GROUP_ID/plans/$PLAN_ID/answer\" \\\n  -H \"x-api-key: $SCHOOLS_OUT_ACCOUNT_CREDENTIAL\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"answer\":\"yes\"}'"}]}}},"security":[{"ApiKeyAuth":[]}],"servers":[{"description":"Production","url":"https://apis.schoolsoutapp.com"}],"tags":[{"name":"Internal family profiles","description":"Private child profiles shared only with explicitly invited and accepted guardians. Machine keys and service impersonation are rejected."},{"name":"Internal family social","description":"Parents' saves, collections, follows, public profiles, friends and reports. Machine keys and service impersonation are rejected, except on the public collection and photo reads."},{"name":"Internal family groups","description":"Parents' group chats: groups of accepted friends, invitations, messages polled by seq, and Assistant answering in the thread. Machine keys and service impersonation are rejected."},{"name":"Internal family account","description":"The signed-in parent's account: profile, devices, data export and connected Google accounts. Machine keys and service impersonation are rejected."},{"name":"Internal family bookings","description":"Parent-approved camp bookings: saved cards, the booking policy (service fee and group rate), approval with a spending limit, status polling, camp questions, applications and their outcomes, changes, cancellation and refunds, and the wallet with its group rate credit. The booking routes are registered only with BOOKINGS_ENABLED=true; the money routes (saved cards, wallet, top-ups, payments and receipts, gift cards, staff charge follow-ups) are always registered, and those that reach Stripe answer 503 without STRIPE_SECRET_KEY. Machine keys and service impersonation are rejected."},{"name":"Internal family calendar","description":"What a family plans: school search and each child's school, school calendars, plans made outside bookings, day marks, the composed calendar and Home. The viewer's own account only; machine keys and service impersonation are rejected."}]}