{"openapi":"3.1.0","info":{"title":"AlertCamp","description":"Service d'alertes de disponibilité SEPAQ pour le camping au Québec","version":"0.1.0"},"paths":{"/api/v1/auth/signin":{"post":{"tags":["auth"],"summary":"Sign In Email","description":"Sign in with email and password via backend.\n\nVerifies credentials using Firebase REST API, then returns id_token +\nrefresh_token directly. The mobile app stores these tokens locally and\nuses them for API calls without touching the Firebase SDK (avoids\nAndroid reCAPTCHA delay).","operationId":"sign_in_email_api_v1_auth_signin_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailSignInRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/signup":{"post":{"tags":["auth"],"summary":"Sign Up Email","description":"Create a new account with email and password via backend.\n\nCreates the user via Firebase REST API (accounts:signUp) which returns\nid_token + refresh_token directly. The mobile app stores these tokens\nlocally without touching the Firebase SDK.\nRequires terms_accepted=true for legal compliance (PIPEDA).","operationId":"sign_up_email_api_v1_auth_signup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailSignUpRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/google":{"post":{"tags":["auth"],"summary":"Sign In Google","description":"Sign in with Google via backend.\n\nReceives the Google OAuth ID token from the mobile client's native\nGoogle Sign-In SDK, exchanges it for Firebase tokens via REST API\n(signInWithIdp). Same pattern as email/password — bypasses the\nFirebase SDK's ~4min reCAPTCHA delay on Android.","operationId":"sign_in_google_api_v1_auth_google_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoogleSignInRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/apple":{"post":{"tags":["auth"],"summary":"Sign In Apple","description":"Sign in with Apple via backend.\n\nReceives the Apple identity token from the mobile client's native\nSign in with Apple SDK, exchanges it for Firebase tokens via REST API.\nRequired for App Store approval when other social logins are offered.","operationId":"sign_in_apple_api_v1_auth_apple_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppleSignInRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/facebook":{"post":{"tags":["auth"],"summary":"Sign In Facebook","description":"Sign in with Facebook via backend.\n\nReceives the Facebook access token from the mobile client's native\nFacebook Login SDK, exchanges it for Firebase tokens via REST API.","operationId":"sign_in_facebook_api_v1_auth_facebook_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacebookSignInRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/facebook/login":{"get":{"tags":["auth"],"summary":"Facebook Oauth Login","description":"Start Facebook OAuth web flow.\n\nGenerates a state nonce, stores it, and redirects to Facebook's\nOAuth dialog. The user authenticates on Facebook, which redirects\nback to /auth/facebook/callback.","operationId":"facebook_oauth_login_api_v1_auth_facebook_login_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/auth/facebook/callback":{"get":{"tags":["auth"],"summary":"Facebook Oauth Callback","description":"Facebook OAuth callback.\n\nReceives the authorization code from Facebook, validates the state nonce,\nstores the FB code for deferred processing, and redirects to the app\nvia deep link. All heavy work (token exchange, Firebase sign-in, DB)\nhappens in /facebook/exchange so the browser dismisses instantly.","operationId":"facebook_oauth_callback_api_v1_auth_facebook_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/facebook/exchange":{"post":{"tags":["auth"],"summary":"Facebook Oauth Exchange","description":"Exchange a one-time code for Firebase tokens.\n\nCalled by the mobile app after receiving the deep link from the\nOAuth callback. The code is single-use and expires after 5 minutes.\nPerforms the Facebook token exchange, Firebase sign-in, and DB\nuser creation/lookup that was deferred from the callback.","operationId":"facebook_oauth_exchange_api_v1_auth_facebook_exchange_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacebookExchangeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/refresh":{"post":{"tags":["auth"],"summary":"Refresh Token","description":"Refresh an expired Firebase ID token.\n\nUnauthenticated endpoint — the caller only needs a valid refresh_token.\nReturns a new id_token + refresh_token pair.","operationId":"refresh_token_api_v1_auth_refresh_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/forgot-password":{"post":{"tags":["auth"],"summary":"Forgot Password","description":"Request a password reset code.\n\nSends a 6-digit OTP code to the given email address.\n**Always returns success** even if the email is not found, to prevent\nemail enumeration attacks.","operationId":"forgot_password_api_v1_auth_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/verify-reset-code":{"post":{"tags":["auth"],"summary":"Verify Reset Code","description":"Verify a password reset code without consuming it.\n\nUsed by the mobile app to validate the OTP before showing the\nnew-password form.  The code remains valid for a subsequent\n``/reset-password`` call.","operationId":"verify_reset_code_api_v1_auth_verify_reset_code_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyResetCodeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyResetCodeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/reset-password":{"post":{"tags":["auth"],"summary":"Reset Password","description":"Reset password using a verified 6-digit code.\n\nVerifies the code, then updates the user's password via Firebase Admin SDK.","operationId":"reset_password_api_v1_auth_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/resend-verification":{"post":{"tags":["auth"],"summary":"Resend Verification Email","description":"Resend email verification code.\n\nRequires authentication but allows unverified email users.\nGenerates a new 6-digit code and sends it via email.","operationId":"resend_verification_email_api_v1_auth_resend_verification_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResendVerificationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/verify-email-code":{"post":{"tags":["auth"],"summary":"Verify Email Code","description":"Verify email address with a 6-digit code.\n\nOn success, sets email_verified=true in our DB and on Firebase.","operationId":"verify_email_code_api_v1_auth_verify_email_code_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEmailCodeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEmailCodeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/accept-terms":{"post":{"tags":["auth"],"summary":"Accept Terms","description":"Accept Terms of Service.\n\nUsed by social auth users (Google/Apple/Facebook) who bypass the signup\nform and thus don't have a ToS checkbox. Must be called before accessing\nthe main app.","operationId":"accept_terms_api_v1_auth_accept_terms_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptTermsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptTermsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/me":{"get":{"tags":["auth"],"summary":"Get Current User Profile","description":"Get the current user's profile.\n\nReturns the authenticated user's profile including subscription status.\nCreates a new user record on first login (handled by get_current_user dependency).","operationId":"get_current_user_profile_api_v1_auth_me_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserWithSubscription"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["auth"],"summary":"Update Current User Profile","description":"Update the current user's profile.\n\nAllows updating:\n- display_name\n- language (fr/en)\n- email_notifications_enabled\n- push_notifications_enabled","operationId":"update_current_user_profile_api_v1_auth_me_patch","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/fcm-token":{"post":{"tags":["auth"],"summary":"Register Fcm Token","description":"Register or update the user's FCM token for push notifications.\n\nAccepts optional device metadata (device_name, platform, os_version,\napp_version) alongside the FCM token.\n\nWhen ``device_id`` is provided, upserts into the ``user_devices`` table\n(multi-device path).  Without ``device_id``, falls back to the legacy\nsingle-token columns on the ``users`` table.\n\nShould be called:\n- On app startup after Firebase Messaging initialization\n- When the FCM token is refreshed","operationId":"register_fcm_token_api_v1_auth_fcm_token_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenRegister"}}}},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["auth"],"summary":"Remove Fcm Token","description":"Remove the user's FCM token.\n\nWhen ``device_id`` is provided, deletes the specific row from\n``user_devices``.  Otherwise clears the legacy single-token columns.\n\nShould be called on logout to stop receiving push notifications\non this device.","operationId":"remove_fcm_token_api_v1_auth_fcm_token_delete","parameters":[{"name":"device_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/phone/request-verification":{"post":{"tags":["auth"],"summary":"Request Phone Verification","description":"Request a verification code for a phone number.\n\nSends a 6-digit OTP via SMS. Code expires in 10 minutes.\nA 60-second cooldown applies between requests.","operationId":"request_phone_verification_api_v1_auth_phone_request_verification_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneVerificationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneVerificationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/phone/verify":{"post":{"tags":["auth"],"summary":"Verify Phone","description":"Verify a phone number with the OTP code.\n\nOn success, saves the verified phone number to the user's profile.","operationId":"verify_phone_api_v1_auth_phone_verify_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneVerificationVerify"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneVerifiedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/badge/reset":{"post":{"tags":["auth"],"summary":"Reset Badge","description":"Reset the user's iOS notification badge count to 0.\n\nCalled by the mobile app when it returns to the foreground.","operationId":"reset_badge_api_v1_auth_badge_reset_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/phone":{"delete":{"tags":["auth"],"summary":"Remove Phone","description":"Remove the user's verified phone number.\n\nNo OTP required — the user is already authenticated.\nAlso cancels any pending verification.","operationId":"remove_phone_api_v1_auth_phone_delete","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/itineraries":{"post":{"tags":["itineraries"],"summary":"Create Itinerary","description":"Create a multi-night route alert (parent + one leg per night).","operationId":"create_itinerary_api_v1_alerts_itineraries_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["itineraries"],"summary":"List Itineraries","description":"List the current user's route alerts.","operationId":"list_itineraries_api_v1_alerts_itineraries_get","parameters":[{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Active Only"}},{"name":"include_expired","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Expired"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/itineraries/{itinerary_id}":{"get":{"tags":["itineraries"],"summary":"Get Itinerary","operationId":"get_itinerary_api_v1_alerts_itineraries__itinerary_id__get","parameters":[{"name":"itinerary_id","in":"path","required":true,"schema":{"type":"string","title":"Itinerary Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["itineraries"],"summary":"Delete Itinerary","description":"Soft-delete a route and deactivate its legs.\n\nThe parent is soft-deleted (same contract as alerts: history is kept for\nthe value digest) while the legs are hard-deactivated so the scheduler\nstops polling their zones immediately — a soft-deleted route must not keep\nconsuming poll budget.","operationId":"delete_itinerary_api_v1_alerts_itineraries__itinerary_id__delete","parameters":[{"name":"itinerary_id","in":"path","required":true,"schema":{"type":"string","title":"Itinerary Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["itineraries"],"summary":"Update Itinerary","description":"Update a route's delivery channels.\n\nOnly the parent's flags move.  The legs stay ``notify_* = FALSE`` no matter\nwhat is set here — they exist to put the route's zones into the poll\nrotation, and letting one flip to True would emit a per-night notification,\nwhich is exactly the behaviour routes were built to replace.","operationId":"update_itinerary_api_v1_alerts_itineraries__itinerary_id__patch","parameters":[{"name":"itinerary_id","in":"path","required":true,"schema":{"type":"string","title":"Itinerary Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/itineraries/{itinerary_id}/pause":{"post":{"tags":["itineraries"],"summary":"Pause Itinerary","description":"Pause a route — parent and every leg together.\n\nPausing must be atomic across the legs: a half-paused route would keep\npolling some nights while the aggregator could never satisfy all of them.","operationId":"pause_itinerary_api_v1_alerts_itineraries__itinerary_id__pause_post","parameters":[{"name":"itinerary_id","in":"path","required":true,"schema":{"type":"string","title":"Itinerary Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/itineraries/{itinerary_id}/resume":{"post":{"tags":["itineraries"],"summary":"Resume Itinerary","description":"Resume a paused route.\n\n``last_fired_signature`` is deliberately **preserved**, which is what an\nordinary alert does: the per-site path filters every change against\nexisting ``active``/``seen`` ``AlertMatch`` rows for the same\n(alert_id, site_id) before it dispatches, so a resumed alert can never\nre-announce something the user has already looked at — and its resume\nadditionally skips one whole match cycle (``_resumed_alert_ids``) to avoid\na stale-state burst.\n\nClearing the signature here made routes the only surface that re-announces\non resume, and the openings it named kept their ``seen_at`` — so a toggle\nproduced a push describing start days the card had been rendering as\nalready-read for days. A notification with nothing behind it is worse than\nsilence: it teaches the user to ignore the channel.\n\nPreserving it keeps the half that mattered. The matcher is level-triggered,\nso if the bookable set CHANGED while the route was paused its signature\ndiffers and the next sweep fires normally — the user IS told what is open\nnow. Only the exact set they were last notified about stays quiet.\n\nEdits that change the QUESTION (window, party_size, tent_pads) still\nre-baseline in ``update_itinerary``: a different question deserves a fresh\nanswer, exactly as editing an alert's criteria does.","operationId":"resume_itinerary_api_v1_alerts_itineraries__itinerary_id__resume_post","parameters":[{"name":"itinerary_id","in":"path","required":true,"schema":{"type":"string","title":"Itinerary Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/itineraries/{itinerary_id}/openings/seen":{"post":{"tags":["itineraries"],"summary":"Mark Openings Seen","description":"Clear the NEW badge on every opening this route is currently showing.\n\nIncludes EXPIRED (rebooked) rows, not only active ones. The detail screen\nrenders a taken opening exactly like a live one — struck through, \"booked\nat ...\" — so opening the screen counts as seeing it just the same. An\nopening rebooked BEFORE the user opened the screen was already expired by\nthe time this ran, so scoping to active rows left its ``seen_at`` NULL and\nstranded the \"x new\" badge on a row that could never be cleared — on the\ndetail counter and on the alerts-list badge alike.\n\nRe-stamps rows that are already seen but have REOPENED since, because that\nis what ``is_new`` now asks: ``record_openings`` revives a flickering start\nday on the SAME row and keeps its ``seen_at``, so \"seen once, ever\" would\nstrand a genuinely-new availability window as read (and the bell filter,\nwhich defaults to new-only, would then refuse to render the one bookable\ntrip). Scoping the UPDATE to ``seen_at IS NULL`` did exactly that. The\npredicate below must stay the mirror of ``ItineraryOpening.is_new``; if one\nmoves, so does the other, or the badge never clears.\n\nA user who never opens the screen never calls this, so a genuinely-unseen\nopening keeps its badge.","operationId":"mark_openings_seen_api_v1_alerts_itineraries__itinerary_id__openings_seen_post","parameters":[{"name":"itinerary_id","in":"path","required":true,"schema":{"type":"string","title":"Itinerary Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Mark Openings Seen Api V1 Alerts Itineraries  Itinerary Id  Openings Seen Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts":{"get":{"tags":["alerts"],"summary":"List Alerts","description":"List all alerts for the current user.\n\nReturns alerts with campground_selections included, sorted by creation date (newest first).","operationId":"list_alerts_api_v1_alerts_get","parameters":[{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Filter to only active alerts","default":false,"title":"Active Only"},"description":"Filter to only active alerts"},{"name":"include_expired","in":"query","required":false,"schema":{"type":"boolean","description":"Include alerts with past date ranges","default":true,"title":"Include Expired"},"description":"Include alerts with past date ranges"},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["alerts"],"summary":"Create Alert","description":"Create a new alert with multiple campground selections.\n\nCreates a campsite availability alert supporting multiple campgrounds\nwithin the same park, each with optional zone/site restrictions.\n\nUsers are limited to MAX_ALERTS_PER_USER active alerts.","operationId":"create_alert_api_v1_alerts_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/odds":{"post":{"tags":["alerts"],"summary":"Preview Alert Odds","description":"Preview how likely a draft alert is to ever fire.\n\nReturns a qualitative band (limited_data/low/moderate/good) + evidence\nnumbers + quantified improvement suggestions — never a probability %.\nRead-only; safe to call live as the wizard draft changes.","operationId":"preview_alert_odds_api_v1_alerts_odds_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertOddsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertOddsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/{alert_id}/odds":{"get":{"tags":["alerts"],"summary":"Get Existing Alert Odds","description":"Score an EXISTING alert's odds (alert-detail screen).\n\nSame engine as the draft preview (``POST /odds``), but the scoring scope is\nderived SERVER-SIDE from the stored alert + its campground selections — the\nsingle source of truth — rather than the client re-deriving the request and\nrisking drift (e.g. day-of-week filter, split-stay, the explicit-site set).\nRead-only.","operationId":"get_existing_alert_odds_api_v1_alerts__alert_id__odds_get","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertOddsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/hotspot":{"post":{"tags":["alerts"],"summary":"Create Hotspot Alert","description":"Create a hot spot watch alert.\n\nSubscribes the user to cancellation notifications on high-demand\nstats-tier sites. No campground selections needed — the system\nroutes stats-tier cancellations matching the user's date range\nand park filter automatically.\n\nRequires Regular tier or above.","operationId":"create_hotspot_alert_api_v1_alerts_hotspot_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HotSpotAlertCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/matches":{"get":{"tags":["alerts"],"summary":"List All Matches Summary","description":"Get active match counts across all alerts (for badge display).\n\nReturns a summary with active_matches count per alert.","operationId":"list_all_matches_summary_api_v1_alerts_matches_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertMatchesSummaryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/matches/{match_id}/seen":{"post":{"tags":["alerts"],"summary":"Mark Match Seen","description":"Mark a single match as seen.","operationId":"mark_match_seen_api_v1_alerts_matches__match_id__seen_post","parameters":[{"name":"match_id","in":"path","required":true,"schema":{"type":"string","title":"Match Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertMatchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/matches/{match_id}/clicked":{"post":{"tags":["alerts"],"summary":"Mark Match Clicked","description":"Record that the user tapped \"Book\" on a match (booking click-through).\n\nStamps ``clicked_at`` the first time only (idempotent — later taps don't\noverwrite). This is the last event we own before the user leaves for the\nprovider's reservation site, so comparing ``clicked_at`` against\n``booked_at`` reveals \"tapped Book but the spot was already gone\".\n\nThe mobile client fires this fire-and-forget right before opening the\nbooking URL, so it must stay cheap and must never block that redirect —\nan unknown/foreign match_id is treated as a no-op (``updated: 0``) rather\nthan an error.","operationId":"mark_match_clicked_api_v1_alerts_matches__match_id__clicked_post","parameters":[{"name":"match_id","in":"path","required":true,"schema":{"type":"string","title":"Match Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Mark Match Clicked Api V1 Alerts Matches  Match Id  Clicked Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/{alert_id}/matches":{"get":{"tags":["alerts"],"summary":"List Alert Matches","description":"Get active matches for a specific alert.","operationId":"list_alert_matches_api_v1_alerts__alert_id__matches_get","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertMatchListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/{alert_id}/matches/seen":{"post":{"tags":["alerts"],"summary":"Mark All Matches Seen","description":"Mark all active matches for an alert as seen.","operationId":"mark_all_matches_seen_api_v1_alerts__alert_id__matches_seen_post","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Mark All Matches Seen Api V1 Alerts  Alert Id  Matches Seen Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/{alert_id}":{"get":{"tags":["alerts"],"summary":"Get Alert","description":"Get a specific alert by ID.\n\nReturns the alert with campground_selections if it belongs to the current user.","operationId":"get_alert_api_v1_alerts__alert_id__get","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["alerts"],"summary":"Update Alert","description":"Update an existing alert.\n\nAllows partial updates to alert settings, including narrowing the\ncampground_selections (removing campgrounds/zones/sites).  Cannot\nexpand selections or change ``park_id`` — create a new alert.","operationId":"update_alert_api_v1_alerts__alert_id__patch","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["alerts"],"summary":"Delete Alert","description":"Delete an alert.\n\nSoft-deletes the alert: the row remains in the database with\n`deleted_at` set and `is_active=False` (so the polling scheduler and\nuser-facing reads skip it). Aggregate counters and AlertMatch history\nare preserved for the monthly value digest. The mobile API surface\nis unchanged — 204 on success, 404 if not found or already deleted.","operationId":"delete_alert_api_v1_alerts__alert_id__delete","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/{alert_id}/pause":{"post":{"tags":["alerts"],"summary":"Pause Alert","description":"Pause an alert.\n\nSets is_active to false, stopping monitoring without deleting the alert.","operationId":"pause_alert_api_v1_alerts__alert_id__pause_post","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/{alert_id}/resume":{"post":{"tags":["alerts"],"summary":"Resume Alert","description":"Resume a paused alert.\n\nSets is_active to true, restarting monitoring.\nSubject to MAX_ALERTS_PER_USER limit.","operationId":"resume_alert_api_v1_alerts__alert_id__resume_post","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/itinerary-routes":{"get":{"tags":["itineraries"],"summary":"List Curated Routes","description":"Curated routes for a park, ordered for display.\n\nReturns an EMPTY list rather than 404 when a park has no curated routes —\nthat is the normal state for most parks, and the wizard should fall through\nto \"customize your own\" instead of showing an error.\n\n``pets_only`` filters here rather than client-side because a route is only\nofferable when EVERY night passes: showing one whose night 3 forbids dogs\nwould create an alert that either fires on a zone the user excluded, or —\nif the matcher honoured the filter — could never fire at all.","operationId":"list_curated_routes_api_v1_itinerary_routes_get","parameters":[{"name":"park_id","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"title":"Park Id"}},{"name":"pets_only","in":"query","required":false,"schema":{"type":"boolean","description":"Hide routes containing a zone that is not pet-friendly. Applies the SAME rule as every other surface: unknown counts as no.","default":false,"title":"Pets Only"},"description":"Hide routes containing a zone that is not pet-friendly. Applies the SAME rule as every other surface: unknown counts as no."},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RouteCatalogResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/zone-adjacency":{"get":{"tags":["itineraries"],"summary":"Get Zone Adjacency","description":"Which zone may follow which, for one park's zones.\n\nDrives the custom-route picker: night N+1 offers only zones reachable from\nnight N's selection, so the route the user builds is bookable rather than\nbeing rejected at creation.\n\nThe ``constrained`` flag is what the client must branch on, NOT the\nemptiness of ``adjacency``:\n\n* ``constrained=false`` → this provider's graph has not been crawled, or\n  the provider has no sequence constraint at all.  Free-pick.  (For a\n  constrained provider that also means custom routes are refused at\n  creation — the wizard should offer curated routes only.)\n* ``constrained=true`` with an empty entry for a zone → that zone genuinely\n  leads nowhere; it is not a \"no data\" case.\n\nCollapsing those two would either block routes in parks that permit\neverything, or offer routes the provider will refuse.","operationId":"get_zone_adjacency_api_v1_zone_adjacency_get","parameters":[{"name":"park_id","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"title":"Park Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ZoneAdjacencyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks":{"get":{"tags":["parks"],"summary":"List Parks","description":"List all parks (legacy v1 — NCC excluded, camp_types filtered).\n\nReturns parks sorted alphabetically by name.\nSupports filtering by provider, camp type, and search query.\n\nThis endpoint is public and does not require authentication.","operationId":"list_parks_api_v1_parks_get","parameters":[{"name":"provider","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by provider (e.g., 'sepaq', 'ontario_parks')","title":"Provider"},"description":"Filter by provider (e.g., 'sepaq', 'ontario_parks')"},{"name":"camp_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by camp type (e.g., 'Camping', 'Chalet', 'Peche')","title":"Camp Type"},"description":"Filter by camp type (e.g., 'Camping', 'Chalet', 'Peche')"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":100},{"type":"null"}],"description":"Search parks by name","title":"Search"},"description":"Search parks by name"},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active parks","default":true,"title":"Active Only"},"description":"Only return active parks"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/campgrounds":{"get":{"tags":["parks"],"summary":"List All Campgrounds","description":"List all campgrounds across all parks.\n\nSupports filtering by park_id and search query.\nThis endpoint is public and does not require authentication.","operationId":"list_all_campgrounds_api_v1_parks_campgrounds_get","parameters":[{"name":"park_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by park ID","title":"Park Id"},"description":"Filter by park ID"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":100},{"type":"null"}],"description":"Search campgrounds by name (French or English)","title":"Search"},"description":"Search campgrounds by name (French or English)"},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active campgrounds","default":true,"title":"Active Only"},"description":"Only return active campgrounds"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampgroundListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/campgrounds/{campground_id}":{"get":{"tags":["parks"],"summary":"Get Campground","description":"Get a specific campground by ID.\n\nThis endpoint is public and does not require authentication.","operationId":"get_campground_api_v1_parks_campgrounds__campground_id__get","parameters":[{"name":"campground_id","in":"path","required":true,"schema":{"type":"string","title":"Campground Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampgroundResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/campgrounds/{campground_id}/maps":{"get":{"tags":["parks"],"summary":"Get Campground Maps","description":"Return the campground's site maps + per-site pixel coordinates.\n\nUsed by the mobile app's interactive site picker.  Capability-gated\non ``Platform.has_site_map`` (TRUE for GoingToCamp providers, FALSE\nfor SEPAQ).  Returns 404 for campgrounds whose platform does not\nexpose spatial data.","operationId":"get_campground_maps_api_v1_parks_campgrounds__campground_id__maps_get","parameters":[{"name":"campground_id","in":"path","required":true,"schema":{"type":"string","title":"Campground Id"}},{"name":"camp_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Camp Type"}},{"name":"include_activity_siblings","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Activity Siblings"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampgroundMapsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/campground-maps/{map_id}/image":{"get":{"tags":["parks"],"summary":"Get Campground Map Image","description":"Serve the cached campground-map PNG from our DB.\n\nSee get_campground_maps for why we proxy rather than handing the\nupstream URL straight to the client.  Bytes are fetched once at\nseed time and stored in CampgroundMap.image_bytes, so request-time\nhas no dependency on upstream availability or TLS trust state.\n\nPath uses ``:path`` because CampgroundMap PKs include a colon\n(``ontario_parks:-2147483121``).","operationId":"get_campground_map_image_api_v1_parks_campground_maps__map_id__image_get","parameters":[{"name":"map_id","in":"path","required":true,"schema":{"type":"string","title":"Map Id"}},{"name":"labels","in":"query","required":false,"schema":{"type":"boolean","description":"Draw each site's number onto the sheet at its stored pin. Providers publish these sheets WITHOUT numbers, so an unlabelled one cannot be checked against a recommendation.","default":false,"title":"Labels"},"description":"Draw each site's number onto the sheet at its stored pin. Providers publish these sheets WITHOUT numbers, so an unlabelled one cannot be checked against a recommendation."},{"name":"highlight","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated site ids to ring in red, the rest plain. Implies labels.","title":"Highlight"},"description":"Comma-separated site ids to ring in red, the rest plain. Implies labels."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/amenity-icons/{asset}/image":{"get":{"tags":["parks"],"summary":"Get Amenity Icon Image","description":"Serve a cached amenity legend-icon PNG.\n\nSame proxy rationale as the map image.  Referenced by\n``CampgroundMapItem.amenity_legend[].icon_url`` for providers whose\namenity icons are baked into the map artwork (Alberta), so the picker\ncan render a \"what the icons mean\" list.","operationId":"get_amenity_icon_image_api_v1_parks_amenity_icons__asset__image_get","parameters":[{"name":"asset","in":"path","required":true,"schema":{"type":"string","title":"Asset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/sites/{site_id}/image":{"get":{"tags":["parks"],"summary":"Get Site Image","description":"Serve a site's thumbnail image.\n\nReturns the image as JPEG with long-lived cache headers.\nThis endpoint is public (no auth) so CachedNetworkImage can fetch directly.","operationId":"get_site_image_api_v1_parks_sites__site_id__image_get","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/sites/{site_id}/reviews":{"get":{"tags":["parks"],"summary":"Get Site Reviews","description":"One site's guest reviews, verbatim, newest first by default.\n\nFor PERMITS this is a surface Recreation.gov does not have: their page\nlists a permit's reviews as one undifferentiated stream and publishes no\ndivision id, so a user comparing two entry points has a score and nothing\nto read behind it. Every review still names the place it was written\nabout, so the split exists in their data and is simply never shown.\n\nFor CAMPGROUNDS they already show reviews per campsite — which is why the\nharvester gets an exact ``campsite_id`` join there. This route is not\nuncovering anything on that surface, only putting it in the card.\n\nOffset paging rather than a keyset cursor: the deterministic ORDER BY makes\nit stable, the page is capped, and a review list is short enough that the\noffset never walks far. A keyset would need the sort key echoed back and\ngains nothing at this size.\n\nUnrated sites return an empty list, not a 404 — a site with no reviews is\nan ordinary answer, and the card asks before it knows.","operationId":"get_site_reviews_api_v1_parks_sites__site_id__reviews_get","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","description":"recent | rating_desc | rating_asc","default":"recent","title":"Sort"},"description":"recent | rating_desc | rating_asc"},{"name":"cursor","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Rows already shown","default":0,"title":"Cursor"},"description":"Rows already shown"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteReviewListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/{park_id}":{"get":{"tags":["parks"],"summary":"Get Park","description":"Get a specific park by ID (legacy v1 — NCC parks 404,\nnon-legacy camp_types stripped from response).\n\nThis endpoint is public and does not require authentication.","operationId":"get_park_api_v1_parks__park_id__get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/{park_id}/trails":{"get":{"tags":["parks"],"summary":"Get Park Trails","description":"The park's simplified trail network, as GeoJSON.\n\nDrawn UNDER a route's night pins so the trip can be read against the\ntrails it walks. The basemap renders OSM trails, which thin out in exactly\nthe backcountry these routes cross, leaving pins on blank green.\n\nReturns 404 rather than an empty collection when a park has no trails —\nthat is the normal state for most parks, and the client should draw\nnothing rather than an empty layer. Harvested and simplified offline (see\n``backfill_park_trails``), so this hands over stored bytes without\ntouching the geometry.\n\nUnauthenticated and heavily cacheable: it is static public-domain\ngeography, identical for every user, and the client caches it per park\nrather than refetching per route.","operationId":"get_park_trails_api_v1_parks__park_id__trails_get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/{park_id}/campgrounds":{"get":{"tags":["parks"],"summary":"List Park Campgrounds","description":"List a park's campgrounds (v1 — **frozen at the pre-Backcountry-Zone\nsurface**).  Excludes Parks Canada Backcountry-Zone (bcat=7)\ncampgrounds so shipped builds that predate the zone booking prefills\nnever render a dead-end card.  Current mobile calls\n``/api/v3/parks/{id}/campgrounds`` (returns them).  Public, no auth.","operationId":"list_park_campgrounds_api_v1_parks__park_id__campgrounds_get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active campgrounds","default":true,"title":"Active Only"},"description":"Only return active campgrounds"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampgroundListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/{park_id}/sites":{"get":{"tags":["parks"],"summary":"List Park Sites","description":"Every site in a park, in one call.\n\nThe same rows ``/campgrounds/{id}/sites`` returns, unioned across the\npark — for a caller that wants the whole park rather than one campground.\nThe wizard's activity step is the case that motivated it: it counts the\nsites matching an activity + the refine filters, which meant one request\nper campground, 78 of them on Deschutes NF, ~15s before Next could enable.\n\nWhy that collapses so well: a request costs ~0.18s fixed (warm connection)\nplus ~1.2ms per site, and the fixed part is the park lookup, the platform\nresolve and the hot-site batch — work that is IDENTICAL for every\ncampground in the park.  Doing it once turns 78 x 0.18s into 1 x 0.18s and\nleaves only the per-site cost, which the data volume forces either way.\n\nDeliberately NOT frozen by activity or container kind: callers intersect\nthese sites with the campground list from their own versioned campgrounds\nroute (``zone_campgrounds`` maps each zone to its campground), so a\ncampground that route hides stays hidden without this one having to know\nwhich freeze applies to which caller.","operationId":"list_park_sites_api_v1_parks__park_id__sites_get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active sites (and active zones/campgrounds)","default":true,"title":"Active Only"},"description":"Only return active sites (and active zones/campgrounds)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkSiteListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/zones/{zone_id}/day-use-window":{"get":{"tags":["parks"],"summary":"Get Zone Day Use Window","description":"The dates a day-use zone's time-slot grid should offer.\n\nDay use is seasonal and the season is published NOWHERE in the provider\nAPI — it is only visible in the availability grid, and it moves as the\nprovider releases next season.  So the grid asks for it per zone rather\nthan assuming a fixed number of columns.\n\n``max_span_days`` rides along so the client can cap a SELECTION without\ncapping the VIEW: the user browses the whole season but can only watch a\nwindow the platform's alert date-range cap allows.\n\n``closed_cells`` ({site_id: [\"YYYY-MM-DD\", ...]}) names the cells the grid\nmust not let the user tap because their booking window has ALREADY shut.\nDay use is the one activity whose inventory dies during its own arrival\nday — each slot carries its own cutoff — so \"not in the past\" is not\nenough to know a cell is live.  Computed here rather than shipped as raw\ncutoffs because deciding it needs the park's IANA timezone, and the app\ncarries no tz database (Bruce is America/Toronto, Banff America/Edmonton).\nRead-time, never stored, exactly like a match's ``booking_closed``.\n\nDegrades to a bare cap (``start``/``end`` null) whenever the window can't\nbe read — an unreachable provider must not empty the grid.","operationId":"get_zone_day_use_window_api_v1_parks_zones__zone_id__day_use_window_get","parameters":[{"name":"zone_id","in":"path","required":true,"schema":{"type":"string","title":"Zone Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Get Zone Day Use Window Api V1 Parks Zones  Zone Id  Day Use Window Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/zones/{zone_id}/sites":{"get":{"tags":["parks"],"summary":"List Zone Sites","description":"List all sites for a specific zone.\n\nThis endpoint is public and does not require authentication.","operationId":"list_zone_sites_api_v1_parks_zones__zone_id__sites_get","parameters":[{"name":"zone_id","in":"path","required":true,"schema":{"type":"string","title":"Zone Id"}},{"name":"accommodation_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by accommodation type","title":"Accommodation Type"},"description":"Filter by accommodation type"},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active sites","default":true,"title":"Active Only"},"description":"Only return active sites"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/campgrounds/{campground_id}/zones":{"get":{"tags":["parks"],"summary":"List Campground Zones","description":"List all zones for a specific campground.\n\nEach zone includes site_count and accommodation_types to help with\nzone selection in the alert wizard.\n\nThis endpoint is public and does not require authentication.","operationId":"list_campground_zones_api_v1_parks_campgrounds__campground_id__zones_get","parameters":[{"name":"campground_id","in":"path","required":true,"schema":{"type":"string","title":"Campground Id"}},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active zones","default":true,"title":"Active Only"},"description":"Only return active zones"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ZoneListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/campgrounds/{campground_id}/sites":{"get":{"tags":["parks"],"summary":"List Campground Sites","description":"List all sites for a specific campground (across all zones).\n\nThis endpoint is public and does not require authentication.","operationId":"list_campground_sites_api_v1_parks_campgrounds__campground_id__sites_get","parameters":[{"name":"campground_id","in":"path","required":true,"schema":{"type":"string","title":"Campground Id"}},{"name":"accommodation_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by accommodation type","title":"Accommodation Type"},"description":"Filter by accommodation type"},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active sites","default":true,"title":"Active Only"},"description":"Only return active sites"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/parks/equipment/options":{"get":{"tags":["parks"],"summary":"List Equipment Options","description":"Return equipment options for a specific GoingToCamp provider.\n\nUsed by the mobile app to populate the equipment dropdown in the\nbooking preferences card on the alert detail screen and the\ncreate-alert wizard's activity-type expansion.  Scopable by\n``equipment_category_id`` (parent namespace) and/or\n``internal_category_id`` (activity).  NS needs the activity\nfilter — Camping (6 subs incl. 4 RV brackets) and Backcountry /\nCanoe-Camping (3 subs each, tent-only) share the -32768 parent,\nso the activity dimension is the only way to scope the picker\ncorrectly.  Older callers that pass only ``equipment_category_id``\nkeep working (the activity filter is opt-in).","operationId":"list_equipment_options_api_v1_parks_equipment_options_get","parameters":[{"name":"provider","in":"query","required":true,"schema":{"type":"string","pattern":"^[a-z_]+$","description":"Provider id whose equipment options to return.  NOT a hardcoded whitelist (that would require a code edit per new provider — see the Provider-Driven Architecture rule in CLAUDE.md).  Any provider id is accepted and matched against the EquipmentOption rows; an unknown provider, or one without an equipment filter, simply yields an empty list.","title":"Provider"},"description":"Provider id whose equipment options to return.  NOT a hardcoded whitelist (that would require a code edit per new provider — see the Provider-Driven Architecture rule in CLAUDE.md).  Any provider id is accepted and matched against the EquipmentOption rows; an unknown provider, or one without an equipment filter, simply yields an empty list."},{"name":"lang","in":"query","required":false,"schema":{"type":"string","pattern":"^(en|fr|de|es|it|pt|ko|pl|el|ro|ht|tl|pa|hi|ru|zh|vi)$","description":"Language for labels","default":"fr","title":"Lang"},"description":"Language for labels"},{"name":"equipment_category_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Parent equipment category — -32768 is the frontcountry/Camping vocabulary every provider exposes; -32767 is PC's dedicated Backcountry vocabulary (1-6 Tents).  Optional — when omitted the response is scoped purely by ``internal_category_id`` (or every activity when that's omitted too).","title":"Equipment Category Id"},"description":"Parent equipment category — -32768 is the frontcountry/Camping vocabulary every provider exposes; -32767 is PC's dedicated Backcountry vocabulary (1-6 Tents).  Optional — when omitted the response is scoped purely by ``internal_category_id`` (or every activity when that's omitted too)."},{"name":"internal_category_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Seed-side activity id (0=Camping, 2=Chalet, 3=CanoeCamping, 4=Backcountry).  Scopes the dropdown to the subset the activity actually accepts.  Required for providers whose backcountry/canoe-camping share the camping parent eq cat (NS today) — without it the picker would surface camping-only RV options inside the wilderness pickers.","title":"Internal Category Id"},"description":"Seed-side activity id (0=Camping, 2=Chalet, 3=CanoeCamping, 4=Backcountry).  Scopes the dropdown to the subset the activity actually accepts.  Required for providers whose backcountry/canoe-camping share the camping parent eq cat (NS today) — without it the picker would surface camping-only RV options inside the wilderness pickers."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"},"title":"Response List Equipment Options Api V1 Parks Equipment Options Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v3/parks/{park_id}/campgrounds":{"get":{"tags":["parks-v3"],"summary":"List Park Campgrounds V3","description":"List a park's campgrounds (v3 — incl. Parks Canada day-use campgrounds).\n\nThe route the SHIPPED builds call (1.6.2 / 1.6.3).  It was cut from v2\nbecause a build with no Day Use activity files a \"Parking\"/\"Shuttle\"\ncampground under *Camping* — its camping name-heuristic predates the\nexclusion — and lands the user on an empty zone list.\n\n**Frozen at the pre-permit surface.**  The shipped builds predate the\nentry-date UI, so a Recreation.gov permit campground here is a dead end:\nits zone sells an ENTRY DATE against a daily quota, not a run of nights.\nPermits are on v4.  Public, no auth.","operationId":"list_park_campgrounds_v3_api_v3_parks__park_id__campgrounds_get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active campgrounds","default":true,"title":"Active Only"},"description":"Only return active campgrounds"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampgroundListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v4/parks/{park_id}/campgrounds":{"get":{"tags":["parks-v4"],"summary":"List Park Campgrounds V4","description":"List a park's campgrounds (v4 — incl. Recreation.gov wilderness\npermits).\n\nThe route the RELEASED 1.8.x builds call, paired with ``/api/v8/parks``.\nIt was cut from v3 rather than added as a flag because a permit zone sells\nan ENTRY DATE against a daily quota, and a build without that UI emits a\nnights-based alert that can never match.  Categories gated above v8\n(Ontario Parks DVP day use) stay off it.  Public, no auth.","operationId":"list_park_campgrounds_v4_api_v4_parks__park_id__campgrounds_get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active campgrounds","default":true,"title":"Active Only"},"description":"Only return active campgrounds"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampgroundListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v5/parks/{park_id}/campgrounds":{"get":{"tags":["parks-v5"],"summary":"List Park Campgrounds V5","description":"List a park's campgrounds (v5 — **full surface**, incl. Ontario Parks\nDaily Vehicle Permit day use).\n\nCurrent mobile master calls this, paired with ``/api/v9/parks``.  v4 plus\nevery booking category gated at v9 (``min_parks_api_version``).  Public,\nno auth.","operationId":"list_park_campgrounds_v5_api_v5_parks__park_id__campgrounds_get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active campgrounds","default":true,"title":"Active Only"},"description":"Only return active campgrounds"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampgroundListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v7/parks":{"get":{"tags":["parks-v7"],"summary":"List Parks","description":"List all parks (v7 — full surface incl. Florida).","operationId":"list_parks_api_v7_parks_get","parameters":[{"name":"provider","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by provider (e.g., 'sepaq', 'recreation_gov', 'california_parks', 'florida_parks')","title":"Provider"},"description":"Filter by provider (e.g., 'sepaq', 'recreation_gov', 'california_parks', 'florida_parks')"},{"name":"camp_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by camp type (e.g., 'Camping', 'Chalet', 'Backcountry', 'CanoeCamping')","title":"Camp Type"},"description":"Filter by camp type (e.g., 'Camping', 'Chalet', 'Backcountry', 'CanoeCamping')"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":100},{"type":"null"}],"description":"Search parks by name","title":"Search"},"description":"Search parks by name"},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active parks","default":true,"title":"Active Only"},"description":"Only return active parks"},{"name":"has_coords","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"When true, return only parks with non-null latitude AND longitude. Used by the mobile map to skip parks that have no pin location. Leave unset for the default (include all).","title":"Has Coords"},"description":"When true, return only parks with non-null latitude AND longitude. Used by the mobile map to skip parks that have no pin location. Leave unset for the default (include all)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v7/parks/{park_id}":{"get":{"tags":["parks-v7"],"summary":"Get Park","description":"Get a specific park by ID (v7 — full schema, no provider filter).","operationId":"get_park_api_v7_parks__park_id__get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v7/parks/{park_id}/map-tree":{"get":{"tags":["parks-v7"],"summary":"Get Park Map Tree","description":"Return every non-leaf MapNode in the park's drill-down tree.\n\nIdentical handler to ``v6/parks/{id}/map-tree`` — the map-tree response\nshape is provider-agnostic.  Re-exposed under v7 so the mobile build that\nuses v7 parks doesn't need a mixed-version flow.  (Florida uses the\ngeographic lat/lng site picker, not MapNodes, so this returns empty for it\ntoday — harmless and forward-compatible.)","operationId":"get_park_map_tree_api_v7_parks__park_id__map_tree_get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkMapTreeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v8/parks":{"get":{"tags":["parks-v8"],"summary":"List Parks","description":"List all parks (v8 — full surface incl. wilderness permits).","operationId":"list_parks_api_v8_parks_get","parameters":[{"name":"provider","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by provider (e.g., 'sepaq', 'recreation_gov', 'california_parks', 'florida_parks')","title":"Provider"},"description":"Filter by provider (e.g., 'sepaq', 'recreation_gov', 'california_parks', 'florida_parks')"},{"name":"camp_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by camp type (e.g., 'Camping', 'Chalet', 'Backcountry', 'CanoeCamping')","title":"Camp Type"},"description":"Filter by camp type (e.g., 'Camping', 'Chalet', 'Backcountry', 'CanoeCamping')"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":100},{"type":"null"}],"description":"Search parks by name","title":"Search"},"description":"Search parks by name"},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active parks","default":true,"title":"Active Only"},"description":"Only return active parks"},{"name":"has_coords","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"When true, return only parks with non-null latitude AND longitude. Used by the mobile map to skip parks that have no pin location. Leave unset for the default (include all).","title":"Has Coords"},"description":"When true, return only parks with non-null latitude AND longitude. Used by the mobile map to skip parks that have no pin location. Leave unset for the default (include all)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v8/parks/{park_id}":{"get":{"tags":["parks-v8"],"summary":"Get Park","description":"Get a specific park by ID (v8 — full schema, no freeze).","operationId":"get_park_api_v8_parks__park_id__get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v8/parks/{park_id}/map-tree":{"get":{"tags":["parks-v8"],"summary":"Get Park Map Tree","description":"Return every non-leaf MapNode in the park's drill-down tree.\n\nIdentical handler to ``v7/parks/{id}/map-tree`` — the map-tree response\nshape is provider-agnostic.  Re-exposed under v8 so the mobile build that\nuses v8 parks doesn't need a mixed-version flow.  (Florida uses the\ngeographic lat/lng site picker, not MapNodes, so this returns empty for it\ntoday — harmless and forward-compatible.)","operationId":"get_park_map_tree_api_v8_parks__park_id__map_tree_get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkMapTreeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v9/parks":{"get":{"tags":["parks-v9"],"summary":"List Parks","description":"List all parks (v9 — full surface incl. ACC huts).","operationId":"list_parks_api_v9_parks_get","parameters":[{"name":"provider","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by provider (e.g., 'sepaq', 'recreation_gov', 'california_parks', 'florida_parks', 'arizona_parks', 'nevada_parks', 'virginia_parks', 'minnesota_parks', 'acc_huts', 'cvhs_huts', 'spearhead_huts', 'accwhistler_huts', 'accvi_huts', 'rippleridge_huts', 'kmc_huts')","title":"Provider"},"description":"Filter by provider (e.g., 'sepaq', 'recreation_gov', 'california_parks', 'florida_parks', 'arizona_parks', 'nevada_parks', 'virginia_parks', 'minnesota_parks', 'acc_huts', 'cvhs_huts', 'spearhead_huts', 'accwhistler_huts', 'accvi_huts', 'rippleridge_huts', 'kmc_huts')"},{"name":"camp_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by camp type (e.g., 'Camping', 'Chalet', 'Backcountry', 'CanoeCamping')","title":"Camp Type"},"description":"Filter by camp type (e.g., 'Camping', 'Chalet', 'Backcountry', 'CanoeCamping')"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":100},{"type":"null"}],"description":"Search parks by name","title":"Search"},"description":"Search parks by name"},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active parks","default":true,"title":"Active Only"},"description":"Only return active parks"},{"name":"has_coords","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"When true, return only parks with non-null latitude AND longitude. Used by the mobile map to skip parks that have no pin location. Leave unset for the default (include all).","title":"Has Coords"},"description":"When true, return only parks with non-null latitude AND longitude. Used by the mobile map to skip parks that have no pin location. Leave unset for the default (include all)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v9/parks/{park_id}":{"get":{"tags":["parks-v9"],"summary":"Get Park","description":"Get a specific park by ID (v9 — full schema, no freeze).","operationId":"get_park_api_v9_parks__park_id__get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v9/parks/{park_id}/map-tree":{"get":{"tags":["parks-v9"],"summary":"Get Park Map Tree","description":"Return every non-leaf MapNode in the park's drill-down tree.\n\nIdentical handler to ``v8/parks/{id}/map-tree`` — the response shape is\nprovider-agnostic; re-exposed so a v9 build needs no mixed-version flow.","operationId":"get_park_map_tree_api_v9_parks__park_id__map_tree_get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkMapTreeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v10/parks":{"get":{"tags":["parks-v10"],"summary":"List Parks","description":"List all parks (v10 — full surface incl. the v10 UseDirect states).","operationId":"list_parks_api_v10_parks_get","parameters":[{"name":"provider","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by provider (e.g., 'sepaq', 'recreation_gov', 'california_parks', 'florida_parks', 'arizona_parks', 'nevada_parks', 'virginia_parks', 'minnesota_parks', 'wisconsin_parks', 'mississippi_parks', 'illinois_parks', 'ohio_parks', 'north_dakota_parks', 'missouri_parks', 'wyoming_parks', 'acc_huts', 'cvhs_huts', 'spearhead_huts', 'accwhistler_huts', 'accvi_huts', 'rippleridge_huts', 'kmc_huts')","title":"Provider"},"description":"Filter by provider (e.g., 'sepaq', 'recreation_gov', 'california_parks', 'florida_parks', 'arizona_parks', 'nevada_parks', 'virginia_parks', 'minnesota_parks', 'wisconsin_parks', 'mississippi_parks', 'illinois_parks', 'ohio_parks', 'north_dakota_parks', 'missouri_parks', 'wyoming_parks', 'acc_huts', 'cvhs_huts', 'spearhead_huts', 'accwhistler_huts', 'accvi_huts', 'rippleridge_huts', 'kmc_huts')"},{"name":"camp_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by camp type (e.g., 'Camping', 'Chalet', 'Backcountry', 'CanoeCamping')","title":"Camp Type"},"description":"Filter by camp type (e.g., 'Camping', 'Chalet', 'Backcountry', 'CanoeCamping')"},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":100},{"type":"null"}],"description":"Search parks by name","title":"Search"},"description":"Search parks by name"},{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active parks","default":true,"title":"Active Only"},"description":"Only return active parks"},{"name":"has_coords","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"When true, return only parks with non-null latitude AND longitude. Used by the mobile map to skip parks that have no pin location. Leave unset for the default (include all).","title":"Has Coords"},"description":"When true, return only parks with non-null latitude AND longitude. Used by the mobile map to skip parks that have no pin location. Leave unset for the default (include all)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v10/parks/{park_id}":{"get":{"tags":["parks-v10"],"summary":"Get Park","description":"Get a specific park by ID (v10 — full schema, no freeze).","operationId":"get_park_api_v10_parks__park_id__get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v10/parks/{park_id}/map-tree":{"get":{"tags":["parks-v10"],"summary":"Get Park Map Tree","description":"Return every non-leaf MapNode in the park's drill-down tree.\n\nIdentical handler to ``v9/parks/{id}/map-tree`` — the response shape is\nprovider-agnostic; re-exposed so a v10 build needs no mixed-version flow.","operationId":"get_park_map_tree_api_v10_parks__park_id__map_tree_get","parameters":[{"name":"park_id","in":"path","required":true,"schema":{"type":"string","title":"Park Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParkMapTreeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v4/platforms":{"get":{"tags":["platforms-v4"],"summary":"List Platforms","description":"List platforms (v4 — full surface incl. per-platform quota_bucket).","operationId":"list_platforms_api_v4_platforms_get","parameters":[{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return active platforms","default":true,"title":"Active Only"},"description":"Only return active platforms"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v4/platforms/{platform_id}/inject-script":{"get":{"tags":["platforms-v4"],"summary":"Get Inject Script","description":"The booking-page injection script for ONE platform.\n\nThe list route carries this field too, but it is the wrong way to get it\nat booking time.  ``webview_inject_script`` is ~25 KB and every\nGoingToCamp tenant stores the same copy, so the list payload is ~133 KB —\nseconds on a phone.  The script lives server-side precisely so a DOM fix\nships without an app release, which means the app must refetch before\ninjecting; refetching the whole registry to do that made the booking\nbutton appear dead, and bounding that wait just meant quietly injecting\nthe stale copy the fix was meant to replace.\n\nOne platform, one field: small enough to fetch on the tap that needs it.","operationId":"get_inject_script_api_v4_platforms__platform_id__inject_script_get","parameters":[{"name":"platform_id","in":"path","required":true,"schema":{"type":"string","title":"Platform Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]},"title":"Response Get Inject Script Api V4 Platforms  Platform Id  Inject Script Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/hot-sites/count":{"get":{"tags":["hot-sites-v2"],"summary":"Count Hot Sites","description":"Return the aggregate hot_sites count for the wizard's filters.","operationId":"count_hot_sites_api_v2_hot_sites_count_get","parameters":[{"name":"provider","in":"query","required":true,"schema":{"type":"string","description":"Provider id (e.g. bc_parks, ontario_parks, parks_canada).","title":"Provider"},"description":"Provider id (e.g. bc_parks, ontario_parks, parks_canada)."},{"name":"activity_type","in":"query","required":true,"schema":{"type":"string","description":"Activity bucket: camping | chalet | backcountry | canoe_camping | peche.","title":"Activity Type"},"description":"Activity bucket: camping | chalet | backcountry | canoe_camping | peche."},{"name":"park_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"When set, scope the count to a single park.","title":"Park Id"},"description":"When set, scope the count to a single park."},{"name":"equipment_mode","in":"query","required":false,"schema":{"type":"string","description":"any | tent | rv (matches the wizard's equipment filter).","default":"any","title":"Equipment Mode"},"description":"any | tent | rv (matches the wizard's equipment filter)."},{"name":"sub_equipment_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Required when equipment_mode='rv'.","title":"Sub Equipment Id"},"description":"Required when equipment_mode='rv'."},{"name":"require_electricity","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Require Electricity"}},{"name":"require_accessible","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Require Accessible"}},{"name":"require_pets","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Require Pets"}},{"name":"require_pull_through","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Require Pull Through"}},{"name":"require_tent_only","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Require Tent Only"}},{"name":"party_size","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"When set, drop sites whose ``max_occupancy`` is below this. Sites with NULL max_occupancy pass (capacity unknown) — same convention as ``site_passes_party_filter``.","title":"Party Size"},"description":"When set, drop sites whose ``max_occupancy`` is below this. Sites with NULL max_occupancy pass (capacity unknown) — same convention as ``site_passes_party_filter``."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HotSiteCountResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/map-nodes/{node_id}/image":{"get":{"tags":["map-nodes-v2"],"summary":"Get Map Node Image","description":"Serve the cached PNG for a single MapNode.","operationId":"get_map_node_image_api_v2_map_nodes__node_id__image_get","parameters":[{"name":"node_id","in":"path","required":true,"schema":{"type":"string","title":"Node Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v2/parks/campgrounds/{campground_id}/maps":{"get":{"tags":["parks-v2"],"summary":"Get Campground Maps V2","description":"Every pin, including both activities' rows for cross-listed inventory.\n\nPass ``camp_type`` to get one activity's rows — the ids that client's\nselection and alerts are keyed on.  Omit it to get the full set, which is\nwhat a browser showing every site on a map wants.","operationId":"get_campground_maps_v2_api_v2_parks_campgrounds__campground_id__maps_get","parameters":[{"name":"campground_id","in":"path","required":true,"schema":{"type":"string","title":"Campground Id"}},{"name":"camp_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Camp Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampgroundMapsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/subscriptions/me":{"get":{"tags":["subscriptions"],"summary":"Get My Subscription","description":"Get the current user's subscription details.\n\nCreates an essai subscription if the user doesn't have one.","operationId":"get_my_subscription_api_v1_subscriptions_me_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/subscriptions/status":{"get":{"tags":["subscriptions"],"summary":"Get Subscription Status","description":"Get subscription status and entitlements.\n\nIncludes tier limits and current usage for quota validation.","operationId":"get_subscription_status_api_v1_subscriptions_status_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/subscriptions/sync":{"post":{"tags":["subscriptions"],"summary":"Sync Subscription","description":"Reconcile subscription state with RevenueCat REST API.\n\nMobile calls this after Purchases.restorePurchases() so the backend\ntier matches RevenueCat's view immediately, without waiting for a\nwebhook (which is async and, for events like TRANSFER, may carry no\nactionable single-user payload).","operationId":"sync_subscription_api_v1_subscriptions_sync_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/subscriptions/plans":{"get":{"tags":["subscriptions"],"summary":"Get Subscription Plans","description":"Get available subscription plans with capacity info.\n\n- **v=2**: Returns full dynamic screen config (localized tiers, comparison\n  table, feature list, provider site overrides).\n- **No v param / v=1**: Returns legacy format (backwards compat for old app versions).\n\nPlans are purchased via native IAP through RevenueCat SDK.","operationId":"get_subscription_plans_api_v1_subscriptions_plans_get","parameters":[{"name":"v","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"API version: 2 for dynamic screen config","title":"V"},"description":"API version: 2 for dynamic screen config"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionPlansResponse"},{"$ref":"#/components/schemas/SubscriptionScreenConfigResponse"}],"title":"Response Get Subscription Plans Api V1 Subscriptions Plans Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/subscriptions/waitlist":{"post":{"tags":["subscriptions"],"summary":"Join Waitlist","description":"Join the waitlist for a tier that is at capacity.\n\nUses the authenticated user's email and ID.\nDuplicate (email, tier) pairs are handled gracefully.","operationId":"join_waitlist_api_v1_subscriptions_waitlist_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/subscriptions/dev/tier":{"put":{"tags":["subscriptions"],"summary":"Dev Set Tier","description":"Development endpoint to directly set subscription tier.\n\nOnly available in development environment.\nBypasses RevenueCat for testing tier-based features.","operationId":"dev_set_tier_api_v1_subscriptions_dev_tier_put","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevSetTierRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/agents/fund":{"post":{"tags":["mcp"],"summary":"Fund Agent","description":"Quote, or accept payment and fund a principal.","operationId":"fund_agent_api_v1_agents_fund_post","parameters":[{"name":"PAYMENT-SIGNATURE","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payment-Signature"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FundRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mcp":{"get":{"tags":["mcp"],"summary":"Mcp Get Not Allowed","description":"405 for GET — this revision is POST-only (no SSE stream to resume).","operationId":"mcp_get_not_allowed_mcp_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["mcp"],"summary":"Mcp Endpoint","description":"The single MCP endpoint (2026-07-28 Streamable HTTP, JSON mode).\n\nAuthenticated by an audience-bound MCP token (preferred) or a Firebase\nID token (so the app keeps working).  Either way the agent acts as its\nowner and inherits that user's tier and quotas unchanged — nothing new\nis sold, which is what keeps this clear of the app stores'\nin-app-purchase rules.\n\nAuth is resolved in the body rather than through ``Depends`` because an\nunauthenticated call must answer with the ``WWW-Authenticate`` challenge\nabove, not FastAPI's bare 401.","operationId":"mcp_endpoint_mcp_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"delete":{"tags":["mcp"],"summary":"Mcp Delete Not Allowed","description":"405 for DELETE — there is no session to terminate.","operationId":"mcp_delete_not_allowed_mcp_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/auth/mcp-tokens":{"post":{"tags":["mcp"],"summary":"Create Mcp Token","description":"Mint a credential for one agent.","operationId":"create_mcp_token_api_v1_auth_mcp_tokens_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPTokenCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPTokenCreated"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["mcp"],"summary":"List Mcp Tokens","description":"Every credential this user has issued, revoked ones included.\n\nRevoked rows are kept and returned: \"I revoked that last week\" is the\nquestion a user actually has, and hiding the row makes it unanswerable.","operationId":"list_mcp_tokens_api_v1_auth_mcp_tokens_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MCPTokenInfo"},"title":"Response List Mcp Tokens Api V1 Auth Mcp Tokens Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/mcp-tokens/{jti}":{"delete":{"tags":["mcp"],"summary":"Revoke Mcp Token","description":"Withdraw one credential. Takes effect on the next request.\n\nScoped to the caller's own tokens — the 404 for someone else's is\ndeliberate: confirming a jti exists would leak that it does.","operationId":"revoke_mcp_token_api_v1_auth_mcp_tokens__jti__delete","parameters":[{"name":"jti","in":"path","required":true,"schema":{"type":"string","title":"Jti"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/.well-known/oauth-protected-resource/{resource_path}":{"get":{"tags":["mcp"],"summary":"Protected Resource Metadata","description":"Tell a client where credentials for this resource come from.\n\nRequired of an MCP server by the 2026-07-28 spec.  ``resource`` is the\ncanonical identifier a token's ``aud`` must equal — publishing it is\nwhat lets a client ask its authorization server for a token scoped to\nUS rather than one that would work anywhere.\n\n``authorization_servers`` lists this service: AlertCamp issues its own\nMCP tokens today (see ``services/mcp_tokens.py``).  When the full\nOAuth 2.1 authorization-code + CIMD flow lands (plan §6.3 step 2) the\nentry stays the same — only the flow behind it changes.","operationId":"protected_resource_metadata__well_known_oauth_protected_resource__resource_path__get","parameters":[{"name":"resource_path","in":"path","required":true,"schema":{"type":"string","title":"Resource Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/.well-known/oauth-protected-resource":{"get":{"tags":["mcp"],"summary":"Protected Resource Metadata","description":"Tell a client where credentials for this resource come from.\n\nRequired of an MCP server by the 2026-07-28 spec.  ``resource`` is the\ncanonical identifier a token's ``aud`` must equal — publishing it is\nwhat lets a client ask its authorization server for a token scoped to\nUS rather than one that would work anywhere.\n\n``authorization_servers`` lists this service: AlertCamp issues its own\nMCP tokens today (see ``services/mcp_tokens.py``).  When the full\nOAuth 2.1 authorization-code + CIMD flow lands (plan §6.3 step 2) the\nentry stays the same — only the flow behind it changes.","operationId":"protected_resource_metadata__well_known_oauth_protected_resource_get","parameters":[{"name":"resource_path","in":"query","required":false,"schema":{"type":"string","default":"","title":"Resource Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/.well-known/openai-apps-challenge":{"get":{"tags":["mcp"],"summary":"Openai Apps Challenge","description":"Prove we control this host, for the ChatGPT app directory.\n\nThe token is served verbatim as plain text — OpenAI compares bytes, so\na JSON wrapper or a trailing newline is a failed verification with no\nuseful error.\n\nUnset answers **404**, not an empty 200.  A blank body that matches\nnothing reads as \"verification failed, cause unknown\"; a 404 says\nplainly that no challenge is configured on this deploy.\n\nNever cached: a stale token outlives the submission it belonged to,\nand re-verification would then compare against a value we have since\nrotated.","operationId":"openai_apps_challenge__well_known_openai_apps_challenge_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-authorization-server/{issuer_path}":{"get":{"tags":["mcp"],"summary":"Authorization Server Metadata","description":"What this server supports, so a client need not be told out of band.\n\nAlso answered at the RFC 8414 path-scoped location for the resource's\npath: a client that treats the resource URL as the issuer asks for\n``/.well-known/oauth-authorization-server/mcp``, and some stop at a 404\nrather than retrying the root form.","operationId":"authorization_server_metadata__well_known_oauth_authorization_server__issuer_path__get","parameters":[{"name":"issuer_path","in":"path","required":true,"schema":{"type":"string","title":"Issuer Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"tags":["mcp"],"summary":"Authorization Server Metadata","description":"What this server supports, so a client need not be told out of band.\n\nAlso answered at the RFC 8414 path-scoped location for the resource's\npath: a client that treats the resource URL as the issuer asks for\n``/.well-known/oauth-authorization-server/mcp``, and some stop at a 404\nrather than retrying the root form.","operationId":"authorization_server_metadata__well_known_oauth_authorization_server_get","parameters":[{"name":"issuer_path","in":"query","required":false,"schema":{"type":"string","default":"","title":"Issuer Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/register":{"post":{"tags":["mcp"],"summary":"Register","description":"Register a client that cannot present a metadata document.\n\nOpen to anyone, which is the point and the risk: nothing in the body is\nverified, so the user is later shown the redirect host, not the name.","operationId":"register_oauth_register_post","responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/clients/{name}.json":{"get":{"tags":["mcp"],"summary":"Hosted Client Metadata","description":"A client_id we issued — see ``HOSTED_CLIENTS`` in services/cimd.py.","operationId":"hosted_client_metadata_oauth_clients__name__json_get","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/authorize":{"get":{"tags":["mcp"],"summary":"Authorize Form","description":"Record the request and show its code.","operationId":"authorize_form_oauth_authorize_get","parameters":[{"name":"client_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}},{"name":"redirect_uri","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Redirect Uri"}},{"name":"response_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Response Type"}},{"name":"scope","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scope"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"code_challenge","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code Challenge"}},{"name":"code_challenge_method","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code Challenge Method"}},{"name":"resource","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resource"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/pending/{pending_id}":{"get":{"tags":["mcp"],"summary":"Poll Pending","description":"Where the waiting browser learns the outcome.\n\nKeyed on the long random ``id``, never the short user_code: polling is\nunauthenticated, so the handle it accepts must not be one a person\ncould guess from across the room.","operationId":"poll_pending_oauth_pending__pending_id__get","parameters":[{"name":"pending_id","in":"path","required":true,"schema":{"type":"string","title":"Pending Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/token":{"post":{"tags":["mcp"],"summary":"Token Endpoint","description":"Exchange an authorization code for an MCP access token.","operationId":"token_endpoint_oauth_token_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_token_endpoint_oauth_token_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/oauth-requests":{"get":{"tags":["mcp"],"summary":"Lookup Pending","description":"Resolve a code so the app can show WHAT is being approved.\n\nA 404 covers unknown, expired and already-answered alike — telling them\napart would let someone probe the code space for live requests.","operationId":"lookup_pending_api_v1_auth_oauth_requests_get","parameters":[{"name":"user_code","in":"query","required":true,"schema":{"type":"string","title":"User Code"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PendingRequestInfo"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/oauth-requests/approve":{"post":{"tags":["mcp"],"summary":"Approve Pending","description":"Grant the agent access, as this user.","operationId":"approve_pending_api_v1_auth_oauth_requests_approve_post","parameters":[{"name":"user_code","in":"query","required":true,"schema":{"type":"string","title":"User Code"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalResult"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/oauth-requests/deny":{"post":{"tags":["mcp"],"summary":"Deny Pending","description":"Refuse it, so the waiting page stops rather than timing out.","operationId":"deny_pending_api_v1_auth_oauth_requests_deny_post","parameters":[{"name":"user_code","in":"query","required":true,"schema":{"type":"string","title":"User Code"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalResult"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/webhooks/revenuecat":{"post":{"tags":["webhooks"],"summary":"Handle Revenuecat Webhook","description":"Handle RevenueCat webhook events.\n\nEvents processed:\n- INITIAL_PURCHASE: First subscription\n- RENEWAL: Subscription renewed\n- CANCELLATION: User canceled\n- UNCANCELLATION: User re-enabled renewal\n- EXPIRATION: Subscription expired\n- BILLING_ISSUE: Payment failed\n- PRODUCT_CHANGE: Plan changed\n- TEST: Webhook test\n\nReturns 200 to acknowledge receipt (RevenueCat will retry on non-2xx).","operationId":"handle_revenuecat_webhook_api_v1_webhooks_revenuecat_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Handle Revenuecat Webhook Api V1 Webhooks Revenuecat Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/feedback/":{"post":{"tags":["feedback"],"summary":"Submit Feedback","description":"Submit feedback (bug, feature request, improvement, or other).\n\nSaves the entry, then attempts to create a GitHub issue (best-effort).\nItems default to status='new' — admin must approve before they appear\non the public roadmap.","operationId":"submit_feedback_api_v1_feedback__post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/feedback/{feedback_id}/vote":{"post":{"tags":["feedback"],"summary":"Vote Feedback","description":"Cast a vote for a feedback entry. Idempotent — duplicate vote is a no-op.","operationId":"vote_feedback_api_v1_feedback__feedback_id__vote_post","parameters":[{"name":"feedback_id","in":"path","required":true,"schema":{"type":"string","title":"Feedback Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["feedback"],"summary":"Unvote Feedback","description":"Remove the current user's vote. Idempotent — no-op if not voted.","operationId":"unvote_feedback_api_v1_feedback__feedback_id__vote_delete","parameters":[{"name":"feedback_id","in":"path","required":true,"schema":{"type":"string","title":"Feedback Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/feedback/public/feature-requests":{"get":{"tags":["feedback"],"summary":"List Public Feature Requests","description":"Public, read-only roadmap.\n\nReturns admin-approved feature requests with aggregate vote counts.\nNo authentication required — powers the static `alert.camp/roadmap` page.\nIf a user IS authenticated, each item also reports `has_voted`.","operationId":"list_public_feature_requests_api_v1_feedback_public_feature_requests_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FeatureRequestPublic"},"title":"Response List Public Feature Requests Api V1 Feedback Public Feature Requests Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/stats/public":{"get":{"tags":["stats"],"summary":"Get Public Stats","description":"Public, unauthenticated marketing stats for the website.\n\nFigures come from ``compute_public_stats`` (see its docstring for what\nis counted). Cached for ``_PUBLIC_STATS_TTL_SECONDS``.","operationId":"get_public_stats_api_v1_stats_public_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicStatsResponse"}}}}}}},"/api/v1/stats/cancellations":{"get":{"tags":["stats"],"summary":"Get Cancellation Stats","description":"Get aggregate cancellation statistics.\n\nReturns overview metrics, per-park breakdown, day-of-week distribution,\nand lead time buckets for the specified rolling window.","operationId":"get_cancellation_stats_api_v1_stats_cancellations_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"Rolling window in days","default":30,"title":"Days"},"description":"Rolling window in days"},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancellationStatsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tracking/":{"post":{"tags":["tracking"],"summary":"Record Tracking Event","description":"Record a tracking event (QR scan, campaign redirect, etc.).\n\nPublic endpoint — no authentication required.\nExtracts CF-IPCountry header when available (Cloudflare sends it for free).","operationId":"record_tracking_event_api_v1_tracking__post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackingCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackingResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/events":{"post":{"tags":["events"],"summary":"Ingest Events","description":"Store a batch of client analytics events.","operationId":"ingest_events_api_v1_events_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-dev-user-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Dev-User-Id"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppEventBatch"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppEventBatchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/b/{match_id}":{"get":{"tags":["booking-redirect"],"summary":"Booking Redirect","description":"Resolve a branded booking short link → provider booking URL.\n\nRecords click tracking (best-effort) then 302-redirects. Tracking failures\nnever block the redirect, and an unknown match still redirects (to the\nfallback) so a stale link is never a dead end.","operationId":"booking_redirect_api_v1_b__match_id__get","parameters":[{"name":"match_id","in":"path","required":true,"schema":{"type":"string","title":"Match Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/booking/permit-inject":{"get":{"tags":["booking"],"summary":"Get Permit Inject","description":"The injection contract for the permit this site belongs to.\n\n``{\"permit_id\": int, \"recipe\": {...}, \"script\": \"...\"}`` when the site is a\npermit division with a measured recipe; ``recipe`` and ``script`` are NULL\nfor every other site, which is the signal to fall back to the provider's\nown script (the campground calendar) rather than inject nothing.","operationId":"get_permit_inject_api_v1_booking_permit_inject_get","parameters":[{"name":"site_id","in":"query","required":true,"schema":{"type":"string","description":"Any site in the permit (a route uses night 1)","title":"Site Id"},"description":"Any site in the permit (a route uses night 1)"},{"name":"site_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated site ids (a route's nights, in order) to resolve starting areas for","title":"Site Ids"},"description":"Comma-separated site ids (a route's nights, in order) to resolve starting areas for"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Get Permit Inject Api V1 Booking Permit Inject Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/waitlist/":{"post":{"tags":["waitlist"],"summary":"Join Waitlist","description":"Join the provider waitlist.\n\nPublic endpoint — no authentication required. Idempotent: re-submitting\nthe same (email, provider) pair returns the existing entry instead of\ncreating a duplicate row.","operationId":"join_waitlist_api_v1_waitlist__post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistJoinRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistJoinResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/waitlist":{"post":{"tags":["waitlist"],"summary":"Join Waitlist","description":"Join the provider waitlist.\n\nPublic endpoint — no authentication required. Idempotent: re-submitting\nthe same (email, provider) pair returns the existing entry instead of\ncreating a duplicate row.","operationId":"join_waitlist_api_v1_waitlist_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistJoinRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistJoinResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/login":{"get":{"tags":["admin"],"summary":"Login Page","description":"Render admin login form.","operationId":"login_page_admin_login_get","parameters":[{"name":"error","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["admin"],"summary":"Login Submit","description":"Verify admin token and create session.","operationId":"login_submit_admin_login_post","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_login_submit_admin_login_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/logout":{"get":{"tags":["admin"],"summary":"Logout","description":"Clear admin session.","operationId":"logout_admin_logout_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/":{"get":{"tags":["admin"],"summary":"Overview","description":"Dashboard overview with key metrics.","operationId":"overview_admin__get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/users":{"get":{"tags":["admin"],"summary":"Users List","description":"List users with search, tier, account type, verification, and auto-paused filter.","operationId":"users_list_admin_users_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}},{"name":"tier","in":"query","required":false,"schema":{"type":"string","default":"","title":"Tier"}},{"name":"account","in":"query","required":false,"schema":{"type":"string","default":"","title":"Account"}},{"name":"verified","in":"query","required":false,"schema":{"type":"string","default":"","title":"Verified"}},{"name":"auto_paused","in":"query","required":false,"schema":{"type":"string","default":"","title":"Auto Paused"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}":{"get":{"tags":["admin"],"summary":"User Detail","description":"Show user detail with subscription, alerts, and recent matches.","operationId":"user_detail_admin_users__user_id__get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}},{"name":"ev_start","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ev Start"}},{"name":"ev_end","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ev End"}},{"name":"ev_event","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"default":[],"title":"Ev Event"}},{"name":"ev_history","in":"query","required":false,"schema":{"type":"string","default":"1","title":"Ev History"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/change-tier":{"post":{"tags":["admin"],"summary":"Change Tier","description":"Change a user's subscription tier.","operationId":"change_tier_admin_users__user_id__change_tier_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_change_tier_admin_users__user_id__change_tier_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/toggle-active":{"post":{"tags":["admin"],"summary":"Toggle Active","description":"Toggle a user's is_active flag.","operationId":"toggle_active_admin_users__user_id__toggle_active_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_toggle_active_admin_users__user_id__toggle_active_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/toggle-alerts-paused":{"post":{"tags":["admin"],"summary":"Toggle Alerts Paused","description":"Toggle a user's alerts_paused flag.","operationId":"toggle_alerts_paused_admin_users__user_id__toggle_alerts_paused_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_toggle_alerts_paused_admin_users__user_id__toggle_alerts_paused_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/unpause":{"post":{"tags":["admin"],"summary":"Unpause","description":"Clear the system-set auto_paused_at stamp on a user.","operationId":"unpause_admin_users__user_id__unpause_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_unpause_admin_users__user_id__unpause_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/verify-email":{"post":{"tags":["admin"],"summary":"Verify Email","description":"Manually mark a user's email as verified.\n\nUsed when the verification email is undeliverable (e.g. Yahoo/Hotmail/iCloud\ndeferring or silently dropping mail). PostgreSQL is the source of truth, but\nthe mobile app gates the verify screen on the *Firebase* email_verified\nclaim — so we ALWAYS (re-)push to Firebase, even when our column is already\nTrue. The two can drift (a prior mirror may have failed silently), and only\nre-pushing repairs an account that's verified in the DB yet still blocked in\nthe app. Mirror failures are surfaced to the admin, not swallowed.","operationId":"verify_email_admin_users__user_id__verify_email_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_verify_email_admin_users__user_id__verify_email_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/delete":{"post":{"tags":["admin"],"summary":"Delete User","description":"Permanently delete a user and all related data.\n\nCascades remove alerts, alert_matches, subscriptions, devices, feedback,\nand feedback_votes via FK ON DELETE CASCADE. Firebase Auth must be\ncleaned up separately. Requires the admin to retype the user's email\nas a typo guard.","operationId":"delete_user_admin_users__user_id__delete_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_delete_user_admin_users__user_id__delete_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/alerts":{"get":{"tags":["admin"],"summary":"Alerts List","description":"List all alerts with optional active filter.","operationId":"alerts_list_admin_alerts_get","parameters":[{"name":"active_only","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Active Only"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/alerts/{alert_id}":{"get":{"tags":["admin"],"summary":"Alert Detail","description":"Show alert detail with config, watched sites, and match history.","operationId":"alert_detail_admin_alerts__alert_id__get","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/alerts/{alert_id}/boost":{"post":{"tags":["admin"],"summary":"Boost Alert","description":"Activate an admin scan-boost on a single alert.\n\nInternal lever (not user-facing): drops the alert's poll cadence to\n``interval_seconds`` (clamped 30–45s) until ``until`` (end of that UTC day).\nRejected unless every provider the alert touches is boost-eligible\n(``Platform.allows_scan_boost`` — GoingToCamp + Recreation.gov only).","operationId":"boost_alert_admin_alerts__alert_id__boost_post","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_boost_alert_admin_alerts__alert_id__boost_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/alerts/{alert_id}/clear-boost":{"post":{"tags":["admin"],"summary":"Clear Boost Alert","description":"Clear an admin scan-boost, reverting the alert to its tier cadence.","operationId":"clear_boost_alert_admin_alerts__alert_id__clear_boost_post","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"string","title":"Alert Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_clear_boost_alert_admin_alerts__alert_id__clear_boost_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/itineraries":{"get":{"tags":["admin"],"summary":"Itineraries List","description":"List route (itinerary) parents — the rows the alert list cannot show.\n\n``alert_itineraries`` parents are NOT ``alerts`` rows, so they are absent\nfrom every alert view; only their legs surface there.  This is the one\nplace a route is visible as the single thing the user created.","operationId":"itineraries_list_admin_itineraries_get","parameters":[{"name":"active_only","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Active Only"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/itineraries/{itinerary_id}":{"get":{"tags":["admin"],"summary":"Itinerary Detail","description":"Route parent detail: its legs, its openings, and the route-wide boost.\n\nThe three things an alert page structurally cannot show for a route:\nthe parent's own window/sweep state, the legs as an ordered set rather\nthan N unrelated alerts, and the ``itinerary_openings`` rows that are the\nroute's real output (legs persist ``AlertMatch`` rows, but a leg match is\nnot a route opening — the aggregator emits for the parent).","operationId":"itinerary_detail_admin_itineraries__itinerary_id__get","parameters":[{"name":"itinerary_id","in":"path","required":true,"schema":{"type":"string","title":"Itinerary Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/itineraries/{itinerary_id}/boost":{"post":{"tags":["admin"],"summary":"Boost Itinerary","description":"Activate a scan-boost across EVERY leg of a route, atomically.\n\nBoosting a route one leg at a time is not a slower version of this — it\nis useless.  The aggregator is level-triggered and fires only when all N\nnights stand open simultaneously, so the route's detection latency equals\nthat of its slowest-polling leg.  Speeding up leg 2 while legs 1 and 3\nstay at tier cadence leaves the route exactly as slow as before, at 3x\nthe poll cost of doing nothing.  Hence one lever for the whole route.\n\nSame 30–45s band and provider gate as the per-alert boost: rejected\nunless every provider the route's legs touch is boost-eligible\n(``Platform.allows_scan_boost``).","operationId":"boost_itinerary_admin_itineraries__itinerary_id__boost_post","parameters":[{"name":"itinerary_id","in":"path","required":true,"schema":{"type":"string","title":"Itinerary Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_boost_itinerary_admin_itineraries__itinerary_id__boost_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/itineraries/{itinerary_id}/clear-boost":{"post":{"tags":["admin"],"summary":"Clear Boost Itinerary","description":"Clear the scan-boost on every leg of a route.\n\nClears unconditionally rather than only where a boost is live, so a route\nleft in the ``partial`` state (a leg boosted individually from the alert\npage, or a leg added after the route was boosted) reverts cleanly to tier\ncadence in one action.","operationId":"clear_boost_itinerary_admin_itineraries__itinerary_id__clear_boost_post","parameters":[{"name":"itinerary_id","in":"path","required":true,"schema":{"type":"string","title":"Itinerary Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_clear_boost_itinerary_admin_itineraries__itinerary_id__clear_boost_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/polling":{"get":{"tags":["admin"],"summary":"Polling Dashboard","description":"Live polling dashboard showing cluster health, stats, and circuit breaker.","operationId":"polling_dashboard_admin_polling_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/cancellations":{"get":{"tags":["admin"],"summary":"Cancellations Feed","description":"List recent cancellation events, grouped by detection batch.","operationId":"cancellations_feed_admin_cancellations_get","parameters":[{"name":"park","in":"query","required":false,"schema":{"type":"string","default":"","title":"Park"}},{"name":"provider","in":"query","required":false,"schema":{"type":"string","default":"","title":"Provider"}},{"name":"open_only","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Open Only"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/notifications":{"get":{"tags":["admin"],"summary":"Notifications Log","description":"List recent alert matches (notification log).","operationId":"notifications_log_admin_notifications_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}},{"name":"park","in":"query","required":false,"schema":{"type":"string","default":"","title":"Park"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/announcements":{"get":{"tags":["admin"],"summary":"Announcements Log","description":"Every push sent by ``app/scripts/send_push_announcement.py``.\n\nTwo views of the same table, because they answer different questions.\nThe summary answers \"what have we ever sent, and did it land\"; the row\nlist answers \"what exactly did this camper see\".  Both read\n``push_campaign_sends``, which is written by nothing else — the alert\npipeline's notifications live on the Notifications page.\n\nNote ``delivered`` is devices FCM ACCEPTED, and the row is written before\nthe push is attempted.  ``0 / 2`` therefore means claimed but nothing\narrived, not a silent success.","operationId":"announcements_log_admin_announcements_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}},{"name":"campaign","in":"query","required":false,"schema":{"type":"string","default":"","title":"Campaign"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/feedback":{"get":{"tags":["admin"],"summary":"Feedback List","description":"List all feedback entries.","operationId":"feedback_list_admin_feedback_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","default":"","title":"Status"}},{"name":"category","in":"query","required":false,"schema":{"type":"string","default":"","title":"Category"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/feedback/{feedback_id}":{"get":{"tags":["admin"],"summary":"Feedback Detail","description":"Show feedback detail with admin actions.","operationId":"feedback_detail_admin_feedback__feedback_id__get","parameters":[{"name":"feedback_id","in":"path","required":true,"schema":{"type":"string","title":"Feedback Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/feedback/{feedback_id}/update-status":{"post":{"tags":["admin"],"summary":"Feedback Update Status","description":"Update feedback status.","operationId":"feedback_update_status_admin_feedback__feedback_id__update_status_post","parameters":[{"name":"feedback_id","in":"path","required":true,"schema":{"type":"string","title":"Feedback Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_feedback_update_status_admin_feedback__feedback_id__update_status_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/feedback/{feedback_id}/update-notes":{"post":{"tags":["admin"],"summary":"Feedback Update Notes","description":"Save admin notes on feedback.","operationId":"feedback_update_notes_admin_feedback__feedback_id__update_notes_post","parameters":[{"name":"feedback_id","in":"path","required":true,"schema":{"type":"string","title":"Feedback Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_feedback_update_notes_admin_feedback__feedback_id__update_notes_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/feedback/{feedback_id}/update-github":{"post":{"tags":["admin"],"summary":"Feedback Update Github","description":"Save GitHub issue URL on feedback.","operationId":"feedback_update_github_admin_feedback__feedback_id__update_github_post","parameters":[{"name":"feedback_id","in":"path","required":true,"schema":{"type":"string","title":"Feedback Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_feedback_update_github_admin_feedback__feedback_id__update_github_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/feedback/{feedback_id}/update-description":{"post":{"tags":["admin"],"summary":"Feedback Update Description","description":"Edit the user-submitted description (e.g. translate, clean up wording).\n\nUseful when promoting a feature_request to the public roadmap — admins\noften need to normalize phrasing or translate the original into English\nbefore it goes live.","operationId":"feedback_update_description_admin_feedback__feedback_id__update_description_post","parameters":[{"name":"feedback_id","in":"path","required":true,"schema":{"type":"string","title":"Feedback Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_feedback_update_description_admin_feedback__feedback_id__update_description_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/feedback/{feedback_id}/delete":{"post":{"tags":["admin"],"summary":"Feedback Delete","description":"Permanently delete a feedback entry.\n\nCascades to feedback_votes via the FK ON DELETE CASCADE on the votes\ntable. Used for cleaning up spam, test submissions, or duplicates.","operationId":"feedback_delete_admin_feedback__feedback_id__delete_post","parameters":[{"name":"feedback_id","in":"path","required":true,"schema":{"type":"string","title":"Feedback Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_feedback_delete_admin_feedback__feedback_id__delete_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/fetches":{"get":{"tags":["admin"],"summary":"Fetches Dashboard","description":"List recent per-site fetch events with filters and dedup check.","operationId":"fetches_dashboard_admin_fetches_get","parameters":[{"name":"node","in":"query","required":false,"schema":{"type":"string","default":"","title":"Node"}},{"name":"tier","in":"query","required":false,"schema":{"type":"string","default":"","title":"Tier"}},{"name":"success","in":"query","required":false,"schema":{"type":"string","default":"","title":"Success"}},{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Hours"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/waitlist":{"get":{"tags":["admin"],"summary":"Waitlist Page","description":"List waitlist entries with tier filter and email search.","operationId":"waitlist_page_admin_waitlist_get","parameters":[{"name":"tier","in":"query","required":false,"schema":{"type":"string","default":"","title":"Tier"}},{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/waitlist/{entry_id}/delete":{"post":{"tags":["admin"],"summary":"Waitlist Delete","description":"Remove a waitlist entry.","operationId":"waitlist_delete_admin_waitlist__entry_id__delete_post","parameters":[{"name":"entry_id","in":"path","required":true,"schema":{"type":"integer","title":"Entry Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_waitlist_delete_admin_waitlist__entry_id__delete_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/sepaq-waitlist":{"get":{"tags":["admin"],"summary":"Sepaq Waitlist Page","description":"List provider waitlist entries (SEPAQ launch) with provider filter and email search.","operationId":"sepaq_waitlist_page_admin_sepaq_waitlist_get","parameters":[{"name":"provider","in":"query","required":false,"schema":{"type":"string","default":"","title":"Provider"}},{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/sepaq-waitlist/{entry_id}/delete":{"post":{"tags":["admin"],"summary":"Sepaq Waitlist Delete","description":"Remove a provider waitlist entry.","operationId":"sepaq_waitlist_delete_admin_sepaq_waitlist__entry_id__delete_post","parameters":[{"name":"entry_id","in":"path","required":true,"schema":{"type":"integer","title":"Entry Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_sepaq_waitlist_delete_admin_sepaq_waitlist__entry_id__delete_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/webhooks":{"get":{"tags":["admin"],"summary":"Webhooks Log","description":"List webhook events with filters.","operationId":"webhooks_log_admin_webhooks_get","parameters":[{"name":"event_type","in":"query","required":false,"schema":{"type":"string","default":"","title":"Event Type"}},{"name":"result","in":"query","required":false,"schema":{"type":"string","default":"","title":"Result"}},{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/workers":{"get":{"tags":["admin"],"summary":"Workers Dashboard","description":"Workers dashboard shell — data loaded via JS auto-refresh.","operationId":"workers_dashboard_admin_workers_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/api/workers":{"get":{"tags":["admin"],"summary":"Workers Api","description":"JSON summary: one entry per worker with claimed counts from poll_tracker.","operationId":"workers_api_admin_api_workers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/competitors":{"get":{"tags":["admin"],"summary":"Competitors Dashboard","description":"Competitor public counters (Schnerp, Campnab) vs ours, weekly.","operationId":"competitors_dashboard_admin_competitors_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/api/competitors":{"get":{"tags":["admin"],"summary":"Competitors Api","description":"JSON: every competitor_metric_snapshots row, oldest first.\n\nWritten weekly by ``app.scripts.snapshot_competitor_counters``; ~9 rows a\nweek, so the full history is returned and the page derives deltas/share.","operationId":"competitors_api_admin_api_competitors_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/egress":{"get":{"tags":["admin"],"summary":"Egress Dashboard","description":"Bright Data US ISP egress pool — health snapshot (JS auto-refresh).","operationId":"egress_dashboard_admin_egress_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/api/egress":{"get":{"tags":["admin"],"summary":"Egress Api","description":"JSON snapshot of the shared egress pool: per-IP health + leader + spend.\n\nReads the cross-node ``egress_ip_health`` + ``egress_refresher_lease`` tables\n(written by the egress VMs, decisions made by the leased leader), so this\nfleet-wide view needs no per-node calls. Counts are windowed.","operationId":"egress_api_admin_api_egress_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/workers/{node_name}/claims":{"get":{"tags":["admin"],"summary":"Worker Claims Api","description":"JSON drill-down: sites currently claimed by a specific worker.","operationId":"worker_claims_api_admin_api_workers__node_name__claims_get","parameters":[{"name":"node_name","in":"path","required":true,"schema":{"type":"string","title":"Node Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/overdue-sites":{"get":{"tags":["admin"],"summary":"Overdue Sites Api","description":"JSON list of user-monitored sites that are overdue for polling (excludes stats, op_stats).\n\nHandles two row shapes in poll_tracker:\n  - Per-site rows (SEPAQ): site_id matches sites.id directly.\n  - Zone-level synthetic rows (GoingToCamp user tiers + op_stats):\n    site_id has the shape ``zone:<zone_id>:<equip>:<bcat>:<nights>``\n    — extract the zone_id from segment 2 to resolve park/zone names.","operationId":"overdue_sites_api_admin_api_overdue_sites_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/overdue-history":{"get":{"tags":["admin"],"summary":"Overdue History Api","description":"JSON list of overdue incidents with optional filters.","operationId":"overdue_history_api_admin_api_overdue_history_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Days"}},{"name":"tier","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tier"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","default":"all","title":"Status"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/capacity":{"get":{"tags":["admin"],"summary":"Capacity Api","description":"JSON summary of tier capacity and availability.","operationId":"capacity_api_admin_api_capacity_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/admin/api/waitlist":{"get":{"tags":["admin"],"summary":"Waitlist Api","description":"JSON list of waitlist entries, optionally filtered by tier.","operationId":"waitlist_api_admin_api_waitlist_get","parameters":[{"name":"tier","in":"query","required":false,"schema":{"type":"string","default":"","title":"Tier"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/poll-metrics":{"get":{"tags":["admin"],"summary":"Poll Metrics Api","description":"JSON array of poll metrics for the last N hours (max 48).","operationId":"poll_metrics_api_admin_api_poll_metrics_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","default":48,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/idle-events":{"get":{"tags":["admin"],"summary":"Idle Events Api","description":"JSON array of idle events for the last N hours (max 48).","operationId":"idle_events_api_admin_api_idle_events_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","default":48,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/maintenance-events":{"get":{"tags":["admin"],"summary":"Maintenance Events Api","description":"JSON: maintenance events + prior state for chart reconstruction.","operationId":"maintenance_events_api_admin_api_maintenance_events_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","default":48,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/stress-events":{"get":{"tags":["admin"],"summary":"Stress Events Api","description":"JSON: stress events + prior state for chart reconstruction.","operationId":"stress_events_api_admin_api_stress_events_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","default":48,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/degraded-events":{"get":{"tags":["admin"],"summary":"Degraded Events Api","description":"JSON: degraded events + prior state for chart reconstruction.\n\n\"Degraded\" is the user-visible health status (stricter than stress mode):\na (provider, tier) sustained past the stress threshold long enough AND\nwith a site overdue beyond the tier's max scan window. One row per\nENTER/EXIT transition, written by HealthChecker._check_cluster.","operationId":"degraded_events_api_admin_api_degraded_events_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","default":48,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/exclusions":{"get":{"tags":["admin"],"summary":"Exclusions List","description":"List site exclusion rules with optional search.","operationId":"exclusions_list_admin_exclusions_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/exclusions/add":{"post":{"tags":["admin"],"summary":"Exclusion Add","description":"Create a new site exclusion rule.\n\nAuto-expands to sibling sites that share the same canonical URL path\n(e.g. chalet ↔ peche for the same physical unit).","operationId":"exclusion_add_admin_exclusions_add_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_exclusion_add_admin_exclusions_add_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/exclusions/{exclusion_id}/delete":{"post":{"tags":["admin"],"summary":"Exclusion Delete","description":"Remove a site exclusion rule and its sibling exclusions.\n\nSiblings are identified via canonical URL path and must share the\nsame date_start/date_end to be removed together.","operationId":"exclusion_delete_admin_exclusions__exclusion_id__delete_post","parameters":[{"name":"exclusion_id","in":"path","required":true,"schema":{"type":"integer","title":"Exclusion Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_exclusion_delete_admin_exclusions__exclusion_id__delete_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/tracking":{"get":{"tags":["admin"],"summary":"Tracking Log","description":"List tracking events (QR scans, campaign redirects) with filters and charts.","operationId":"tracking_log_admin_tracking_get","parameters":[{"name":"source","in":"query","required":false,"schema":{"type":"string","default":"","title":"Source"}},{"name":"platform","in":"query","required":false,"schema":{"type":"string","default":"","title":"Platform"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/api/users/{user_id}/timeline":{"get":{"tags":["admin"],"summary":"User Timeline Api","description":"JSON for the user page's DevTools-style timeline (lanes per session,\nbar durations).  No from/to = the 24h ending at the latest activity.","operationId":"user_timeline_api_admin_api_users__user_id__timeline_get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}},{"name":"ev_start","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ev Start"}},{"name":"ev_end","in":"query","required":false,"schema":{"type":"string","default":"","title":"Ev End"}},{"name":"ev_event","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"default":[],"title":"Ev Event"}},{"name":"ev_history","in":"query","required":false,"schema":{"type":"string","default":"1","title":"Ev History"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/events":{"get":{"tags":["admin"],"summary":"Events Page","description":"Searchable log of analytics events across all users (UTC).","operationId":"events_page_admin_events_get","parameters":[{"name":"start","in":"query","required":false,"schema":{"type":"string","default":"","title":"Start"}},{"name":"end","in":"query","required":false,"schema":{"type":"string","default":"","title":"End"}},{"name":"email","in":"query","required":false,"schema":{"type":"string","default":"","title":"Email"}},{"name":"event","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"default":[],"title":"Event"}},{"name":"source","in":"query","required":false,"schema":{"type":"string","default":"","title":"Source"}},{"name":"platform","in":"query","required":false,"schema":{"type":"string","default":"","title":"Platform"}},{"name":"app_version","in":"query","required":false,"schema":{"type":"string","default":"","title":"App Version"}},{"name":"install_id","in":"query","required":false,"schema":{"type":"string","default":"","title":"Install Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/events.csv":{"get":{"tags":["admin"],"summary":"Events Csv","description":"CSV export of the filtered event log (oldest first, capped).","operationId":"events_csv_admin_events_csv_get","parameters":[{"name":"start","in":"query","required":false,"schema":{"type":"string","default":"","title":"Start"}},{"name":"end","in":"query","required":false,"schema":{"type":"string","default":"","title":"End"}},{"name":"email","in":"query","required":false,"schema":{"type":"string","default":"","title":"Email"}},{"name":"event","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"default":[],"title":"Event"}},{"name":"source","in":"query","required":false,"schema":{"type":"string","default":"","title":"Source"}},{"name":"platform","in":"query","required":false,"schema":{"type":"string","default":"","title":"Platform"}},{"name":"app_version","in":"query","required":false,"schema":{"type":"string","default":"","title":"App Version"}},{"name":"install_id","in":"query","required":false,"schema":{"type":"string","default":"","title":"Install Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/funnel":{"get":{"tags":["admin"],"summary":"Funnel Page","description":"Product analytics funnel over app_events (docs/PRODUCT_ANALYTICS.md).","operationId":"funnel_page_admin_funnel_get","parameters":[{"name":"start","in":"query","required":false,"schema":{"type":"string","default":"","title":"Start"}},{"name":"end","in":"query","required":false,"schema":{"type":"string","default":"","title":"End"}},{"name":"platform","in":"query","required":false,"schema":{"type":"string","default":"","title":"Platform"}},{"name":"app_version","in":"query","required":false,"schema":{"type":"string","default":"","title":"App Version"}},{"name":"tier","in":"query","required":false,"schema":{"type":"string","default":"","title":"Tier"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/rate-limits":{"get":{"tags":["admin"],"summary":"Rate Limits Page","description":"List rate limit events (429 responses) with filters and stats.","operationId":"rate_limits_page_admin_rate_limits_get","parameters":[{"name":"identifier_type","in":"query","required":false,"schema":{"type":"string","default":"","title":"Identifier Type"}},{"name":"endpoint","in":"query","required":false,"schema":{"type":"string","default":"","title":"Endpoint"}},{"name":"search","in":"query","required":false,"schema":{"type":"string","default":"","title":"Search"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1,"title":"Page"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/blocked-ips":{"get":{"tags":["admin"],"summary":"Blocked Ips Page","description":"List all blocked IPs.","operationId":"blocked_ips_page_admin_blocked_ips_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/blocked-ips/add":{"post":{"tags":["admin"],"summary":"Add Blocked Ip","description":"Block an IP address.","operationId":"add_blocked_ip_admin_blocked_ips_add_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_add_blocked_ip_admin_blocked_ips_add_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/blocked-ips/{blocked_id}/unblock":{"post":{"tags":["admin"],"summary":"Unblock Ip","description":"Unblock an IP address.","operationId":"unblock_ip_admin_blocked_ips__blocked_id__unblock_post","parameters":[{"name":"blocked_id","in":"path","required":true,"schema":{"type":"integer","title":"Blocked Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"allOf":[{"$ref":"#/components/schemas/Body_unblock_ip_admin_blocked_ips__blocked_id__unblock_post"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"tags":["root"],"summary":"Root","description":"Root endpoint. Returns basic API information.","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Root  Get"}}}}}}},"/health":{"get":{"tags":["health"],"summary":"Health Check","description":"Health check endpoint.\n\nPerforms real checks against database, polling loop, and circuit\nbreaker. Returns overall status: healthy, degraded, or unhealthy.\nUptime Robot keyword monitoring should check for \"healthy\".","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/ping":{"get":{"tags":["health"],"summary":"Ping","description":"Minimal liveness probe. Always returns 200 with {\"status\": \"ok\"}.","operationId":"ping_ping_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PingResponse"}}}}}}}},"components":{"schemas":{"AcceptTermsRequest":{"properties":{"terms_accepted":{"type":"boolean","title":"Terms Accepted","description":"Must be true to accept Terms of Service"}},"type":"object","required":["terms_accepted"],"title":"AcceptTermsRequest","description":"Request to accept Terms of Service."},"AcceptTermsResponse":{"properties":{"message":{"type":"string","title":"Message"},"terms_accepted":{"type":"boolean","title":"Terms Accepted"}},"type":"object","required":["message","terms_accepted"],"title":"AcceptTermsResponse","description":"Response after accepting Terms of Service."},"AlertCreate":{"properties":{"park_id":{"type":"string","maxLength":64,"minLength":1,"title":"Park Id"},"park_name":{"type":"string","maxLength":255,"minLength":1,"title":"Park Name"},"campground_selections":{"items":{"$ref":"#/components/schemas/CampgroundSelectionCreate"},"type":"array","maxItems":200,"minItems":1,"title":"Campground Selections","description":"At least one campground must be selected"},"date_start":{"type":"string","format":"date","title":"Date Start","description":"Arrival date start"},"date_end":{"type":"string","format":"date","title":"Date End","description":"Arrival date end (date_start + window)"},"days_filter":{"type":"string","pattern":"^(all|weekends|weekdays)$","title":"Days Filter","default":"all"},"nights_min":{"type":"integer","maximum":14.0,"minimum":1.0,"title":"Nights Min","default":1},"notify_push":{"type":"boolean","title":"Notify Push","default":true},"notify_email":{"type":"boolean","title":"Notify Email","default":true},"notify_sms":{"type":"boolean","title":"Notify Sms","default":false},"split_stay":{"type":"boolean","title":"Split Stay","default":false},"activity_type":{"anyOf":[{"type":"string","pattern":"^(camping|chalet|canoe_camping|backcountry|fishing|boat|equestrian|day_use)$"},{"type":"null"}],"title":"Activity Type"},"equipment_filter_mode":{"type":"string","pattern":"^(any|tent|rv)$","title":"Equipment Filter Mode","default":"any"},"sub_equipment_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sub Equipment Id"},"party_size":{"anyOf":[{"type":"integer","maximum":99.0,"minimum":1.0},{"type":"null"}],"title":"Party Size"},"tent_pads":{"anyOf":[{"type":"integer","maximum":50.0,"minimum":1.0},{"type":"null"}],"title":"Tent Pads"},"day_use_cells":{"anyOf":[{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},{"type":"null"}],"title":"Day Use Cells"},"entry_dates":{"anyOf":[{"items":{"type":"string","format":"date"},"type":"array"},{"type":"null"}],"title":"Entry Dates"},"webhook_url":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"Webhook Url"},"has_electricity":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Electricity"},"is_accessible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Accessible"},"pets_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Pets Only"},"is_pull_through":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Pull Through"},"is_tent_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Tent Only"},"shade_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Shade Only"},"waterfront_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Waterfront Only"}},"type":"object","required":["park_id","park_name","campground_selections","date_start","date_end"],"title":"AlertCreate","description":"Schema for creating an alert with multiple campground selections."},"AlertListResponse":{"properties":{"alerts":{"items":{"$ref":"#/components/schemas/AlertResponse"},"type":"array","title":"Alerts"},"total":{"type":"integer","title":"Total"},"active_count":{"type":"integer","title":"Active Count"}},"type":"object","required":["alerts","total","active_count"],"title":"AlertListResponse","description":"Schema for list of alerts response."},"AlertMatchListResponse":{"properties":{"matches":{"items":{"$ref":"#/components/schemas/AlertMatchResponse"},"type":"array","title":"Matches"},"total":{"type":"integer","title":"Total"},"active_count":{"type":"integer","title":"Active Count"}},"type":"object","required":["matches","total","active_count"],"title":"AlertMatchListResponse","description":"Schema for a list of alert matches."},"AlertMatchResponse":{"properties":{"id":{"type":"string","title":"Id"},"alert_id":{"type":"string","title":"Alert Id"},"user_id":{"type":"string","title":"User Id"},"site_id":{"type":"string","title":"Site Id"},"site_name":{"type":"string","title":"Site Name"},"site_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Url"},"site_image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Image Url"},"park_id":{"type":"string","title":"Park Id"},"park_name":{"type":"string","title":"Park Name"},"camp_type":{"type":"string","title":"Camp Type"},"available_date":{"type":"string","format":"date-time","title":"Available Date"},"price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Price"},"booking_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Booking Url"},"nights":{"type":"integer","title":"Nights","default":1},"segment_group_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Segment Group Id"},"segment_order":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Segment Order"},"pets_allowed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Pets Allowed"},"accommodation_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accommodation Type"},"quota_span_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Quota Span Days"},"has_water":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Water"},"has_electricity":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Electricity"},"has_sewer":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Sewer"},"is_accessible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Accessible"},"is_double_site":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Double Site"},"is_pull_through":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Pull Through"},"is_tent_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Tent Only"},"equipment_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Equipment Label"},"status":{"type":"string","title":"Status"},"detected_at":{"type":"string","format":"date-time","title":"Detected At"},"seen_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Seen At"},"booked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Booked At"},"clicked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Clicked At"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"booking_closed":{"type":"boolean","title":"Booking Closed","default":false},"booking_deadline":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Booking Deadline"}},"type":"object","required":["id","alert_id","user_id","site_id","site_name","park_id","park_name","camp_type","available_date","status","detected_at","expires_at"],"title":"AlertMatchResponse","description":"Schema for an individual alert match (persisted cancellation detection)."},"AlertMatchSummary":{"properties":{"alert_id":{"type":"string","title":"Alert Id"},"active_matches":{"type":"integer","title":"Active Matches"}},"type":"object","required":["alert_id","active_matches"],"title":"AlertMatchSummary","description":"Badge-count summary for a single alert."},"AlertMatchesSummaryResponse":{"properties":{"summaries":{"items":{"$ref":"#/components/schemas/AlertMatchSummary"},"type":"array","title":"Summaries"}},"type":"object","required":["summaries"],"title":"AlertMatchesSummaryResponse","description":"Summary of active matches across all alerts (for badge display)."},"AlertOddsRequest":{"properties":{"park_id":{"type":"string","maxLength":64,"minLength":1,"title":"Park Id"},"nights_min":{"type":"integer","maximum":30.0,"minimum":1.0,"title":"Nights Min","default":1},"campground_ids":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":200},{"type":"null"}],"title":"Campground Ids"},"site_ids":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":5000},{"type":"null"}],"title":"Site Ids"},"activity_type":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Activity Type"},"date_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Start"},"date_end":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date End"},"equipment_filter_mode":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Equipment Filter Mode"},"days_filter":{"type":"string","pattern":"^(all|weekends|weekdays)$","title":"Days Filter","default":"all"},"split_stay":{"type":"boolean","title":"Split Stay","default":false}},"type":"object","required":["park_id"],"title":"AlertOddsRequest","description":"Draft-alert fields for an odds preview (POST /api/v1/alerts/odds).\n\n``park_id`` + ``nights_min`` always drive scoring; ``campground_ids`` scopes\nto a specific campground when the draft targets exactly one (a park\naggregate misleads for heterogeneous parks). The date/equipment fields are\naccepted for forward-compatibility and are optional so current clients can\nomit them."},"AlertOddsResponse":{"properties":{"band":{"type":"string","title":"Band"},"evidence":{"type":"object","title":"Evidence"},"suggestions":{"items":{"$ref":"#/components/schemas/AlertOddsSuggestion"},"type":"array","title":"Suggestions"}},"type":"object","required":["band"],"title":"AlertOddsResponse","description":"Qualitative odds band + evidence + improvement suggestions.\n\n``band`` is one of: ``limited_data`` | ``low`` | ``moderate`` | ``good``.\nIntentionally NO probability percentage — the band + evidence numbers are\nthe trustworthy surface. ``evidence`` is a flat map the client renders into\na localized line (e.g. observation_days, total_openings, max_nights_seen)."},"AlertOddsSuggestion":{"properties":{"type":{"type":"string","title":"Type"},"nights_min":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Nights Min"},"qualifying_openings":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Qualifying Openings"},"uplift":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Uplift"},"recommended_tier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recommended Tier"},"min_rebooking_minutes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Min Rebooking Minutes"},"median_rebooking_minutes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Median Rebooking Minutes"}},"type":"object","required":["type"],"title":"AlertOddsSuggestion","description":"A quantified, data-backed way to improve an alert's odds."},"AlertResponse":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"type":"string","title":"User Id"},"alert_type":{"type":"string","title":"Alert Type","default":"standard"},"provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider"},"park_id":{"type":"string","title":"Park Id"},"park_name":{"type":"string","title":"Park Name"},"date_start":{"type":"string","format":"date","title":"Date Start"},"date_end":{"type":"string","format":"date","title":"Date End"},"days_filter":{"type":"string","title":"Days Filter","default":"all"},"split_stay":{"type":"boolean","title":"Split Stay","default":false},"nights_min":{"type":"integer","title":"Nights Min"},"nights_max":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Nights Max"},"campground_selections":{"items":{"$ref":"#/components/schemas/CampgroundSelectionResponse"},"type":"array","title":"Campground Selections"},"is_active":{"type":"boolean","title":"Is Active"},"notify_push":{"type":"boolean","title":"Notify Push"},"notify_email":{"type":"boolean","title":"Notify Email"},"notify_sms":{"type":"boolean","title":"Notify Sms","default":false},"party_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Party Size"},"sub_equipment_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sub Equipment Id"},"op_access_point_resource_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Access Point Resource Id"},"tent_pads":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tent Pads"},"day_use_cells":{"anyOf":[{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},{"type":"null"}],"title":"Day Use Cells"},"entry_dates":{"anyOf":[{"items":{"type":"string","format":"date"},"type":"array"},{"type":"null"}],"title":"Entry Dates"},"activity_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Activity Type"},"equipment_filter_mode":{"type":"string","title":"Equipment Filter Mode","default":"any"},"has_electricity":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Electricity"},"is_accessible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Accessible"},"pets_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Pets Only"},"is_pull_through":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Pull Through"},"is_tent_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Tent Only"},"shade_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Shade Only"},"waterfront_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Waterfront Only"},"booking_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Booking Url"},"out_of_season":{"anyOf":[{"$ref":"#/components/schemas/OutOfSeasonHint"},{"type":"null"}]},"last_checked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Checked At"},"last_notified_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Notified At"},"notification_count":{"type":"integer","title":"Notification Count"},"availabilities_found":{"type":"integer","title":"Availabilities Found","default":0},"cancellations_found":{"type":"integer","title":"Cancellations Found","default":0},"scan_count":{"type":"integer","title":"Scan Count","default":0},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"created_via":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created Via"},"created_by_agent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By Agent"}},"type":"object","required":["id","user_id","park_id","park_name","date_start","date_end","nights_min","nights_max","is_active","notify_push","notify_email","last_checked_at","last_notified_at","notification_count","created_at","updated_at"],"title":"AlertResponse","description":"Schema for alert response with campground selections."},"AlertSuccessRate":{"properties":{"total_alerts":{"type":"integer","title":"Total Alerts"},"alerts_with_matches":{"type":"integer","title":"Alerts With Matches"},"success_rate":{"type":"number","title":"Success Rate"}},"type":"object","required":["total_alerts","alerts_with_matches","success_rate"],"title":"AlertSuccessRate","description":"Alert success rate over a period."},"AlertUpdate":{"properties":{"date_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Start"},"date_end":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date End"},"days_filter":{"anyOf":[{"type":"string","pattern":"^(all|weekends|weekdays)$"},{"type":"null"}],"title":"Days Filter"},"nights_min":{"anyOf":[{"type":"integer","maximum":14.0,"minimum":1.0},{"type":"null"}],"title":"Nights Min"},"nights_max":{"anyOf":[{"type":"integer","maximum":14.0,"minimum":1.0},{"type":"null"}],"title":"Nights Max"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"notify_push":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Notify Push"},"notify_email":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Notify Email"},"notify_sms":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Notify Sms"},"split_stay":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Split Stay"},"party_size":{"anyOf":[{"type":"integer","maximum":99.0,"minimum":1.0},{"type":"null"}],"title":"Party Size"},"sub_equipment_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sub Equipment Id"},"op_access_point_resource_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Access Point Resource Id"},"tent_pads":{"anyOf":[{"type":"integer","maximum":50.0,"minimum":1.0},{"type":"null"}],"title":"Tent Pads"},"day_use_cells":{"anyOf":[{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},{"type":"null"}],"title":"Day Use Cells"},"entry_dates":{"anyOf":[{"items":{"type":"string","format":"date"},"type":"array"},{"type":"null"}],"title":"Entry Dates"},"equipment_filter_mode":{"anyOf":[{"type":"string","pattern":"^(any|tent|rv)$"},{"type":"null"}],"title":"Equipment Filter Mode"},"activity_type":{"anyOf":[{"type":"string","pattern":"^(camping|chalet|canoe_camping|backcountry|fishing|boat|equestrian|day_use)$"},{"type":"null"}],"title":"Activity Type"},"has_electricity":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Electricity"},"is_accessible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Accessible"},"pets_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Pets Only"},"is_pull_through":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Pull Through"},"is_tent_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Tent Only"},"shade_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Shade Only"},"waterfront_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Waterfront Only"},"campground_selections":{"anyOf":[{"items":{"$ref":"#/components/schemas/CampgroundSelectionCreate"},"type":"array","maxItems":200,"minItems":1},{"type":"null"}],"title":"Campground Selections"}},"type":"object","title":"AlertUpdate","description":"Schema for updating an alert."},"AmenityLegendEntry":{"properties":{"label":{"type":"string","maxLength":128,"title":"Label"},"icon_url":{"type":"string","maxLength":512,"title":"Icon Url"},"bg_color":{"anyOf":[{"type":"string","maxLength":9},{"type":"null"}],"title":"Bg Color"}},"type":"object","required":["label","icon_url"],"title":"AmenityLegendEntry","description":"One row of the amenity legend key — an icon + its name.\n\nUsed for providers whose amenity icons are baked into the map artwork\n(Alberta), so we show a \"what the icons mean\" list rather than\npositional markers.  ``icon_url`` proxies the icon from our DB."},"AmenityPosition":{"properties":{"legend_type":{"type":"integer","minimum":0.0,"title":"Legend Type"},"x":{"type":"integer","minimum":0.0,"title":"X"},"y":{"type":"integer","minimum":0.0,"title":"Y"},"r":{"type":"integer","maximum":255.0,"minimum":0.0,"title":"R"},"g":{"type":"integer","maximum":255.0,"minimum":0.0,"title":"G"},"b":{"type":"integer","maximum":255.0,"minimum":0.0,"title":"B"},"icon_asset":{"type":"string","maxLength":64,"title":"Icon Asset"}},"type":"object","required":["legend_type","x","y","r","g","b","icon_asset"],"title":"AmenityPosition","description":"A legend marker (parking, restroom, water tap, etc.) on a campground map.\n\n`legend_type` is the GTC platform-wide enum (same code = same icon\nacross OP/PC/BC).  `icon_asset` is the filename under\n`mobile/assets/legend_icons/` the client should render.  `r/g/b`\nis the colored backdrop the booking site draws behind the icon."},"AppEventBatch":{"properties":{"install_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Install Id"},"session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session Id"},"platform":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform"},"app_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Version"},"events":{"items":{"$ref":"#/components/schemas/AppEventIn"},"type":"array","maxItems":200,"title":"Events"}},"type":"object","title":"AppEventBatch","description":"A batch of client events from one install/session."},"AppEventBatchResponse":{"properties":{"accepted":{"type":"integer","title":"Accepted"},"dropped":{"type":"integer","title":"Dropped"}},"type":"object","required":["accepted","dropped"],"title":"AppEventBatchResponse"},"AppEventIn":{"properties":{"event":{"type":"string","maxLength":64,"minLength":1,"title":"Event"},"props":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Props"},"occurred_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Occurred At"}},"type":"object","required":["event"],"title":"AppEventIn","description":"One client event.  ``event`` is validated against the allowlist in the\nservice (unknown names are dropped, never rejected)."},"AppleSignInRequest":{"properties":{"id_token":{"type":"string","minLength":1,"title":"Id Token","description":"Apple identity token"},"language":{"anyOf":[{"type":"string","pattern":"^(fr|en|es|zh|pt|it|ht|ro|vi|el|tl|ru|hi|ko|pa|pl|de)$"},{"type":"null"}],"title":"Language"}},"type":"object","required":["id_token"],"title":"AppleSignInRequest","description":"Schema for Apple sign-in via backend (reCAPTCHA bypass)."},"ApprovalResult":{"properties":{"status":{"type":"string","title":"Status"}},"type":"object","required":["status"],"title":"ApprovalResult"},"Body_add_blocked_ip_admin_blocked_ips_add_post":{"properties":{"ip_address":{"type":"string","title":"Ip Address","default":""},"reason":{"type":"string","title":"Reason","default":""},"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_add_blocked_ip_admin_blocked_ips_add_post"},"Body_boost_alert_admin_alerts__alert_id__boost_post":{"properties":{"interval_seconds":{"type":"integer","title":"Interval Seconds"},"until":{"type":"string","title":"Until"},"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","required":["interval_seconds","until"],"title":"Body_boost_alert_admin_alerts__alert_id__boost_post"},"Body_boost_itinerary_admin_itineraries__itinerary_id__boost_post":{"properties":{"interval_seconds":{"type":"integer","title":"Interval Seconds"},"until":{"type":"string","title":"Until"},"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","required":["interval_seconds","until"],"title":"Body_boost_itinerary_admin_itineraries__itinerary_id__boost_post"},"Body_change_tier_admin_users__user_id__change_tier_post":{"properties":{"tier":{"type":"string","title":"Tier"},"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","required":["tier"],"title":"Body_change_tier_admin_users__user_id__change_tier_post"},"Body_clear_boost_alert_admin_alerts__alert_id__clear_boost_post":{"properties":{"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_clear_boost_alert_admin_alerts__alert_id__clear_boost_post"},"Body_clear_boost_itinerary_admin_itineraries__itinerary_id__clear_boost_post":{"properties":{"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_clear_boost_itinerary_admin_itineraries__itinerary_id__clear_boost_post"},"Body_delete_user_admin_users__user_id__delete_post":{"properties":{"confirm_email":{"type":"string","title":"Confirm Email","default":""},"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_delete_user_admin_users__user_id__delete_post"},"Body_exclusion_add_admin_exclusions_add_post":{"properties":{"site_id":{"type":"string","title":"Site Id"},"date_start":{"type":"string","title":"Date Start","default":""},"date_end":{"type":"string","title":"Date End","default":""},"reason":{"type":"string","title":"Reason","default":""},"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","required":["site_id"],"title":"Body_exclusion_add_admin_exclusions_add_post"},"Body_exclusion_delete_admin_exclusions__exclusion_id__delete_post":{"properties":{"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_exclusion_delete_admin_exclusions__exclusion_id__delete_post"},"Body_feedback_delete_admin_feedback__feedback_id__delete_post":{"properties":{"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_feedback_delete_admin_feedback__feedback_id__delete_post"},"Body_feedback_update_description_admin_feedback__feedback_id__update_description_post":{"properties":{"description":{"type":"string","title":"Description"},"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","required":["description"],"title":"Body_feedback_update_description_admin_feedback__feedback_id__update_description_post"},"Body_feedback_update_github_admin_feedback__feedback_id__update_github_post":{"properties":{"github_issue_url":{"type":"string","title":"Github Issue Url","default":""},"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_feedback_update_github_admin_feedback__feedback_id__update_github_post"},"Body_feedback_update_notes_admin_feedback__feedback_id__update_notes_post":{"properties":{"admin_notes":{"type":"string","title":"Admin Notes","default":""},"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_feedback_update_notes_admin_feedback__feedback_id__update_notes_post"},"Body_feedback_update_status_admin_feedback__feedback_id__update_status_post":{"properties":{"status":{"type":"string","title":"Status"},"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","required":["status"],"title":"Body_feedback_update_status_admin_feedback__feedback_id__update_status_post"},"Body_login_submit_admin_login_post":{"properties":{"token":{"type":"string","title":"Token"},"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","required":["token"],"title":"Body_login_submit_admin_login_post"},"Body_sepaq_waitlist_delete_admin_sepaq_waitlist__entry_id__delete_post":{"properties":{"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_sepaq_waitlist_delete_admin_sepaq_waitlist__entry_id__delete_post"},"Body_toggle_active_admin_users__user_id__toggle_active_post":{"properties":{"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_toggle_active_admin_users__user_id__toggle_active_post"},"Body_toggle_alerts_paused_admin_users__user_id__toggle_alerts_paused_post":{"properties":{"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_toggle_alerts_paused_admin_users__user_id__toggle_alerts_paused_post"},"Body_token_endpoint_oauth_token_post":{"properties":{"grant_type":{"type":"string","title":"Grant Type","default":""},"code":{"type":"string","title":"Code","default":""},"redirect_uri":{"type":"string","title":"Redirect Uri","default":""},"client_id":{"type":"string","title":"Client Id","default":""},"client_secret":{"type":"string","title":"Client Secret","default":""},"code_verifier":{"type":"string","title":"Code Verifier","default":""},"resource":{"type":"string","title":"Resource","default":""}},"type":"object","title":"Body_token_endpoint_oauth_token_post"},"Body_unblock_ip_admin_blocked_ips__blocked_id__unblock_post":{"properties":{"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_unblock_ip_admin_blocked_ips__blocked_id__unblock_post"},"Body_unpause_admin_users__user_id__unpause_post":{"properties":{"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_unpause_admin_users__user_id__unpause_post"},"Body_verify_email_admin_users__user_id__verify_email_post":{"properties":{"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_verify_email_admin_users__user_id__verify_email_post"},"Body_waitlist_delete_admin_waitlist__entry_id__delete_post":{"properties":{"csrf_token":{"type":"string","title":"Csrf Token","default":""}},"type":"object","title":"Body_waitlist_delete_admin_waitlist__entry_id__delete_post"},"CampgroundListResponse":{"properties":{"campgrounds":{"items":{"$ref":"#/components/schemas/CampgroundResponse"},"type":"array","title":"Campgrounds"},"total":{"type":"integer","title":"Total"},"park_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Park Id","description":"Park ID if filtered by park"}},"type":"object","required":["campgrounds","total"],"title":"CampgroundListResponse","description":"Schema for list of campgrounds response."},"CampgroundMapItem":{"properties":{"id":{"type":"string","maxLength":128,"title":"Id"},"gtc_map_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Gtc Map Id"},"image_url":{"type":"string","maxLength":512,"title":"Image Url"},"image_is_svg":{"type":"boolean","title":"Image Is Svg","default":false},"width":{"type":"integer","exclusiveMinimum":0.0,"title":"Width"},"height":{"type":"integer","exclusiveMinimum":0.0,"title":"Height"},"name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Name"},"display_order":{"type":"integer","minimum":0.0,"title":"Display Order"},"sites":{"items":{"$ref":"#/components/schemas/SiteMapPosition"},"type":"array","title":"Sites"},"amenities":{"items":{"$ref":"#/components/schemas/AmenityPosition"},"type":"array","title":"Amenities"},"amenity_legend":{"items":{"$ref":"#/components/schemas/AmenityLegendEntry"},"type":"array","title":"Amenity Legend"}},"type":"object","required":["id","image_url","width","height","display_order","sites"],"title":"CampgroundMapItem","description":"A single map within a campground (one zone or area)."},"CampgroundMapsResponse":{"properties":{"campground_id":{"type":"string","maxLength":128,"title":"Campground Id"},"maps":{"items":{"$ref":"#/components/schemas/CampgroundMapItem"},"type":"array","title":"Maps"}},"type":"object","required":["campground_id","maps"],"title":"CampgroundMapsResponse","description":"Wrapper for the maps endpoint."},"CampgroundResponse":{"properties":{"id":{"type":"string","title":"Id"},"park_id":{"type":"string","title":"Park Id"},"name":{"type":"string","title":"Name"},"name_en":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name En"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"is_active":{"type":"boolean","title":"Is Active"},"latitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Latitude"},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Longitude"},"max_watched_sites":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Watched Sites","description":"Max sites ONE alert may watch in this campground, narrowing the tier's sites-per-alert limit. null = tier limit only."},"max_sites_per_night":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Sites Per Night","description":"Max sites one ROUTE NIGHT may offer here. null = no limit."},"has_roofed_sites":{"type":"boolean","title":"Has Roofed Sites","description":"Whether any zone in this campground contains roofed accommodation sites.","default":false},"has_canoe_sites":{"type":"boolean","title":"Has Canoe Sites","description":"Whether any zone in this campground contains canoe-camping sites.","default":false},"has_backcountry_sites":{"type":"boolean","title":"Has Backcountry Sites","description":"Whether any zone in this campground contains backcountry sites.","default":false},"has_site_map":{"type":"boolean","title":"Has Site Map","description":"Whether THIS campground has a photo-overlay site map, so the picker can route to it per campground instead of per platform. Maps are sparse within a provider — Recreation.gov has exactly one (Yosemite's permit trailheads, from the NPS wilderness map) against 92,467 mapless sites — so `Platform.has_site_map` is the wrong unit and a per-park override would only restate what the map rows already say. Defaults False, so a client that ignores it behaves exactly as before.","default":false},"has_day_use_sites":{"type":"boolean","title":"Has Day Use Sites","description":"Whether any zone in this campground contains day-use sites (Parks Canada parking time slots, Banff shuttle departures). Defaults False, so builds that predate the activity are unaffected — the v1 campgrounds route hides day-use-only campgrounds from them entirely.","default":false},"seasons":{"items":{"$ref":"#/components/schemas/SeasonSpan"},"type":"array","title":"Seasons","description":"Distinct operating seasons among this campground's active sites — usually one, occasionally several (a campground can open its loops on different dates). Empty when no site publishes a season. Data only: the server does not act on it, and a client that ignores it behaves exactly as before."},"has_unknown_season":{"type":"boolean","title":"Has Unknown Season","description":"Whether ANY active site here has no published season. Most providers publish none at all (Aspira, UseDirect, and every Recreation.gov site not yet swept), and unknown must never read as closed. Defaults TRUE so a client treats the campground as always open unless told otherwise — the safe direction, since the only use for this is warning that dates can never match.","default":true},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","park_id","name","name_en","description","url","is_active","created_at","updated_at"],"title":"CampgroundResponse","description":"Schema for campground response."},"CampgroundSelectionCreate":{"properties":{"campground_id":{"type":"string","maxLength":128,"minLength":1,"title":"Campground Id"},"zone_ids":{"items":{"type":"string"},"type":"array","maxItems":200,"title":"Zone Ids","description":"Zone IDs to monitor. ['all'] = all zones."},"site_ids":{"items":{"type":"string"},"type":"array","maxItems":5000,"title":"Site Ids","description":"Site IDs to monitor. ['all'] = all sites."}},"type":"object","required":["campground_id"],"title":"CampgroundSelectionCreate","description":"Schema for creating a campground selection within an alert."},"CampgroundSelectionResponse":{"properties":{"id":{"type":"string","title":"Id"},"campground_id":{"type":"string","title":"Campground Id"},"zone_ids":{"items":{"type":"string"},"type":"array","title":"Zone Ids"},"site_ids":{"items":{"type":"string"},"type":"array","title":"Site Ids"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","campground_id","created_at","updated_at"],"title":"CampgroundSelectionResponse","description":"Schema for campground selection in response."},"CancellationOverview":{"properties":{"total_cancellations":{"type":"integer","title":"Total Cancellations"},"total_rebooked":{"type":"integer","title":"Total Rebooked"},"rebooking_rate":{"type":"number","title":"Rebooking Rate"},"avg_nights":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Nights"},"avg_rebooking_delay_minutes":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Rebooking Delay Minutes"},"median_rebooking_delay_minutes":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Median Rebooking Delay Minutes"},"avg_days_until":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Days Until"},"period_start":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Period Start"},"period_end":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Period End"}},"type":"object","required":["total_cancellations","total_rebooked","rebooking_rate","avg_nights","avg_rebooking_delay_minutes","median_rebooking_delay_minutes","avg_days_until","period_start","period_end"],"title":"CancellationOverview","description":"Aggregate overview of cancellation events."},"CancellationStatsResponse":{"properties":{"overview":{"$ref":"#/components/schemas/CancellationOverview"},"by_park":{"items":{"$ref":"#/components/schemas/ParkCancellationStats"},"type":"array","title":"By Park"},"by_day_of_week":{"items":{"$ref":"#/components/schemas/DayOfWeekStats"},"type":"array","title":"By Day Of Week"},"by_hour_of_day":{"items":{"$ref":"#/components/schemas/HourOfDayStats"},"type":"array","title":"By Hour Of Day"},"lead_time_distribution":{"items":{"$ref":"#/components/schemas/LeadTimeBucket"},"type":"array","title":"Lead Time Distribution"},"by_detection_month":{"items":{"$ref":"#/components/schemas/MonthStats"},"type":"array","title":"By Detection Month"},"by_camping_month":{"items":{"$ref":"#/components/schemas/MonthStats"},"type":"array","title":"By Camping Month"},"rebooking_speed_by_park":{"items":{"$ref":"#/components/schemas/ParkRebookingSpeed"},"type":"array","title":"Rebooking Speed By Park"},"alert_success_rate":{"$ref":"#/components/schemas/AlertSuccessRate"}},"type":"object","required":["overview","by_park","by_day_of_week","by_hour_of_day","lead_time_distribution","by_detection_month","by_camping_month","rebooking_speed_by_park","alert_success_rate"],"title":"CancellationStatsResponse","description":"Full cancellation statistics response."},"CircuitBreakerHealth":{"properties":{"status":{"$ref":"#/components/schemas/ComponentStatus"},"state":{"type":"string","title":"State"},"failure_count":{"type":"integer","title":"Failure Count","default":0},"opened_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Opened At"}},"type":"object","required":["status","state"],"title":"CircuitBreakerHealth","description":"Circuit breaker component health."},"ClusterHealth":{"properties":{"status":{"$ref":"#/components/schemas/ComponentStatus"},"total_nodes":{"type":"integer","title":"Total Nodes"},"healthy_nodes":{"type":"integer","title":"Healthy Nodes"},"workers":{"items":{"$ref":"#/components/schemas/WorkerStatus"},"type":"array","title":"Workers"},"cluster_sites_polled":{"type":"integer","title":"Cluster Sites Polled","default":0},"cluster_sites_successful":{"type":"integer","title":"Cluster Sites Successful","default":0},"cluster_sites_failed":{"type":"integer","title":"Cluster Sites Failed","default":0},"cluster_success_rate":{"type":"number","title":"Cluster Success Rate","default":0.0},"cluster_alerts_matched":{"type":"integer","title":"Cluster Alerts Matched","default":0},"cluster_requests_per_second":{"type":"number","title":"Cluster Requests Per Second","default":0.0},"cluster_backlog":{"anyOf":[{"$ref":"#/components/schemas/PollingBacklog"},{"type":"null"}]},"cluster_stats_backlog":{"anyOf":[{"$ref":"#/components/schemas/PollingBacklog"},{"type":"null"}]},"cluster_provider_status":{"additionalProperties":{"type":"string"},"type":"object","title":"Cluster Provider Status","default":{}},"cluster_maintenance_providers":{"items":{"type":"string"},"type":"array","title":"Cluster Maintenance Providers","default":[]},"cluster_maintenance_since":{"additionalProperties":{"type":"string"},"type":"object","title":"Cluster Maintenance Since","default":{}},"provider_tier_status":{"additionalProperties":{"additionalProperties":{"type":"string"},"type":"object"},"type":"object","title":"Provider Tier Status","default":{}},"stuck_targets":{"items":{"$ref":"#/components/schemas/StuckTarget"},"type":"array","title":"Stuck Targets","default":[]}},"type":"object","required":["status","total_nodes","healthy_nodes","workers"],"title":"ClusterHealth","description":"Aggregate health of all nodes in the cluster."},"ComparisonRowConfig":{"properties":{"label":{"type":"string","title":"Label"},"values":{"items":{"type":"string"},"type":"array","title":"Values"},"type":{"type":"string","title":"Type"}},"type":"object","required":["label","values","type"],"title":"ComparisonRowConfig","description":"A row in the tier comparison table."},"ComponentStatus":{"type":"string","enum":["ok","warn","fail"],"title":"ComponentStatus","description":"Health status of an individual component."},"DatabaseHealth":{"properties":{"status":{"$ref":"#/components/schemas/ComponentStatus"},"latency_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Latency Ms"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["status"],"title":"DatabaseHealth","description":"Database component health."},"DayOfWeekStats":{"properties":{"day_of_week":{"type":"integer","title":"Day Of Week"},"cancellation_count":{"type":"integer","title":"Cancellation Count"}},"type":"object","required":["day_of_week","cancellation_count"],"title":"DayOfWeekStats","description":"Cancellation counts by day of the week."},"DevSetTierRequest":{"properties":{"tier":{"type":"string","title":"Tier","description":"Tier to set: essai, regular, pro"}},"type":"object","required":["tier"],"title":"DevSetTierRequest","description":"Request schema for development tier setting endpoint."},"DeviceTokenRegister":{"properties":{"fcm_token":{"type":"string","maxLength":512,"minLength":1,"title":"Fcm Token"},"device_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":1},{"type":"null"}],"title":"Device Id","description":"Stable device UUID (generated on client). None for backward compat with old mobile clients."},"device_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Device Name"},"platform":{"anyOf":[{"type":"string","pattern":"^(ios|android)$"},{"type":"null"}],"title":"Platform"},"os_version":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Os Version"},"app_version":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"App Version"}},"type":"object","required":["fcm_token"],"title":"DeviceTokenRegister","description":"Schema for registering FCM token with device metadata."},"DirectTokenResponse":{"properties":{"id_token":{"type":"string","title":"Id Token"},"refresh_token":{"type":"string","title":"Refresh Token"},"user_id":{"type":"string","title":"User Id"},"email":{"type":"string","title":"Email"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"email_verified":{"type":"boolean","title":"Email Verified","default":false},"terms_accepted":{"type":"boolean","title":"Terms Accepted","default":false},"terms_previously_accepted":{"type":"boolean","title":"Terms Previously Accepted","default":false}},"type":"object","required":["id_token","refresh_token","user_id","email"],"title":"DirectTokenResponse","description":"Response containing Firebase tokens for direct auth (no SDK needed)."},"EmailSignInRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","minLength":1,"title":"Password"}},"type":"object","required":["email","password"],"title":"EmailSignInRequest","description":"Schema for email/password sign-in via backend."},"EmailSignUpRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","minLength":6,"title":"Password"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"language":{"anyOf":[{"type":"string","pattern":"^(fr|en|es|zh|pt|it|ht|ro|vi|el|tl|ru|hi|ko|pa|pl|de)$"},{"type":"null"}],"title":"Language"},"terms_accepted":{"type":"boolean","title":"Terms Accepted","description":"User must accept Terms of Service"}},"type":"object","required":["email","password","terms_accepted"],"title":"EmailSignUpRequest","description":"Schema for email/password sign-up via backend."},"FacebookExchangeRequest":{"properties":{"code":{"type":"string","minLength":1,"title":"Code","description":"Exchange code from OAuth callback"},"language":{"anyOf":[{"type":"string","pattern":"^(fr|en|es|zh|pt|it|ht|ro|vi|el|tl|ru|hi|ko|pa|pl|de)$"},{"type":"null"}],"title":"Language"}},"type":"object","required":["code"],"title":"FacebookExchangeRequest","description":"Schema for exchanging a Facebook OAuth code for tokens."},"FacebookSignInRequest":{"properties":{"access_token":{"type":"string","minLength":1,"title":"Access Token","description":"Facebook access token"},"language":{"anyOf":[{"type":"string","pattern":"^(fr|en|es|zh|pt|it|ht|ro|vi|el|tl|ru|hi|ko|pa|pl|de)$"},{"type":"null"}],"title":"Language"}},"type":"object","required":["access_token"],"title":"FacebookSignInRequest","description":"Schema for Facebook sign-in via backend (reCAPTCHA bypass)."},"FeatureItemConfig":{"properties":{"icon":{"type":"string","title":"Icon"},"title":{"type":"string","title":"Title"},"description":{"type":"string","title":"Description"}},"type":"object","required":["icon","title","description"],"title":"FeatureItemConfig","description":"A feature in the expandable feature list."},"FeatureRequestPublic":{"properties":{"id":{"type":"string","title":"Id"},"description":{"type":"string","title":"Description"},"status":{"type":"string","title":"Status"},"vote_count":{"type":"integer","title":"Vote Count"},"has_voted":{"type":"boolean","title":"Has Voted","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","description","status","vote_count","created_at"],"title":"FeatureRequestPublic","description":"Public-roadmap card for a feature request."},"FeedbackCreate":{"properties":{"description":{"type":"string","maxLength":5000,"minLength":10,"title":"Description"},"category":{"type":"string","enum":["bug","feature_request","improvement","other"],"title":"Category","default":"bug"},"app_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Version"},"platform":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform"},"os_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Os Version"},"device_model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Model"},"subscription_tier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subscription Tier"}},"type":"object","required":["description"],"title":"FeedbackCreate","description":"Schema for creating a feedback entry."},"FeedbackResponse":{"properties":{"id":{"type":"string","title":"Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"github_issue_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Github Issue Url"}},"type":"object","required":["id","created_at"],"title":"FeedbackResponse","description":"Schema for feedback response."},"ForgotPasswordRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"language":{"type":"string","pattern":"^(fr|en|es|zh|pt|it|ht|ro|vi|el|tl|ru|hi|ko|pa|pl|de)$","title":"Language","default":"fr"}},"type":"object","required":["email"],"title":"ForgotPasswordRequest","description":"Request to send a password reset code."},"ForgotPasswordResponse":{"properties":{"message":{"type":"string","title":"Message"}},"type":"object","required":["message"],"title":"ForgotPasswordResponse","description":"Response after requesting a password reset code."},"FundRequest":{"properties":{"watch_days":{"type":"integer","minimum":1.0,"title":"Watch Days","description":"How many days of monitoring to buy. The billed unit is a WATCH-DAY, not a request: one cheap call here commits us to polling for that long. Already funded and only need a fresh token? Send 1 — funding extends, so you keep the days you have and the token covers all of them."}},"type":"object","required":["watch_days"],"title":"FundRequest"},"GeoAmenityMarker":{"properties":{"label":{"type":"string","maxLength":128,"title":"Label"},"icon_url":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"Icon Url"},"bg_color":{"anyOf":[{"type":"string","maxLength":9},{"type":"null"}],"title":"Bg Color"},"lat":{"type":"number","title":"Lat"},"lng":{"type":"number","title":"Lng"},"name":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Name"},"icon_name":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Icon Name"}},"type":"object","required":["label","lat","lng"],"title":"GeoAmenityMarker","description":"An amenity POI pinned on the geographic site picker.\n\nSame icon/label/backdrop fields as ``AmenityLegendEntry`` plus a\nlat/lng so the mobile map can place it; the legend key is derived\nclient-side from the distinct (label, icon) pairs."},"GoogleSignInRequest":{"properties":{"id_token":{"type":"string","minLength":1,"title":"Id Token","description":"Google OAuth ID token"},"language":{"anyOf":[{"type":"string","pattern":"^(fr|en|es|zh|pt|it|ht|ro|vi|el|tl|ru|hi|ko|pa|pl|de)$"},{"type":"null"}],"title":"Language"}},"type":"object","required":["id_token"],"title":"GoogleSignInRequest","description":"Schema for Google sign-in via backend (reCAPTCHA bypass)."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"$ref":"#/components/schemas/HealthStatus"},"app":{"type":"string","title":"App"},"version":{"type":"string","title":"Version"},"environment":{"type":"string","title":"Environment"},"uptime_seconds":{"type":"number","title":"Uptime Seconds"},"started_at":{"type":"string","title":"Started At"},"checked_at":{"type":"string","title":"Checked At"},"min_app_version":{"type":"string","title":"Min App Version"},"database":{"$ref":"#/components/schemas/DatabaseHealth"},"polling":{"$ref":"#/components/schemas/PollingHealth"},"circuit_breaker":{"anyOf":[{"$ref":"#/components/schemas/CircuitBreakerHealth"},{"type":"null"}]},"notifications":{"$ref":"#/components/schemas/NotificationHealth"},"cluster":{"anyOf":[{"$ref":"#/components/schemas/ClusterHealth"},{"type":"null"}]}},"type":"object","required":["status","app","version","environment","uptime_seconds","started_at","checked_at","min_app_version","database","polling","notifications"],"title":"HealthResponse","description":"Full health check response."},"HealthStatus":{"type":"string","enum":["healthy","degraded","unhealthy"],"title":"HealthStatus","description":"Overall health status of the application."},"HotSiteCountResponse":{"properties":{"total":{"type":"integer","title":"Total","description":"Active hot_sites matching provider + activity_type (+ optional park_id). No equipment/amenity filters applied."},"matching":{"type":"integer","title":"Matching","description":"Subset of ``total`` that also passes the supplied equipment + amenity filters. Equal to ``total`` when no filters are active."}},"type":"object","required":["total","matching"],"title":"HotSiteCountResponse","description":"Aggregate count of hot sites matching the wizard's filters."},"HotSpotAlertCreate":{"properties":{"park_id":{"type":"string","maxLength":64,"minLength":1,"title":"Park Id","description":"Specific park ID or '__all__' for all parks"},"park_name":{"type":"string","maxLength":255,"minLength":1,"title":"Park Name","description":"Display name or 'Tous les parcs'"},"provider":{"type":"string","maxLength":64,"minLength":1,"title":"Provider","description":"Provider for hotspot matching","default":"sepaq"},"date_start":{"type":"string","format":"date","title":"Date Start","description":"Broad window start (e.g., June 1)"},"date_end":{"type":"string","format":"date","title":"Date End","description":"Broad window end (e.g., Aug 31)"},"days_filter":{"type":"string","pattern":"^(all|weekends|weekdays)$","title":"Days Filter","default":"all"},"nights_min":{"type":"integer","maximum":14.0,"minimum":1.0,"title":"Nights Min","default":1},"notify_push":{"type":"boolean","title":"Notify Push","default":true},"notify_email":{"type":"boolean","title":"Notify Email","default":false},"notify_sms":{"type":"boolean","title":"Notify Sms","default":false},"activity_type":{"type":"string","pattern":"^(camping|chalet|canoe_camping|backcountry|fishing|boat|equestrian|day_use)$","title":"Activity Type"},"equipment_filter_mode":{"type":"string","pattern":"^(any|tent|rv)$","title":"Equipment Filter Mode","default":"any"},"sub_equipment_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sub Equipment Id"},"party_size":{"anyOf":[{"type":"integer","maximum":99.0,"minimum":1.0},{"type":"null"}],"title":"Party Size"},"has_electricity":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Electricity"},"is_accessible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Accessible"},"pets_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Pets Only"},"is_pull_through":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Pull Through"},"is_tent_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Tent Only"},"shade_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Shade Only"},"waterfront_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Waterfront Only"}},"type":"object","required":["park_id","park_name","date_start","date_end","activity_type"],"title":"HotSpotAlertCreate","description":"Schema for creating a hot spot watch alert.\n\nHot spot alerts subscribe users to cancellation notifications on\nhigh-demand stats-tier sites without needing specific campground\nselections.\n\nPush-only: Hot Spot alerts can generate 50-300 notifications/day,\nso email and SMS are always disabled."},"HourOfDayStats":{"properties":{"hour":{"type":"integer","title":"Hour"},"cancellation_count":{"type":"integer","title":"Cancellation Count"}},"type":"object","required":["hour","cancellation_count"],"title":"HourOfDayStats","description":"Cancellation counts by hour of the day (Eastern time)."},"ItineraryCreate":{"properties":{"park_id":{"type":"string","maxLength":64,"minLength":1,"title":"Park Id"},"nights":{"items":{"$ref":"#/components/schemas/ItineraryNightCreate"},"type":"array","maxItems":14,"minItems":1,"title":"Nights"},"window_start":{"type":"string","format":"date","title":"Window Start"},"window_end":{"type":"string","format":"date","title":"Window End"},"route_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Route Id"},"party_size":{"anyOf":[{"type":"integer","maximum":99.0,"minimum":1.0},{"type":"null"}],"title":"Party Size"},"tent_pads":{"anyOf":[{"type":"integer","maximum":50.0,"minimum":1.0},{"type":"null"}],"title":"Tent Pads"},"notify_push":{"type":"boolean","title":"Notify Push","default":true},"notify_email":{"type":"boolean","title":"Notify Email","default":true},"notify_sms":{"type":"boolean","title":"Notify Sms","default":false}},"type":"object","required":["park_id","nights","window_start","window_end"],"title":"ItineraryCreate","description":"Create a multi-night route alert."},"ItineraryListResponse":{"properties":{"itineraries":{"items":{"$ref":"#/components/schemas/ItineraryResponse"},"type":"array","title":"Itineraries"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["itineraries","total"],"title":"ItineraryListResponse"},"ItineraryNightCreate":{"properties":{"campground_id":{"type":"string","maxLength":128,"minLength":1,"title":"Campground Id"},"site_ids":{"items":{"type":"string"},"type":"array","maxItems":50,"minItems":1,"title":"Site Ids"}},"type":"object","required":["campground_id","site_ids"],"title":"ItineraryNightCreate","description":"One night: the OR-set of zones that would satisfy it.\n\n``site_ids`` are zone-resource site ids.  The set is an OR: the night is\nsatisfied when ANY of them is open, which is what lets a user say \"night 2\ncan be Numa Creek or Tumbling Creek\" and get one alert instead of two."},"ItineraryNightResponse":{"properties":{"night_index":{"type":"integer","title":"Night Index"},"campground_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campground Id"},"site_ids":{"items":{"type":"string"},"type":"array","title":"Site Ids","default":[]},"site_names":{"items":{"type":"string"},"type":"array","title":"Site Names","default":[]},"date_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Start"},"date_end":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date End"}},"type":"object","required":["night_index"],"title":"ItineraryNightResponse"},"ItineraryResponse":{"properties":{"id":{"type":"string","title":"Id"},"park_id":{"type":"string","title":"Park Id"},"park_name":{"type":"string","title":"Park Name"},"provider":{"type":"string","title":"Provider"},"route_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Route Id"},"window_start":{"type":"string","format":"date","title":"Window Start"},"window_end":{"type":"string","format":"date","title":"Window End"},"nights":{"type":"integer","title":"Nights"},"party_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Party Size"},"tent_pads":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tent Pads"},"is_active":{"type":"boolean","title":"Is Active"},"booking_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Booking Url"},"notify_push":{"type":"boolean","title":"Notify Push","default":true},"notify_email":{"type":"boolean","title":"Notify Email","default":true},"notify_sms":{"type":"boolean","title":"Notify Sms","default":false},"notification_count":{"type":"integer","title":"Notification Count"},"last_notified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Notified At"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"created_via":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created Via"},"created_by_agent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By Agent"},"scan_count":{"type":"integer","title":"Scan Count","default":0},"last_checked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Checked At"},"availabilities_found":{"type":"integer","title":"Availabilities Found","default":0},"open_routes":{"items":{"$ref":"#/components/schemas/OpenRoute"},"type":"array","title":"Open Routes","default":[]},"night_openings_recent":{"type":"integer","title":"Night Openings Recent","default":0},"night_openings_latest":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Night Openings Latest"},"distinct_zone_count":{"type":"integer","title":"Distinct Zone Count","default":0},"legs":{"items":{"$ref":"#/components/schemas/ItineraryNightResponse"},"type":"array","title":"Legs","default":[]}},"type":"object","required":["id","park_id","park_name","provider","window_start","window_end","nights","is_active","notification_count"],"title":"ItineraryResponse"},"ItineraryUpdate":{"properties":{"notify_push":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Notify Push"},"notify_email":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Notify Email"},"notify_sms":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Notify Sms"},"window_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Window Start"},"window_end":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Window End"},"party_size":{"anyOf":[{"type":"integer","maximum":99.0,"minimum":1.0},{"type":"null"}],"title":"Party Size"},"tent_pads":{"anyOf":[{"type":"integer","maximum":50.0,"minimum":1.0},{"type":"null"}],"title":"Tent Pads"}},"type":"object","title":"ItineraryUpdate","description":"Partial update of a route: delivery channels and the search window.\n\nThe NIGHTS are not editable — their order and adjacency are validated as a\nwhole at create time (walkable hops, a trailhead on night 1), so changing\none in isolation could leave a route the provider would refuse to book.\nChanging those means creating a new route.\n\nThe window is different: shifting or widening it re-derives every leg's\nrange mechanically and cannot invalidate the sequence.  Widening it is also\nthe single most useful edit — more candidate start days is more chances to\nfire — which is exactly why it is editable on an ordinary alert too.\n\n``party_size`` and ``tent_pads`` are editable for the same reason they are\non an ordinary alert — and for routes they are not cosmetic: both feed the\ncapacity gate (a night's zone must have ``max(tent_pads, party_size, 1)``\nunits free).  The handler mirrors any change onto every leg and re-baselines\nthe route so a looser requirement can fire again instead of staying\nsuppressed by the prior signature."},"LeadTimeBucket":{"properties":{"bucket":{"type":"string","title":"Bucket"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["bucket","count"],"title":"LeadTimeBucket","description":"Lead time distribution bucket."},"MCPTokenCreate":{"properties":{"name":{"type":"string","maxLength":100,"title":"Name","default":""},"scopes":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Scopes","description":"Subset of alerts:read / alerts:write. Default: both."}},"type":"object","title":"MCPTokenCreate"},"MCPTokenCreated":{"properties":{"jti":{"type":"string","title":"Jti"},"name":{"type":"string","title":"Name"},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id","description":"The client's CIMD URL, for a token minted through the OAuth flow. `name` is the client's own claim about itself; this is the part that was verified."},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At"},"revoked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Revoked At"},"token":{"type":"string","title":"Token","description":"Shown ONCE. Nothing recoverable is stored, so a lost token is re-minted rather than retrieved."}},"type":"object","required":["jti","name","scopes","created_at","expires_at","last_used_at","revoked_at","token"],"title":"MCPTokenCreated"},"MCPTokenInfo":{"properties":{"jti":{"type":"string","title":"Jti"},"name":{"type":"string","title":"Name"},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id","description":"The client's CIMD URL, for a token minted through the OAuth flow. `name` is the client's own claim about itself; this is the part that was verified."},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At"},"revoked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Revoked At"}},"type":"object","required":["jti","name","scopes","created_at","expires_at","last_used_at","revoked_at"],"title":"MCPTokenInfo","description":"A token's record. Never carries the token itself — it is\nunrecoverable after issue, by design."},"MapNodeAmenity":{"properties":{"legend_type":{"type":"integer","title":"Legend Type"},"x":{"type":"integer","title":"X"},"y":{"type":"integer","title":"Y"},"r":{"type":"integer","title":"R","default":0},"g":{"type":"integer","title":"G","default":0},"b":{"type":"integer","title":"B","default":0},"icon_asset":{"type":"string","title":"Icon Asset","description":"Filename under mobile/assets/legend_icons/ — empty when the upstream uses iconType=16 (badge-only, no glyph).","default":""}},"type":"object","required":["legend_type","x","y"],"title":"MapNodeAmenity","description":"One amenity icon on a map (point overlay)."},"MapNodeChildLink":{"properties":{"child_map_id":{"type":"integer","title":"Child Map Id","description":"GTC mapId of the child"},"x":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"X","description":"Pixel x on the parent image"},"y":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Y","description":"Pixel y on the parent image"},"label_en":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label En"},"label_fr":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label Fr"},"target_kind":{"type":"string","title":"Target Kind","description":"'campground_map' = child is a leaf zone map already exposed via the existing /campgrounds/{id}/maps endpoint (mobile hands off to the SiteMapPicker).  'node' = child is another MapNode in the tree (mobile drills deeper)."},"target_campground_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Campground Id","description":"Campground PK when target_kind='campground_map'."}},"type":"object","required":["child_map_id","target_kind"],"title":"MapNodeChildLink","description":"Single drill-down target rendered as a click-circle on a parent\nmap's image."},"MapNodeItem":{"properties":{"id":{"type":"string","title":"Id"},"park_id":{"type":"string","title":"Park Id"},"gtc_map_id":{"type":"integer","title":"Gtc Map Id"},"gtc_map_type":{"type":"integer","title":"Gtc Map Type"},"parent_gtc_map_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parent Gtc Map Id"},"leaf_campground_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Leaf Campground Id","description":"Campground PK when this MapNode IS a leaf — mobile uses it to fetch the leaf's full CampgroundMaps + sites and hand off to the existing SiteMapPickerScreen with labels, long-press preview, (i) legend, etc."},"is_root":{"type":"boolean","title":"Is Root","description":"True for the entry-point node — typically the park-level map (matches Park.op_park_map_id).  Mobile picker opens this one by default."},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url","description":"Proxied URL for the cached PNG, served from our endpoint to bypass upstream TLS chains.  Falls back to the raw upstream URL when bytes aren't cached."},"width":{"type":"integer","title":"Width"},"height":{"type":"integer","title":"Height"},"name_en":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name En"},"name_fr":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name Fr"},"child_links":{"items":{"$ref":"#/components/schemas/MapNodeChildLink"},"type":"array","title":"Child Links"},"sites":{"items":{"$ref":"#/components/schemas/MapNodeSiteDot"},"type":"array","title":"Sites","description":"Site dots — populated for leaf maps that carry GTC mapResources entries.  Empty on intermediate maps."},"amenities":{"items":{"$ref":"#/components/schemas/MapNodeAmenity"},"type":"array","title":"Amenities","description":"Amenity icon overlays — present on both leaf and non-leaf maps (intermediate maps still draw beach / visitor centre / etc. markers)."},"amenity_legend":{"items":{"$ref":"#/components/schemas/AmenityLegendEntry"},"type":"array","title":"Amenity Legend","description":"Amenity legend key (icon + name) for providers whose icons are baked into the map artwork (Alberta) — lets the root node show the same legend as its leaf maps."}},"type":"object","required":["id","park_id","gtc_map_id","gtc_map_type","parent_gtc_map_id","is_root","width","height"],"title":"MapNodeItem","description":"A single non-leaf map node in the park's drill-down tree."},"MapNodeSiteDot":{"properties":{"site_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Id","description":"Our Site PK.  Null when the upstream marker doesn't match a Site row (typically excluded categories like group / seasonal sites that we filter at seed time but the upstream still draws on the map)."},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label","description":"Site number / name (e.g. 'B4-72', '142')."},"x":{"type":"integer","title":"X"},"y":{"type":"integer","title":"Y"},"is_hot":{"type":"boolean","title":"Is Hot","default":false},"has_image":{"type":"boolean","title":"Has Image","description":"True when the matching Site row has a cached photo — drives SiteMapPicker's long-press preview affordance.","default":false}},"type":"object","required":["x","y"],"title":"MapNodeSiteDot","description":"One site marker on a leaf map."},"MonthStats":{"properties":{"month":{"type":"integer","title":"Month"},"cancellation_count":{"type":"integer","title":"Cancellation Count"}},"type":"object","required":["month","cancellation_count"],"title":"MonthStats","description":"Cancellation counts by month (1=January ... 12=December)."},"NotificationHealth":{"properties":{"total":{"type":"integer","title":"Total","default":0},"push_sent":{"type":"integer","title":"Push Sent","default":0},"push_failed":{"type":"integer","title":"Push Failed","default":0},"email_sent":{"type":"integer","title":"Email Sent","default":0},"email_failed":{"type":"integer","title":"Email Failed","default":0},"sms_sent":{"type":"integer","title":"Sms Sent","default":0},"sms_failed":{"type":"integer","title":"Sms Failed","default":0}},"type":"object","title":"NotificationHealth","description":"Notification service stats."},"OpenRoute":{"properties":{"start_date":{"type":"string","format":"date","title":"Start Date"},"end_date":{"type":"string","format":"date","title":"End Date"},"nights":{"items":{"$ref":"#/components/schemas/OpenRouteNight"},"type":"array","title":"Nights","default":[]},"booking_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Booking Url"},"webview_inject":{"type":"boolean","title":"Webview Inject","default":true},"is_new":{"type":"boolean","title":"Is New","default":true},"first_seen_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"First Seen At"},"expired_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expired At"},"last_seen_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Seen At"},"booking_closed":{"type":"boolean","title":"Booking Closed","default":false},"booking_deadline":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Booking Deadline"}},"type":"object","required":["start_date","end_date"],"title":"OpenRoute","description":"One start day on which the WHOLE route is currently bookable.\n\nA route's availability is the whole route, so this — not a per-night\nopening — is what the detail screen offers and what \"Book this route\"\nacts on.  Several can be open at once; a night is an OR-set, so the zone\nsatisfying a night may differ between start days."},"OpenRouteNight":{"properties":{"night_index":{"type":"integer","title":"Night Index"},"site_id":{"type":"string","title":"Site Id"},"site_name":{"type":"string","title":"Site Name"}},"type":"object","required":["night_index","site_id","site_name"],"title":"OpenRouteNight"},"OutOfSeasonHint":{"properties":{"season_start":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Season Start"},"season_end":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Season End"},"distinct_seasons":{"type":"integer","title":"Distinct Seasons"},"sites_checked":{"type":"integer","title":"Sites Checked"}},"type":"object","required":["distinct_seasons","sites_checked"],"title":"OutOfSeasonHint","description":"Why an alert's dates cannot match: every watched site is out of season.\n\nThe dates are reported ONLY when every watched site shares one season, and\nare NULL otherwise.  An aggregate across differing seasons contradicts its\nown message: a site open Jan-Mar beside one open Sep-Dec spans the whole\nyear, so a June request would be told it falls outside \"Jan 1 - Dec 31\".\nWidening to the union, or narrowing to the intersection, is wrong in the\nsame way — only unanimity can be stated as a single range.\n\n``distinct_seasons`` lets the app choose its wording: one season gets\n\"this park runs May-October\", several get \"none of the N sites you picked\nare open then\".  ``sites_checked`` counts how much of the selection had a\nknown season at all, so the app can soften when it is a small fraction."},"ParkCancellationStats":{"properties":{"park_id":{"type":"string","title":"Park Id"},"park_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Park Name"},"cancellation_count":{"type":"integer","title":"Cancellation Count"}},"type":"object","required":["park_id","park_name","cancellation_count"],"title":"ParkCancellationStats","description":"Per-park cancellation counts."},"ParkListResponse":{"properties":{"parks":{"items":{"$ref":"#/components/schemas/ParkResponse"},"type":"array","title":"Parks"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["parks","total"],"title":"ParkListResponse","description":"Schema for list of parks response."},"ParkMapTreeResponse":{"properties":{"park_id":{"type":"string","title":"Park Id"},"nodes":{"items":{"$ref":"#/components/schemas/MapNodeItem"},"type":"array","title":"Nodes"}},"type":"object","required":["park_id","nodes"],"title":"ParkMapTreeResponse","description":"All non-leaf MapNodes belonging to a single park."},"ParkRebookingSpeed":{"properties":{"park_id":{"type":"string","title":"Park Id"},"park_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Park Name"},"avg_rebooking_delay_minutes":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Rebooking Delay Minutes"},"median_rebooking_delay_minutes":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Median Rebooking Delay Minutes"},"sample_size":{"type":"integer","title":"Sample Size"}},"type":"object","required":["park_id","park_name","avg_rebooking_delay_minutes","median_rebooking_delay_minutes","sample_size"],"title":"ParkRebookingSpeed","description":"Rebooking speed metrics per park."},"ParkResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"provider":{"type":"string","title":"Provider","default":"sepaq"},"camp_types":{"items":{"type":"string"},"type":"array","title":"Camp Types"},"website_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website Url"},"is_active":{"type":"boolean","title":"Is Active"},"is_hot":{"type":"boolean","title":"Is Hot","default":false},"hot_camp_types":{"items":{"type":"string"},"type":"array","title":"Hot Camp Types","description":"Subset of camp_types that has at least one active hot_site. Used by the mobile picker / activity step in hotspot mode to show only the activity buckets that will actually trigger notifications. Empty when the park has no active hot_sites."},"latitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Latitude","description":"Latitude in decimal degrees (WGS84). NULL for parks whose upstream provider doesn't expose coordinates."},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Longitude","description":"Longitude in decimal degrees (WGS84). NULL for parks whose upstream provider doesn't expose coordinates."},"campground_count":{"type":"integer","title":"Campground Count","description":"Number of bookable areas inside this park. Powers the federated park-discovery picker's bottom-sheet metadata so the user can see park 'size' before committing to the wizard. UNCHANGED and still the TOTAL — see wilderness_count, which says how many of these are wilderness rather than campgrounds.","default":0},"site_count":{"type":"integer","title":"Site Count","description":"Number of active bookable sites in this park. A park with exactly ONE (an Alpine Club of Canada hut) has nothing to choose, so the mobile wizard skips its accommodations step. Additive: older clients ignore it.","default":0},"wilderness_count":{"type":"integer","title":"Wilderness Count","description":"How many of `campground_count` are WILDERNESS rather than campgrounds — a backcountry permit or zone, which the card used to call a campground (Wichita Mountains' 'three campgrounds' were two campgrounds and a one-site wilderness permit). Always <= campground_count, and counted at the same level: by zone for a parks-as-campgrounds tenant, by campground otherwise, so the two numbers add up. An area holding both kinds counts as whichever it mostly is. Additive: a client that ignores it still shows the correct total.","default":0},"backcountry_is_zone":{"type":"boolean","title":"Backcountry Is Zone","description":"TRUE when this park's backcountry inventory is zone-quota (GTC bcat=7/13/14/17 — Egypt Lake, West Coast Trail, …) rather than the eastern per-site Backcountry-Campsite flow (bcat=5).  The two are disjoint per park (spec §8), so a single boolean is well-defined.  Drives the wizard/alert-detail backcountry EQUIPMENT picker (hidden for zone parks — zones have no equipment vocabulary, only tent-pads/party-size) so the platform-wide has_backcountry_equipment_picker isn't applied to zone parks.","default":false},"backcountry_has_release_schedule":{"type":"boolean","title":"Backcountry Has Release Schedule","description":"TRUE when at least one of this park's zones publishes a per-cell quota RELEASE time, so a 'remind me before it opens' opt-in can actually fire.  Recreation.gov's family-B wilderness permits do (not_yet_released + release_date); its family-A permits publish nothing of the sort, and GoingToCamp publishes nothing at all.  ANY semantics, unlike backcountry_is_entry_date below: a park holding one permit of each family still shows the control, and the alert-level gate at creation refuses it for a selection that cannot honour it.  Gate the toggle on this AND the provider's has_release_schedule.  Defaults FALSE, so a build that predates the field simply never offers the reminder.","default":false},"backcountry_is_entry_date":{"type":"boolean","title":"Backcountry Is Entry Date","description":"TRUE when this park's backcountry is ENTRY-DATE inventory and nothing else (Quetico: you reserve one entry point for your entry DATE and camp anywhere; asking for 1 night and 5 returns identical availability).  ALL semantics — a park with any per-night backcountry alongside it reads FALSE, because that part can still be sequenced.  Mobile labels the date range as ENTRY DATES and the nights stepper as TRIP LENGTH, since nights are a booking parameter here rather than an availability filter.  Server-derived for the same reason as supports_routes: which parks are entry-date is a data question that must be able to change without a mobile release.","default":false},"supports_routes":{"type":"boolean","title":"Supports Routes","description":"TRUE when a multi-night ROUTE alert can be built for this park. Computed server-side from the provider's supports_routes capability AND this park having route-capable backcountry inventory. Deliberately a PARK field rather than a client-side derivation: which parks of an enabled tenant can actually hold a route is a server decision, so widening it (say from zone-quota parks to per-site ones) ships without a mobile release. Distinct from backcountry_is_zone, which answers the unrelated question of whether to show tent pads.","default":false},"op_transaction_location_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Transaction Location Id","description":"GoingToCamp transactionLocationId for this park. Used by the mobile picker to build a per-park reservation URL (?transactionLocationId={id}) so the 'View on website' link lands with the correct park pre-selected in the dropdown. NULL for SEPAQ (different reservation flow)."},"op_park_map_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Park Map Id","description":"GoingToCamp park-level mapId, resolved at seed time by walking the provider's map tree.  When set, the mobile picker uses it as the SPA deep-link target so the GTC reservation page lands on this park's view (?mapId={op_park_map_id}&...) instead of the All-Parks root.  NULL for SEPAQ and for any GTC park whose TL doesn't appear in the upstream mapLinks tree."},"op_resource_location_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Resource Location Id","description":"GoingToCamp resourceLocationId of the park's sole campground.  Populated only when the park has exactly one active campground (the parks-as-campgrounds shape — every OP park, every MB park, 114/115 BC parks, 13/34 PC parks).  The mobile picker appends ``&resourceLocationId={...}&view=map`` to the GTC deep-link URL so the SPA renders the campground map view; without it OP/BC parks land on All-Parks even when ``mapId`` is set.  NULL for multi-campground parks (most PC national parks, Wells Gray, NCC) where the user must pick a campground at the overview level."},"booking_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Booking Url","description":"Pre-filled, park-specific reservation deep link, computed server-side.  Populated only for Aspira/ReserveAmerica providers (Alberta, Saskatchewan), whose ``parkId`` is a numeric ``aspira_park_id`` the mobile client doesn't carry — so the client's ``reservation_url_template`` substitution (which fills ``{park_id}`` with the string park id) would land on the provider's homepage gate.  When set, the mobile 'View on website' link uses this verbatim.  NULL for SEPAQ/GoingToCamp, where the client builds the link from the template / op_park_map_id."},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","name","website_url","is_active","created_at","updated_at"],"title":"ParkResponse","description":"Schema for park response."},"ParkSiteListResponse":{"properties":{"sites":{"items":{"$ref":"#/components/schemas/SiteResponse"},"type":"array","title":"Sites"},"total":{"type":"integer","title":"Total"},"park_id":{"type":"string","title":"Park Id"},"zone_campgrounds":{"additionalProperties":{"type":"string"},"type":"object","title":"Zone Campgrounds","description":"zone_id -> campground_id, for every zone represented"}},"type":"object","required":["sites","total","park_id"],"title":"ParkSiteListResponse","description":"Every site in a park, in ONE response.\n\nThe per-campground route answers the same data in one call per campground,\nand the fixed cost of a call (~0.18s warm: routing, the campground / park /\nzone lookups, the hot-site batch, response construction) is ~150x the cost\nof a single site (~1.2ms).  So a 78-campground park like Deschutes NF spent\n~14s of its ~15.6s paying that fixed cost 78 times over.  This route pays it\nonce, which is the whole point of it.\n\n``zone_campgrounds`` is what makes the collapse lossless: the caller still\nneeds to know which campground a site belongs to (the wizard filters\nper-campground, and the activity rules differ by campground), and\n``SiteResponse`` carries only ``zone_id``.  Sending the mapping once beats\nwidening every site row with a campground id that repeats.\n\nNot a frozen surface: no shipped build calls it, and callers intersect\nthese sites with the campground list they already fetched from their own\n(frozen) campgrounds route, so a campground hidden there stays hidden."},"PendingRequestInfo":{"properties":{"user_code":{"type":"string","title":"User Code"},"client_name":{"type":"string","title":"Client Name","description":"From the client's own metadata document — display it as an untrusted claim, not as a verified identity."},"client_id":{"type":"string","title":"Client Id"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes"}},"type":"object","required":["user_code","client_name","client_id","scopes"],"title":"PendingRequestInfo","description":"What the app shows before asking \"approve?\"."},"PhoneVerificationRequest":{"properties":{"phone_number":{"type":"string","title":"Phone Number","description":"E.164 phone number"}},"type":"object","required":["phone_number"],"title":"PhoneVerificationRequest","description":"Schema for requesting phone verification."},"PhoneVerificationResponse":{"properties":{"message":{"type":"string","title":"Message"},"expires_in_seconds":{"type":"integer","title":"Expires In Seconds"}},"type":"object","required":["message","expires_in_seconds"],"title":"PhoneVerificationResponse","description":"Response after requesting phone verification."},"PhoneVerificationVerify":{"properties":{"code":{"type":"string","maxLength":6,"minLength":6,"title":"Code"}},"type":"object","required":["code"],"title":"PhoneVerificationVerify","description":"Schema for verifying a phone code."},"PhoneVerifiedResponse":{"properties":{"phone_number":{"type":"string","title":"Phone Number"},"verified":{"type":"boolean","title":"Verified"}},"type":"object","required":["phone_number","verified"],"title":"PhoneVerifiedResponse","description":"Response after successfully verifying a phone number."},"PingResponse":{"properties":{"status":{"type":"string","title":"Status","default":"ok"}},"type":"object","title":"PingResponse","description":"Minimal liveness probe response."},"PlatformListResponse":{"properties":{"platforms":{"items":{"$ref":"#/components/schemas/PlatformResponse"},"type":"array","title":"Platforms"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["platforms","total"],"title":"PlatformListResponse","description":"Wrapper response for ``GET /platforms``."},"PlatformResponse":{"properties":{"id":{"type":"string","maxLength":50,"title":"Id"},"name":{"type":"string","maxLength":100,"title":"Name"},"scanner_name":{"type":"string","maxLength":50,"title":"Scanner Name"},"quota_bucket":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Quota Bucket"},"country":{"type":"string","maxLength":10,"title":"Country"},"base_booking_url":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Base Booking Url"},"website_url":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Website Url"},"is_active":{"type":"boolean","title":"Is Active","default":true},"show_quota_banner":{"type":"boolean","title":"Show Quota Banner","default":true},"immediate_poll_all":{"type":"boolean","title":"Immediate Poll All","default":false},"has_equipment_filter":{"type":"boolean","title":"Has Equipment Filter","default":false},"has_pets_filter":{"type":"boolean","title":"Has Pets Filter","default":false},"has_electricity_data":{"type":"boolean","title":"Has Electricity Data","default":false},"has_accessibility_data":{"type":"boolean","title":"Has Accessibility Data","default":false},"has_pull_through_data":{"type":"boolean","title":"Has Pull Through Data","default":false},"has_split_stay":{"type":"boolean","title":"Has Split Stay","default":false},"has_cross_listings":{"type":"boolean","title":"Has Cross Listings","default":false},"requires_paid_tier":{"type":"boolean","title":"Requires Paid Tier","default":false},"requires_url":{"type":"boolean","title":"Requires Url","default":true},"requires_resource_id":{"type":"boolean","title":"Requires Resource Id","default":false},"booking_roofed_tab_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Booking Roofed Tab Id"},"booking_roofed_category_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Booking Roofed Category Id"},"booking_camping_tab_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Booking Camping Tab Id"},"booking_camping_category_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Booking Camping Category Id"},"hides_party_size_for_camping":{"type":"boolean","title":"Hides Party Size For Camping","default":false},"has_van_equipment_category":{"type":"boolean","title":"Has Van Equipment Category","default":false},"icon_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Icon Key"},"expected_url_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expected Url Domain"},"reservation_url_template":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reservation Url Template"},"has_site_map":{"type":"boolean","title":"Has Site Map","default":false},"has_geographic_site_picker":{"type":"boolean","title":"Has Geographic Site Picker","default":false},"has_geo_campground_photo_site_picker":{"type":"boolean","title":"Has Geo Campground Photo Site Picker","default":false},"has_canoe_camping":{"type":"boolean","title":"Has Canoe Camping","default":false},"has_backcountry":{"type":"boolean","title":"Has Backcountry","default":false},"has_day_use":{"type":"boolean","title":"Has Day Use","default":false},"has_backcountry_zone":{"type":"boolean","title":"Has Backcountry Zone","default":false},"has_entry_date_quota":{"type":"boolean","title":"Has Entry Date Quota","default":false},"has_release_schedule":{"type":"boolean","title":"Has Release Schedule","default":false},"fetch_is_month_bucketed":{"type":"boolean","title":"Fetch Is Month Bucketed","default":false},"has_sequence_constraint":{"type":"boolean","title":"Has Sequence Constraint","default":false},"supports_routes":{"type":"boolean","title":"Supports Routes","default":false},"zone_is_trip_unit":{"type":"boolean","title":"Zone Is Trip Unit","default":false},"party_size_gates_inventory":{"type":"boolean","title":"Party Size Gates Inventory","default":false},"webview_booking":{"type":"boolean","title":"Webview Booking","default":false},"webview_route_booking":{"type":"boolean","title":"Webview Route Booking","default":false},"webview_inject_script":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webview Inject Script"},"webview_preseed_origin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webview Preseed Origin"},"webview_user_agent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webview User Agent"},"backcountry_has_equipment_filter":{"type":"boolean","title":"Backcountry Has Equipment Filter","default":false},"backcountry_equipment_category_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Backcountry Equipment Category Id"},"canoe_camping_equipment_category_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Canoe Camping Equipment Category Id"},"canoe_camping_has_equipment_filter":{"type":"boolean","title":"Canoe Camping Has Equipment Filter","default":false},"has_hot_spot":{"type":"boolean","title":"Has Hot Spot","default":true},"max_date_range_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Date Range Days"},"max_stay_nights":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Stay Nights"},"date_range_cap_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Date Range Cap Enabled"},"max_advance_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Advance Days"},"advance_days_cap_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Advance Days Cap Enabled"},"monitoring_disabled":{"type":"boolean","title":"Monitoring Disabled","default":false}},"type":"object","required":["id","name","scanner_name","country"],"title":"PlatformResponse","description":"Public-facing platform metadata returned by ``GET /platforms``."},"PollingBacklog":{"properties":{"sites_tracked":{"type":"integer","title":"Sites Tracked","default":0},"sites_overdue":{"type":"integer","title":"Sites Overdue","default":0},"max_overdue_seconds":{"type":"number","title":"Max Overdue Seconds","default":0.0},"max_overdue_ratio":{"type":"number","title":"Max Overdue Ratio","default":0.0},"max_overdue_tier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Max Overdue Tier"}},"type":"object","title":"PollingBacklog","description":"Polling backlog metrics — shows whether the backend can keep up."},"PollingHealth":{"properties":{"status":{"$ref":"#/components/schemas/ComponentStatus"},"task_running":{"type":"boolean","title":"Task Running"},"last_poll_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Poll At"},"seconds_since_last_poll":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Seconds Since Last Poll"},"sites_polled_total":{"type":"integer","title":"Sites Polled Total","default":0},"sites_successful":{"type":"integer","title":"Sites Successful","default":0},"sites_failed":{"type":"integer","title":"Sites Failed","default":0},"sites_skipped":{"type":"integer","title":"Sites Skipped","default":0},"cancellations_detected":{"type":"integer","title":"Cancellations Detected","default":0},"alerts_matched":{"type":"integer","title":"Alerts Matched","default":0},"api_calls_total":{"type":"integer","title":"Api Calls Total","default":0},"success_rate":{"type":"number","title":"Success Rate","default":0.0},"sites_polled_by_tier":{"additionalProperties":{"type":"integer"},"type":"object","title":"Sites Polled By Tier","default":{}},"backlog":{"anyOf":[{"$ref":"#/components/schemas/PollingBacklog"},{"type":"null"}]},"stats_backlog":{"anyOf":[{"$ref":"#/components/schemas/PollingBacklog"},{"type":"null"}]},"stress_mode":{"additionalProperties":{"additionalProperties":{"type":"boolean"},"type":"object"},"type":"object","title":"Stress Mode","default":{}},"maintenance_mode":{"type":"boolean","title":"Maintenance Mode","default":false},"maintenance_since":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Maintenance Since"},"maintenance_providers":{"items":{"type":"string"},"type":"array","title":"Maintenance Providers","default":[]},"maintenance_since_by_provider":{"additionalProperties":{"type":"string"},"type":"object","title":"Maintenance Since By Provider","default":{}},"provider_status":{"additionalProperties":{"type":"string"},"type":"object","title":"Provider Status","default":{}},"provider_tier_status":{"additionalProperties":{"additionalProperties":{"type":"string"},"type":"object"},"type":"object","title":"Provider Tier Status","default":{}},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["status","task_running"],"title":"PollingHealth","description":"Polling loop component health."},"ProviderConfig":{"properties":{"show_quota_banner":{"type":"boolean","title":"Show Quota Banner","default":true}},"type":"object","title":"ProviderConfig","description":"Provider-level configuration flags."},"ProviderSiteOverride":{"properties":{"max_sites_per_alert":{"type":"integer","title":"Max Sites Per Alert"},"max_total_sites":{"type":"integer","title":"Max Total Sites"}},"type":"object","required":["max_sites_per_alert","max_total_sites"],"title":"ProviderSiteOverride","description":"Site limit overrides for a provider."},"PublicStatsResponse":{"properties":{"cancellation_alerts_sent":{"type":"integer","title":"Cancellation Alerts Sent"},"active_alerts":{"type":"integer","title":"Active Alerts"},"campers":{"type":"integer","title":"Campers"},"since":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Since"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["cancellation_alerts_sent","active_alerts","campers","since","updated_at"],"title":"PublicStatsResponse","description":"Public marketing stats — safe to expose unauthenticated on the website.\n\nAll figures are aggregate, contain no user-identifying data, and are\nintended for headline copy (e.g. \"we've sent campers N cancellation\nalerts\")."},"QuotaUsageResponse":{"properties":{"active_alerts":{"type":"integer","title":"Active Alerts"},"total_alerts":{"type":"integer","title":"Total Alerts"},"total_sites":{"type":"integer","title":"Total Sites"},"total_sites_by_bucket":{"additionalProperties":{"type":"integer"},"type":"object","title":"Total Sites By Bucket"}},"type":"object","required":["active_alerts","total_alerts","total_sites"],"title":"QuotaUsageResponse","description":"Schema for current quota usage."},"RefreshTokenRequest":{"properties":{"refresh_token":{"type":"string","minLength":1,"title":"Refresh Token"}},"type":"object","required":["refresh_token"],"title":"RefreshTokenRequest","description":"Request to refresh an expired ID token."},"RefreshTokenResponse":{"properties":{"id_token":{"type":"string","title":"Id Token"},"refresh_token":{"type":"string","title":"Refresh Token"}},"type":"object","required":["id_token","refresh_token"],"title":"RefreshTokenResponse","description":"Response containing refreshed tokens."},"ResendVerificationResponse":{"properties":{"message":{"type":"string","title":"Message"}},"type":"object","required":["message"],"title":"ResendVerificationResponse","description":"Response after resending verification email."},"ResetPasswordRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"code":{"type":"string","maxLength":6,"minLength":6,"pattern":"^\\d{6}$","title":"Code"},"new_password":{"type":"string","minLength":6,"title":"New Password"}},"type":"object","required":["email","code","new_password"],"title":"ResetPasswordRequest","description":"Request to reset password with a verified code."},"ResetPasswordResponse":{"properties":{"message":{"type":"string","title":"Message"}},"type":"object","required":["message"],"title":"ResetPasswordResponse","description":"Response after successfully resetting the password."},"RouteCatalogResponse":{"properties":{"park_id":{"type":"string","title":"Park Id"},"trails":{"items":{"$ref":"#/components/schemas/TrailSummary"},"type":"array","title":"Trails","default":[]},"total":{"type":"integer","title":"Total"}},"type":"object","required":["park_id","total"],"title":"RouteCatalogResponse"},"RouteNightSummary":{"properties":{"night_index":{"type":"integer","title":"Night Index"},"campground_id":{"type":"string","title":"Campground Id"},"site_ids":{"items":{"type":"string"},"type":"array","title":"Site Ids"},"site_names":{"items":{"type":"string"},"type":"array","title":"Site Names","default":[]},"map_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Map Id"},"site_map_x":{"items":{"anyOf":[{"type":"integer"},{"type":"null"}]},"type":"array","title":"Site Map X","default":[]},"site_map_y":{"items":{"anyOf":[{"type":"integer"},{"type":"null"}]},"type":"array","title":"Site Map Y","default":[]},"site_lat":{"items":{"anyOf":[{"type":"number"},{"type":"null"}]},"type":"array","title":"Site Lat","default":[]},"site_lng":{"items":{"anyOf":[{"type":"number"},{"type":"null"}]},"type":"array","title":"Site Lng","default":[]}},"type":"object","required":["night_index","campground_id","site_ids"],"title":"RouteNightSummary"},"RouteSummary":{"properties":{"id":{"type":"string","title":"Id"},"park_id":{"type":"string","title":"Park Id"},"name":{"type":"string","title":"Name"},"nights":{"type":"integer","title":"Nights"},"trail_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trail Key"},"variant_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Variant Label"},"distance_km":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Distance Km"},"trip_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trip Type"},"map_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Map Id"},"night_rows":{"items":{"$ref":"#/components/schemas/RouteNightSummary"},"type":"array","title":"Night Rows","default":[]}},"type":"object","required":["id","park_id","name","nights"],"title":"RouteSummary","description":"One pace of a trail — 'Rockwall, 4 nights'."},"SeasonSpan":{"properties":{"start":{"type":"string","title":"Start","description":"Season start as \"MM-DD\"."},"end":{"type":"string","title":"End","description":"Season end as \"MM-DD\"."}},"type":"object","required":["start","end"],"title":"SeasonSpan","description":"One recurring operating season, WITHOUT a year.\n\n\"05-01\" / \"10-31\".  A season recurs, so a year would be wrong the moment\nit turned — see ``app/utils/season.py``.  ``start`` > ``end`` is legal and\nmeans the season wraps the new year (Nov 2 - Apr 30); 373 seeded sites are\nshaped that way, so a client comparing ``start <= d <= end`` would call\nevery one of them closed all year."},"SiteListResponse":{"properties":{"sites":{"items":{"$ref":"#/components/schemas/SiteResponse"},"type":"array","title":"Sites"},"total":{"type":"integer","title":"Total"},"amenity_markers":{"items":{"$ref":"#/components/schemas/GeoAmenityMarker"},"type":"array","title":"Amenity Markers"},"zone_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Zone Id","description":"Zone ID if filtered by zone"},"campground_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campground Id","description":"Campground ID if filtered by campground"},"accommodation_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accommodation Type","description":"Accommodation type filter applied"}},"type":"object","required":["sites","total"],"title":"SiteListResponse","description":"Schema for list of sites response."},"SiteMapPosition":{"properties":{"site_id":{"type":"string","maxLength":512,"title":"Site Id"},"x":{"type":"integer","minimum":0.0,"title":"X"},"y":{"type":"integer","minimum":0.0,"title":"Y"},"is_hot":{"type":"boolean","title":"Is Hot","description":"Whether this site is in the curated hot_sites set (matches the same gold ring used on the park discovery map).","default":false}},"type":"object","required":["site_id","x","y"],"title":"SiteMapPosition","description":"A site's pixel position on a campground map's PNG."},"SiteResponse":{"properties":{"id":{"type":"string","title":"Id"},"zone_id":{"type":"string","title":"Zone Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"accommodation_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accommodation Type"},"availability_period":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Availability Period"},"features":{"anyOf":[{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"},{"type":"null"}],"title":"Features"},"pets_allowed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Pets Allowed"},"has_water":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Water"},"has_electricity":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Electricity"},"has_sewer":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Sewer"},"is_accessible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Accessible"},"is_double_site":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Double Site"},"is_pull_through":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Pull Through"},"is_tent_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Tent Only"},"price_per_night":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Price Per Night"},"max_occupancy":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Occupancy"},"display_order":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Display Order"},"max_tent_pads":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Tent Pads"},"can_start_route":{"type":"boolean","title":"Can Start Route","default":false},"routable":{"type":"boolean","title":"Routable","default":true},"single_permit":{"type":"boolean","title":"Single Permit","default":false},"op_min_nights":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Min Nights"},"op_max_nights":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Max Nights"},"op_long_stay_min_nights":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Long Stay Min Nights"},"op_long_stay_search_tab_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Long Stay Search Tab Id"},"op_long_stay_api_bcat":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Long Stay Api Bcat"},"op_allowed_equipment":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Op Allowed Equipment","description":"Allowed sub-equipment IDs (GoingToCamp). Used by the wizard to filter sites that don't fit the user's equipment."},"op_booking_category_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Booking Category Id","description":"Booking category ID (2=roofed accommodation). Used for site-level filtering within mixed zones."},"camp_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Camp Type","description":"Canonical CampType bucket ('Camping', 'Chalet', 'CanoeCamping', 'Peche', etc.).  Provider-agnostic; preferred over op_booking_category_id for activity-type filtering."},"lat":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lat","description":"WGS84 latitude. Populated for SEPAQ chalet/pêche units (bookable points spread across vast wildlife reserves). NULL for photo-overlay sites — those carry map_id/map_x/map_y instead, exposed via the /campgrounds/{id}/maps endpoint. Mobile's GeographicSitePicker uses this + lng to plot pins on flutter_map."},"lng":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lng","description":"WGS84 longitude. See lat docstring for shape."},"permit_district":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Permit District","description":"The land manager's own grouping for this row — the wilderness a trailhead sits in (Central Cascades: 'Three Sisters', 'Mt. Jefferson', 'Mt. Washington'). Parsed at seed time from the upstream name, which ships it as a prefix ('Three Sisters - Broken Top Trailhead'). Lets the day-use grid section eighteen rows the way the provider itself presents them, instead of one flat eighteen-row scroll. NULL where the provider publishes no grouping — Parks Canada day use has none."},"sepaq_unit_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sepaq Unit Id","description":"SEPAQ-specific reservable unit id from /carte/resultats. Stable across re-extractions and useful as a provider-stable join key. NULL for non-SEPAQ sites."},"coord_is_approximate":{"type":"boolean","title":"Coord Is Approximate","description":"TRUE when lat/lng was inferred rather than published — a handful of permit divisions have no coordinate anywhere in the provider's payload, so theirs come from a gazetteer and may be the named feature rather than its trailhead. Clients that present a coordinate as precise should say otherwise for these.","default":false},"map_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Map Url","description":"External map for this site published by the land manager, e.g. the Forest Service trail map for a permit trailhead. Distinct from the booking page in `url`: the site card offers both, and tapping the site image opens THIS when present. NULL when the land manager publishes none."},"permit_trailhead_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Permit Trailhead Code","description":"The land manager's own label for a permit division ('12A'). Providers stamp the TRAILHEAD's coordinate on every division leaving it, so Yosemite's 56 divisions sit on 43 distinct lat/lng pairs and no amount of zoom separates them. The BASE of this code ('12' of '12A') is the shared pin, so a picker that groups by base reproduces the printed map's hierarchy: one row per trailhead, its competing permits nested under it. NULL for non-permit sites and for divisions the land manager does not number."},"permit_addon_eligibility":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Permit Addon Eligibility","description":"Slugs of OTHER permits this division qualifies the holder for, e.g. ['half_dome']. A cross-permit relationship published by the land manager (Half Dome is its own permit, 234652), not an attribute of the wilderness permit itself. NULL when none."},"has_image":{"type":"boolean","title":"Has Image","default":false},"is_active":{"type":"boolean","title":"Is Active"},"is_hot":{"type":"boolean","title":"Is Hot","description":"Whether this site is in the curated hot_sites set (eligible for hot-spot notifications). Populated by the API handler from a batch lookup; defaults to False when constructed outside the handler chain.","default":false},"rating":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rating","description":"Mean guest star rating, 1-5, or NULL when this site has no reviews. NULL and 0.0 are different answers and must not render the same way, which is why this is nullable rather than defaulted."},"review_count":{"type":"integer","title":"Review Count","description":"How many guest reviews resolved to this site. Shown beside the rating so a 4.6 from 200 reviews reads differently from a 5.0 from one.","default":0},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","zone_id","name","description","url","accommodation_type","availability_period","features","pets_allowed","has_water","has_electricity","has_sewer","price_per_night","is_active","created_at","updated_at"],"title":"SiteResponse","description":"Schema for site response."},"SiteReviewListResponse":{"properties":{"site_id":{"type":"string","title":"Site Id"},"total":{"type":"integer","title":"Total","description":"Every review we hold for this site."},"next_cursor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Cursor","description":"Pass back as ``cursor`` for the next chunk; null at the end of the list, which is how the client knows to stop asking rather than inferring it from a short page."},"reviews":{"items":{"$ref":"#/components/schemas/SiteReviewResponse"},"type":"array","title":"Reviews"}},"type":"object","required":["site_id","total"],"title":"SiteReviewListResponse","description":"One chunk of a site's reviews, plus where the next chunk starts."},"SiteReviewResponse":{"properties":{"id":{"type":"string","title":"Id"},"rating":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rating","description":"Stars 1-5. Null when the review carried no score — which must render as absent, never as zero."},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text","description":"The review, capped at 1000 characters at harvest."},"author":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Author","description":"'Jason M.' — first name and last initial, the form the source publishes. Never a full name."},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At","description":"When the guest wrote it, not when we read it."},"helpful_votes":{"type":"integer","title":"Helpful Votes","default":0}},"type":"object","required":["id"],"title":"SiteReviewResponse","description":"One guest review, verbatim.\n\nThe text is Recreation.gov's, unedited and unsummarised — the score already\nlives on the site; this is the reasoning behind it. Never rewritten, so the\nattribution on the card stays honest."},"StuckTarget":{"properties":{"provider":{"type":"string","title":"Provider"},"tier":{"type":"string","title":"Tier"},"worst_overdue_seconds":{"type":"number","title":"Worst Overdue Seconds"},"max_window_seconds":{"type":"number","title":"Max Window Seconds"},"windows_overdue":{"type":"number","title":"Windows Overdue"},"stuck_since":{"type":"string","title":"Stuck Since"}},"type":"object","required":["provider","tier","worst_overdue_seconds","max_window_seconds","windows_overdue","stuck_since"],"title":"StuckTarget","description":"One (provider, tier) whose WORST single poll target is badly overdue.\n\nOps-only: this is deliberately NOT folded into ``provider_tier_status``.\nA single stuck target means one user's alert is dark, not that the provider\nis down for everyone, so it degrades the aggregate ``/health`` status (what\nthe uptime monitor keys on) without showing a degraded banner to every user\nof that provider and tier."},"SubscriptionPlanEnum":{"type":"string","enum":["monthly"],"const":"monthly","title":"SubscriptionPlanEnum","description":"Available subscription plans."},"SubscriptionPlanInfo":{"properties":{"tier":{"$ref":"#/components/schemas/SubscriptionTierEnum"},"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"price_cad":{"type":"number","title":"Price Cad"},"interval":{"type":"string","title":"Interval","default":"month"},"features":{"items":{"type":"string"},"type":"array","title":"Features"},"capacity":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Capacity"},"current_subscribers":{"type":"integer","title":"Current Subscribers","default":0},"available_spots":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Available Spots"},"is_available":{"type":"boolean","title":"Is Available","default":true}},"type":"object","required":["tier","name","description","price_cad","features"],"title":"SubscriptionPlanInfo","description":"Schema for subscription plan information."},"SubscriptionPlansResponse":{"properties":{"plans":{"items":{"$ref":"#/components/schemas/SubscriptionPlanInfo"},"type":"array","title":"Plans"},"trial_days":{"type":"integer","title":"Trial Days","default":0}},"type":"object","title":"SubscriptionPlansResponse","description":"Schema for available subscription plans."},"SubscriptionResponse":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"type":"string","title":"User Id"},"status":{"$ref":"#/components/schemas/SubscriptionStatusEnum"},"plan":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionPlanEnum"},{"type":"null"}]},"tier":{"allOf":[{"$ref":"#/components/schemas/SubscriptionTierEnum"}],"default":"essai"},"pending_tier":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionTierEnum"},{"type":"null"}]},"trial_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Trial Start"},"trial_end":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Trial End"},"current_period_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Current Period Start"},"current_period_end":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Current Period End"},"canceled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Canceled At"},"cancel_at_period_end":{"type":"boolean","title":"Cancel At Period End"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"is_active":{"type":"boolean","title":"Is Active"},"is_trial":{"type":"boolean","title":"Is Trial"},"trial_days_remaining":{"type":"integer","title":"Trial Days Remaining"},"days_until_renewal":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Days Until Renewal"}},"type":"object","required":["id","user_id","status","plan","trial_start","trial_end","current_period_start","current_period_end","canceled_at","cancel_at_period_end","created_at","updated_at","is_active","is_trial","trial_days_remaining","days_until_renewal"],"title":"SubscriptionResponse","description":"Schema for subscription response."},"SubscriptionScreenConfigResponse":{"properties":{"version":{"type":"integer","title":"Version","default":1},"tiers":{"items":{"$ref":"#/components/schemas/TierTileConfig"},"type":"array","title":"Tiers","default":[]},"paid_tiers":{"items":{"type":"string"},"type":"array","title":"Paid Tiers","default":[]},"comparison_rows":{"items":{"$ref":"#/components/schemas/ComparisonRowConfig"},"type":"array","title":"Comparison Rows","default":[]},"features":{"items":{"$ref":"#/components/schemas/FeatureItemConfig"},"type":"array","title":"Features","default":[]},"provider_site_overrides":{"additionalProperties":{"additionalProperties":{"$ref":"#/components/schemas/ProviderSiteOverride"},"type":"object"},"type":"object","title":"Provider Site Overrides","default":{}},"capacity":{"additionalProperties":{"$ref":"#/components/schemas/TierCapacityInfo"},"type":"object","title":"Capacity","default":{}},"provider_config":{"additionalProperties":{"$ref":"#/components/schemas/ProviderConfig"},"type":"object","title":"Provider Config","default":{}}},"type":"object","title":"SubscriptionScreenConfigResponse","description":"v2 response for /plans endpoint — full subscription screen config."},"SubscriptionStatusEnum":{"type":"string","enum":["trial","active","past_due","canceled","expired"],"title":"SubscriptionStatusEnum","description":"Subscription status states."},"SubscriptionStatusResponse":{"properties":{"is_active":{"type":"boolean","title":"Is Active"},"is_trial":{"type":"boolean","title":"Is Trial"},"status":{"$ref":"#/components/schemas/SubscriptionStatusEnum"},"plan":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionPlanEnum"},{"type":"null"}]},"tier":{"allOf":[{"$ref":"#/components/schemas/SubscriptionTierEnum"}],"default":"essai"},"pending_tier":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionTierEnum"},{"type":"null"}]},"trial_days_remaining":{"type":"integer","title":"Trial Days Remaining"},"current_period_end":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Current Period End"},"entitlements":{"items":{"type":"string"},"type":"array","title":"Entitlements","default":[]},"limits":{"anyOf":[{"$ref":"#/components/schemas/TierLimitsResponse"},{"type":"null"}]},"usage":{"anyOf":[{"$ref":"#/components/schemas/QuotaUsageResponse"},{"type":"null"}]}},"type":"object","required":["is_active","is_trial","status","plan","trial_days_remaining","current_period_end"],"title":"SubscriptionStatusResponse","description":"Schema for subscription status check response."},"SubscriptionTierEnum":{"type":"string","enum":["essai","regular","pro"],"title":"SubscriptionTierEnum","description":"Subscription tier levels."},"TierCapacityInfo":{"properties":{"capacity":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Capacity"},"current_subscribers":{"type":"integer","title":"Current Subscribers","default":0},"available_spots":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Available Spots"},"is_available":{"type":"boolean","title":"Is Available","default":true}},"type":"object","title":"TierCapacityInfo","description":"Capacity information for a single tier."},"TierLimitsResponse":{"properties":{"max_alerts":{"type":"integer","title":"Max Alerts"},"max_sites_per_alert":{"type":"integer","title":"Max Sites Per Alert"},"max_total_sites":{"type":"integer","title":"Max Total Sites"},"max_total_alerts":{"type":"integer","title":"Max Total Alerts"},"sms_enabled":{"type":"boolean","title":"Sms Enabled"},"email_enabled":{"type":"boolean","title":"Email Enabled"},"ontario_parks_enabled":{"type":"boolean","title":"Ontario Parks Enabled"}},"type":"object","required":["max_alerts","max_sites_per_alert","max_total_sites","max_total_alerts","sms_enabled","email_enabled","ontario_parks_enabled"],"title":"TierLimitsResponse","description":"Schema for tier limits."},"TierTileConfig":{"properties":{"tier":{"type":"string","title":"Tier"},"icon":{"type":"string","title":"Icon"},"price_cad":{"type":"number","title":"Price Cad"},"revenuecat_product_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revenuecat Product Id"},"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"badge":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Badge"},"features":{"items":{"$ref":"#/components/schemas/TileFeatureRow"},"type":"array","title":"Features","default":[]}},"type":"object","required":["tier","icon","price_cad","name","description"],"title":"TierTileConfig","description":"Configuration for a single tier tile on the subscription screen."},"TileFeatureRow":{"properties":{"icon":{"type":"string","title":"Icon"},"text":{"type":"string","title":"Text"},"highlight":{"type":"boolean","title":"Highlight","default":false},"highlight_color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Highlight Color"}},"type":"object","required":["icon","text"],"title":"TileFeatureRow","description":"Feature row within a tier tile card."},"TrackingCreate":{"properties":{"event_type":{"type":"string","maxLength":50,"minLength":1,"title":"Event Type"},"source":{"type":"string","maxLength":100,"minLength":1,"title":"Source"},"platform":{"type":"string","maxLength":20,"minLength":1,"title":"Platform"},"user_agent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Agent"}},"type":"object","required":["event_type","source","platform"],"title":"TrackingCreate","description":"Schema for creating a tracking event."},"TrackingResponse":{"properties":{"id":{"type":"integer","title":"Id"},"recorded_at":{"type":"string","format":"date-time","title":"Recorded At"}},"type":"object","required":["id","recorded_at"],"title":"TrackingResponse","description":"Schema for tracking event response."},"TrailSummary":{"properties":{"trail_key":{"type":"string","title":"Trail Key"},"name":{"type":"string","title":"Name"},"distance_km":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Distance Km"},"variants":{"items":{"$ref":"#/components/schemas/RouteSummary"},"type":"array","title":"Variants","default":[]}},"type":"object","required":["trail_key","name"],"title":"TrailSummary","description":"A trail plus the paces it can be walked at.\n\nHikers pick the trail first and the pace second (\"I want the Rockwall, and\nI have four days\"), and the parks themselves publish trails with a duration\nRANGE rather than one fixed itinerary. Grouping here keeps the picker one\ndecision the user already has an opinion about, instead of a list of\nnear-duplicate rows."},"UserResponse":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"language":{"type":"string","pattern":"^(fr|en|es|zh|pt|it|ht|ro|vi|el|tl|ru|hi|ko|pa|pl|de)$","title":"Language","default":"fr"},"id":{"type":"string","title":"Id"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"photo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Photo Url"},"email_notifications_enabled":{"type":"boolean","title":"Email Notifications Enabled"},"push_notifications_enabled":{"type":"boolean","title":"Push Notifications Enabled"},"alerts_paused":{"type":"boolean","title":"Alerts Paused"},"is_active":{"type":"boolean","title":"Is Active"},"total_notifications":{"type":"integer","title":"Total Notifications","default":0},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["email","id","email_notifications_enabled","push_notifications_enabled","alerts_paused","is_active","created_at","updated_at"],"title":"UserResponse","description":"Schema for user response."},"UserUpdate":{"properties":{"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"language":{"anyOf":[{"type":"string","pattern":"^(fr|en|es|zh|pt|it|ht|ro|vi|el|tl|ru|hi|ko|pa|pl|de)$"},{"type":"null"}],"title":"Language"},"email_notifications_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Email Notifications Enabled"},"push_notifications_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Push Notifications Enabled"},"alerts_paused":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Alerts Paused"}},"type":"object","title":"UserUpdate","description":"Schema for updating user profile."},"UserWithSubscription":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"language":{"type":"string","pattern":"^(fr|en|es|zh|pt|it|ht|ro|vi|el|tl|ru|hi|ko|pa|pl|de)$","title":"Language","default":"fr"},"id":{"type":"string","title":"Id"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"photo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Photo Url"},"email_notifications_enabled":{"type":"boolean","title":"Email Notifications Enabled"},"push_notifications_enabled":{"type":"boolean","title":"Push Notifications Enabled"},"alerts_paused":{"type":"boolean","title":"Alerts Paused"},"is_active":{"type":"boolean","title":"Is Active"},"total_notifications":{"type":"integer","title":"Total Notifications","default":0},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"has_active_subscription":{"type":"boolean","title":"Has Active Subscription"},"is_in_trial":{"type":"boolean","title":"Is In Trial"},"trial_days_remaining":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Trial Days Remaining"},"total_cancellations_detected":{"type":"integer","title":"Total Cancellations Detected","default":0},"total_availabilities_detected":{"type":"integer","title":"Total Availabilities Detected","default":0},"terms_accepted":{"type":"boolean","title":"Terms Accepted","default":false},"terms_previously_accepted":{"type":"boolean","title":"Terms Previously Accepted","default":false}},"type":"object","required":["email","id","email_notifications_enabled","push_notifications_enabled","alerts_paused","is_active","created_at","updated_at","has_active_subscription","is_in_trial"],"title":"UserWithSubscription","description":"Schema for user response with subscription status."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyEmailCodeRequest":{"properties":{"code":{"type":"string","maxLength":6,"minLength":6,"pattern":"^\\d{6}$","title":"Code"}},"type":"object","required":["code"],"title":"VerifyEmailCodeRequest","description":"Request to verify email with a 6-digit code."},"VerifyEmailCodeResponse":{"properties":{"message":{"type":"string","title":"Message"},"email_verified":{"type":"boolean","title":"Email Verified"}},"type":"object","required":["message","email_verified"],"title":"VerifyEmailCodeResponse","description":"Response after successfully verifying email."},"VerifyResetCodeRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"code":{"type":"string","maxLength":6,"minLength":6,"pattern":"^\\d{6}$","title":"Code"}},"type":"object","required":["email","code"],"title":"VerifyResetCodeRequest","description":"Request to verify a password reset code without consuming it."},"VerifyResetCodeResponse":{"properties":{"valid":{"type":"boolean","title":"Valid"}},"type":"object","required":["valid"],"title":"VerifyResetCodeResponse","description":"Response after successfully verifying a reset code."},"VoteResponse":{"properties":{"feedback_id":{"type":"string","title":"Feedback Id"},"vote_count":{"type":"integer","title":"Vote Count"},"has_voted":{"type":"boolean","title":"Has Voted"}},"type":"object","required":["feedback_id","vote_count","has_voted"],"title":"VoteResponse","description":"Response after voting on a feedback entry."},"WaitlistJoinRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"provider":{"type":"string","maxLength":32,"minLength":1,"title":"Provider"},"locale":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Locale"},"source":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Source"}},"type":"object","required":["email","provider"],"title":"WaitlistJoinRequest","description":"Schema for joining the provider waitlist."},"WaitlistJoinResponse":{"properties":{"id":{"type":"integer","title":"Id"},"email":{"type":"string","format":"email","title":"Email"},"provider":{"type":"string","title":"Provider"},"locale":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Locale"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","email","provider","created_at"],"title":"WaitlistJoinResponse","description":"Response returned after joining (or re-joining) a waitlist."},"WaitlistRequest":{"properties":{"desired_tier":{"allOf":[{"$ref":"#/components/schemas/SubscriptionTierEnum"}],"description":"Tier to join waitlist for"}},"type":"object","required":["desired_tier"],"title":"WaitlistRequest","description":"Request schema for joining a tier waitlist."},"WaitlistResponse":{"properties":{"message":{"type":"string","title":"Message"},"already_registered":{"type":"boolean","title":"Already Registered","default":false}},"type":"object","required":["message"],"title":"WaitlistResponse","description":"Response schema for waitlist operations."},"WorkerStatus":{"properties":{"node_name":{"type":"string","title":"Node Name"},"instance_role":{"type":"string","title":"Instance Role"},"polling_tiers":{"type":"string","title":"Polling Tiers"},"status":{"type":"string","title":"Status"},"uptime_seconds":{"type":"number","title":"Uptime Seconds"},"last_heartbeat_seconds_ago":{"type":"number","title":"Last Heartbeat Seconds Ago"},"polling_task_running":{"type":"boolean","title":"Polling Task Running"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"sites_polled_total":{"type":"integer","title":"Sites Polled Total","default":0},"sites_successful":{"type":"integer","title":"Sites Successful","default":0},"sites_failed":{"type":"integer","title":"Sites Failed","default":0},"success_rate":{"type":"number","title":"Success Rate","default":0.0},"sites_polled_by_tier":{"additionalProperties":{"type":"integer"},"type":"object","title":"Sites Polled By Tier","default":{}},"sites_tracked":{"type":"integer","title":"Sites Tracked","default":0},"sites_overdue":{"type":"integer","title":"Sites Overdue","default":0},"stats_sites_tracked":{"type":"integer","title":"Stats Sites Tracked","default":0},"stats_sites_overdue":{"type":"integer","title":"Stats Sites Overdue","default":0}},"type":"object","required":["node_name","instance_role","polling_tiers","status","uptime_seconds","last_heartbeat_seconds_ago","polling_task_running"],"title":"WorkerStatus","description":"Health status of a single cluster node."},"ZoneAdjacencyResponse":{"properties":{"park_id":{"type":"string","title":"Park Id"},"provider":{"type":"string","title":"Provider"},"constrained":{"type":"boolean","title":"Constrained"},"adjacency":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object","title":"Adjacency","default":{}}},"type":"object","required":["park_id","provider","constrained"],"title":"ZoneAdjacencyResponse"},"ZoneListResponse":{"properties":{"zones":{"items":{"$ref":"#/components/schemas/ZoneResponse"},"type":"array","title":"Zones"},"total":{"type":"integer","title":"Total"},"campground_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campground Id","description":"Campground ID if filtered by campground"}},"type":"object","required":["zones","total"],"title":"ZoneListResponse","description":"Schema for list of zones response."},"ZoneResponse":{"properties":{"id":{"type":"string","title":"Id"},"campground_id":{"type":"string","title":"Campground Id"},"name":{"type":"string","title":"Name"},"name_en":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name En"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"is_active":{"type":"boolean","title":"Is Active"},"is_default":{"type":"boolean","title":"Is Default","description":"Whether this is a default/collapsed zone (campground name == zone name). UI should auto-select and skip zone selection step.","default":false},"op_map_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Map Id","description":"GoingToCamp map ID this zone corresponds to. Used by mobile to match zones with their CampgroundMap (gtc_map_id) when filtering the site-map picker to only the zones the user has selected."},"op_booking_category_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Booking Category Id","description":"Ontario Parks booking category ID (0=camping, 2=roofed accommodation). Null for SEPAQ zones."},"op_min_nights":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Op Min Nights","description":"Ontario Parks minimum nights required for roofed accommodation zones. Null for SEPAQ zones and OP camping zones."},"has_roofed_sites":{"type":"boolean","title":"Has Roofed Sites","description":"Whether this zone contains any roofed accommodation sites (op_booking_category_id=2). Used for mixed-zone filtering.","default":false},"has_canoe_sites":{"type":"boolean","title":"Has Canoe Sites","description":"Whether this zone contains any canoe-camping sites (camp_type='CanoeCamping'). Used to filter zones for the Canoe-Camping activity tile in the wizard.","default":false},"has_backcountry_sites":{"type":"boolean","title":"Has Backcountry Sites","description":"Whether this zone contains any backcountry sites (camp_type='Backcountry'). Used to filter zones for the Backcountry activity tile in the wizard.","default":false},"has_day_use_sites":{"type":"boolean","title":"Has Day Use Sites","description":"Whether this zone contains day-use sites (Parks Canada parking time slots, Banff shuttle departures). Defaults False so builds predating the activity are unaffected.","default":false},"has_camping_sites":{"type":"boolean","title":"Has Camping Sites","description":"Whether this zone contains any standard camping sites (camp_type='Camping' — ready-to-camp accommodations roll up into the same bucket). Defaults TRUE so legacy zones without computed flags still appear under the Camping activity.","default":true},"site_count":{"type":"integer","title":"Site Count","description":"Number of sites in this zone","default":0},"accommodation_types":{"items":{"type":"string"},"type":"array","title":"Accommodation Types","description":"Unique accommodation types available in this zone"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","campground_id","name","name_en","description","is_active","created_at","updated_at"],"title":"ZoneResponse","description":"Schema for zone response."}}}}