{"openapi":"3.1.0","info":{"title":"VolunteerReminder API","version":"1.0.0","description":"REST API for VolunteerReminder — automated volunteer shift reminders by\nemail, SMS, and calendar invite.\n\nAuthentication: pass an API key as `Authorization: Bearer vrk_…` on every\nendpoint except the public `GET /pricing` and `POST /signups`. Keys are issued\nfrom the account security screen and carry scopes (`read`, `schedules:write`,\n`contacts:write`, `reminders:write`, `keys:manage`).\n\nSafety: agent-created accounts start in reminder **test mode** — reminders\nreroute to the owner's own address until a human flips live sending on from the\ndashboard. Nothing in this API can enable live sends, mutate billing, change the\nlogin email, or delete an account.\n\nErrors are always `{ \"error\": { \"code\", \"message\" } }`; branch on `code`.","contact":{"name":"VolunteerReminder","email":"info@volunteerreminder.com"}},"servers":[{"url":"https://volunteerreminder.com","description":"Production"}],"security":[{"BearerAuth":[]}],"tags":[{"name":"Account","description":"Plan, entitlements, reminder mode, and usage."},{"name":"Pricing","description":"Public plan catalog (no authentication)."},{"name":"Signups","description":"Public agent signup (pending human email verification)."},{"name":"Schedules","description":"Recurring volunteer schedules, occurrences, and previews."},{"name":"Contacts","description":"Volunteer roster and per-schedule roster assignment."},{"name":"Reminders","description":"Reminder lead times, channel availability, open days, and send history."},{"name":"Billing","description":"Hand the human a Stripe Checkout URL to subscribe — the agent never takes payment."},{"name":"Keys","description":"Scoped API-key minting, listing, and revocation."}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"vrk_<64 hex chars>","description":"VolunteerReminder API key. Issue one from the account security screen and send it as `Authorization: Bearer vrk_…`."}},"schemas":{"PricingResponse":{"type":"object","properties":{"product":{"type":"string","example":"VolunteerReminder"},"description":{"type":"string"},"currency":{"type":"string","example":"USD"},"url":{"type":"string"},"pricing_url":{"type":"string"},"signup_url":{"type":"string"},"sms_pricing":{"type":"string"},"billing_cycles":{"type":"array","items":{"type":"string"},"example":["monthly","annual"]},"plans":{"type":"array","items":{"$ref":"#/components/schemas/PricingPlan"}}},"required":["product","description","currency","url","pricing_url","signup_url","sms_pricing","billing_cycles","plans"]},"PricingPlan":{"type":"object","properties":{"id":{"type":"string","example":"deluxe"},"name":{"type":"string","example":"Deluxe"},"monthly_price_usd":{"type":"number","example":9},"annual_price_usd":{"type":"number","example":90},"max_volunteers":{"type":"number","example":60},"blurb":{"type":"string"},"features":{"type":"array","items":{"type":"string"}},"reminders_per_shift":{"type":"number","example":2},"sms_reminders_included":{"type":"boolean"},"signup_widget_included":{"type":"boolean"},"substitute_finder_included":{"type":"boolean"}},"required":["id","name","monthly_price_usd","annual_price_usd","max_volunteers","blurb","features","reminders_per_shift","sms_reminders_included","signup_widget_included","substitute_finder_included"]},"AccountResponse":{"type":"object","properties":{"account":{"type":"object","properties":{"id":{"type":"number","example":4021},"name":{"type":["string","null"],"example":"Riverside Food Pantry"},"time_zone":{"type":["string","null"],"description":"IANA timezone id, or null if unset.","example":"America/Chicago"},"unique_code":{"type":["string","null"],"description":"Public sign-up widget code for this account, or null."},"created_at":{"type":"number","description":"Epoch milliseconds (UTC)."}},"required":["id","name","time_zone","unique_code","created_at"]},"plan":{"type":"object","properties":{"id":{"type":["string","null"],"description":"Paid plan id (basic/deluxe/pro), or null on trial/legacy.","example":"deluxe"},"label":{"type":"string","example":"Free trial"},"source":{"type":"string","enum":["paid","trial","trial_expired","legacy"],"description":"How the effective entitlements were derived."},"is_active":{"type":"boolean","description":"Whether reminders may flow (paid-active or live trial)."},"is_trialing":{"type":"boolean"},"trial_days_left":{"type":["number","null"],"description":"Whole days left in the trial, or null when not trialing."},"trial_ends_at":{"type":["number","null"],"description":"Trial end, epoch ms (UTC), or null."}},"required":["id","label","source","is_active","is_trialing","trial_days_left","trial_ends_at"]},"entitlements":{"type":"object","properties":{"sms_reminders":{"type":"boolean"},"signup_widget":{"type":"boolean"},"substitute_finder":{"type":"boolean"},"customized_reminders":{"type":"boolean"},"group_names":{"type":"boolean"},"reminders_per_shift":{"type":"number","example":2}},"required":["sms_reminders","signup_widget","substitute_finder","customized_reminders","group_names","reminders_per_shift"]},"reminder_mode":{"type":"string","enum":["test","live"],"description":"THIS ACCOUNT's own reminder posture. 'test' is an agent sandbox — reminders reroute to the account owner's own address, never a real volunteer; 'live' reaches real volunteers. Agent-created accounts start 'test'; only a human can switch it to 'live' from the dashboard (never via this API or the MCP server). See also `effective_mode`."},"effective_mode":{"type":"string","enum":["off","test","live"],"description":"What actually happens at send time: the MOST RESTRICTIVE of this account's `reminder_mode` and the deployment-wide kill-switch. It is 'off' when sending is globally paused, 'test' when either this account or the deployment is in test, and 'live' only when BOTH are live. Read this to know whether a reminder would actually reach a volunteer."},"usage":{"type":"object","properties":{"volunteers":{"type":"object","properties":{"used":{"type":"number","description":"Active (non-deleted) volunteers."},"limit":{"type":"number","description":"Plan volunteer cap."}},"required":["used","limit"]},"schedules":{"type":"object","properties":{"used":{"type":"number","description":"Active schedules (no enforced cap)."}},"required":["used"]}},"required":["volunteers","schedules"]}},"required":["account","plan","entitlements","reminder_mode","effective_mode","usage"]},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Stable machine-readable error code to branch on.","example":"invalid_token"},"message":{"type":"string","description":"Actionable, human/LLM-readable explanation.","example":"That API key is not valid."}},"required":["code","message"],"description":"The error detail."}},"required":["error"]},"SignupResponse":{"type":"object","properties":{"account_id":{"type":"integer","description":"The id of the account for this email — the one just created, or the existing account if the email was already registered (the response is identical either way). The account is INERT until a human verifies via the emailed link: no reminders send and no API key exists yet.","example":4021},"verification":{"type":"string","enum":["email_sent"],"description":"Always \"email_sent\". A single-use link was emailed to the address; the account owner must click it to activate the account. Nothing further can happen through the API until they do."},"api_key":{"type":"null","description":"Always null. An API credential is NEVER issued at signup. After the human verifies their email they can mint a key from the dashboard (or via POST /api/v1/keys once a first key exists). This human gate is deliberate (agent-channel-plan §3.4)."}},"required":["account_id","verification","api_key"]},"SignupRequest":{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email","pattern":"^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$","description":"The account owner's email address. The verification/sign-in link is sent here; only the person who controls this inbox can activate the account. Required.","example":"director@riversidefoodpantry.org"},"organization_name":{"type":"string","maxLength":200,"description":"Display name for the organization (becomes the account name). Optional — defaults to a placeholder the owner can rename after verifying. The account timezone defaults to America/Chicago and is editable later.","example":"Riverside Food Pantry"}},"required":["email"]},"SchedulePreviewResponse":{"type":"object","properties":{"occurrences":{"type":"array","items":{"$ref":"#/components/schemas/PreviewOccurrence"},"description":"Up to `count` upcoming occurrence dates for this configuration. Empty when the recurrence shape yields none within the lookahead window."},"time_zone":{"type":"string","description":"IANA timezone used to compute dates.","example":"America/Chicago"}},"required":["occurrences","time_zone"]},"PreviewOccurrence":{"type":"object","properties":{"date":{"type":"string","description":"\"yyyy-MM-dd\" civil date.","example":"2026-07-14"},"weekday":{"type":"string","description":"Full weekday name.","example":"Tuesday"},"time":{"type":["string","null"],"description":"Wall-clock \"HH:mm\", or null."},"label":{"type":"string","description":"Human-readable label.","example":"Tue Jul 14 9:00 AM"}},"required":["date","weekday","time","label"]},"SchedulePreviewRequest":{"type":"object","properties":{"schedule_type":{"type":"string","enum":["weekly","monthly_fixed","monthly_nth","bimonthly_fixed","bimonthly_nth","custom"],"description":"Recurrence shape. 'weekly' recurs on one or more days of the week; 'monthly_fixed' / 'bimonthly_fixed' recur on a fixed day of the month; 'monthly_nth' / 'bimonthly_nth' recur on the Nth weekday of the month; the 'bimonthly_*' variants additionally alternate months starting from start_month; 'custom' uses an explicit list of dates.","example":"weekly"},"days_of_week":{"type":"array","items":{"type":"string","enum":["sunday","monday","tuesday","wednesday","thursday","friday","saturday"]},"description":"Days of the week this recurs on. Required (non-empty) when schedule_type is 'weekly'; ignored for every other schedule_type.","example":["sunday"]},"nth_weekday":{"type":["string","null"],"enum":["sunday","monday","tuesday","wednesday","thursday","friday","saturday",null],"description":"The weekday that recurs. Used by schedule_type 'monthly_nth' / 'bimonthly_nth'; defaults to Sunday if omitted for those types.","example":"tuesday"},"week_of_month":{"type":"integer","minimum":1,"maximum":5,"description":"Which occurrence of the weekday in the month (1st through 5th). Used by 'monthly_nth' / 'bimonthly_nth'; defaults to 1.","example":2},"day_of_month":{"type":"integer","minimum":1,"maximum":31,"description":"Fixed day of the month. Used by 'monthly_fixed' / 'bimonthly_fixed'; defaults to 1. Clamped to the last day of shorter months (e.g. 31 in February becomes the 28th/29th).","example":15},"start_month":{"type":"integer","minimum":1,"maximum":12,"description":"Anchor month (1=January .. 12=December) the every-other-month cadence steps from. Used by 'bimonthly_fixed' / 'bimonthly_nth'; defaults to the account's current month.","example":1},"time":{"type":["string","null"],"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Wall-clock time of day \"HH:mm\" (24h) in the account's timezone, or null for no specific time (midnight).","example":"09:00"},"start_date":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"\"yyyy-MM-dd\" in the account's timezone before which no occurrences materialize, or null for no lower bound.","example":null},"end_date":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"\"yyyy-MM-dd\" in the account's timezone after which occurrence materialization stops, or null for no end date.","example":null},"custom_dates":{"type":"array","items":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"description":"Explicit list of \"yyyy-MM-dd\" dates. Required (non-empty) when schedule_type is 'custom'; ignored otherwise.","example":["2026-08-01","2026-09-05"]},"count":{"type":"integer","minimum":1,"maximum":52,"description":"How many upcoming occurrences to return (default 8, max 52).","example":8}},"required":["schedule_type"]},"ScheduleSummary":{"type":"object","properties":{"id":{"type":"number","description":"Schedule id.","example":42},"description":{"type":"string","description":"Schedule label."},"job_name":{"type":["string","null"],"description":"Associated job/role name."},"group_name":{"type":["string","null"],"description":"Associated group name, or null."},"notes":{"type":["string","null"],"description":"Free-text internal notes."},"next_occurrence_at":{"type":["number","null"],"description":"Epoch ms (UTC) of the next upcoming, attendable occurrence, or null."},"last_reminder_sent":{"type":["number","null"],"description":"Epoch ms (UTC) reminders were last sent for this schedule, or null."},"roster_count":{"type":"number","description":"Number of volunteers assigned to this schedule."}},"required":["id","description","job_name","group_name","notes","next_occurrence_at","last_reminder_sent","roster_count"]},"Schedule":{"type":"object","properties":{"id":{"type":"number","description":"Schedule id.","example":42},"description":{"type":"string","minLength":1,"description":"Short label shown on the schedule list and in reminder messages, e.g. \"Sunday morning greeters\".","example":"Sunday morning greeters"},"job_name":{"type":["string","null"],"description":"Resolved job/role name."},"group_id":{"type":["integer","null"],"description":"Id of an existing volunteer group to attach, or null. An id not owned by this account is silently ignored (treated as null).","example":null},"group_name":{"type":["string","null"],"description":"Resolved group name, or null."},"schedule_type":{"type":"string","enum":["weekly","monthly_fixed","monthly_nth","bimonthly_fixed","bimonthly_nth","custom"],"description":"Recurrence shape. 'weekly' recurs on one or more days of the week; 'monthly_fixed' / 'bimonthly_fixed' recur on a fixed day of the month; 'monthly_nth' / 'bimonthly_nth' recur on the Nth weekday of the month; the 'bimonthly_*' variants additionally alternate months starting from start_month; 'custom' uses an explicit list of dates.","example":"weekly"},"days_of_week":{"type":"array","items":{"type":"string","enum":["sunday","monday","tuesday","wednesday","thursday","friday","saturday"]},"description":"Days of the week this recurs on. Required (non-empty) when schedule_type is 'weekly'; ignored for every other schedule_type.","example":["sunday"]},"nth_weekday":{"type":["string","null"],"enum":["sunday","monday","tuesday","wednesday","thursday","friday","saturday",null],"description":"The weekday that recurs. Used by schedule_type 'monthly_nth' / 'bimonthly_nth'; defaults to Sunday if omitted for those types.","example":"tuesday"},"week_of_month":{"type":["number","null"],"description":"Nth-weekday occurrence (1-5), or null when schedule_type doesn't use it."},"day_of_month":{"type":["number","null"],"description":"Fixed day of month, or null when schedule_type doesn't use it."},"start_month":{"type":["number","null"],"description":"Bimonthly anchor month (1-12), or null when schedule_type doesn't use it."},"time":{"type":["string","null"],"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Wall-clock time of day \"HH:mm\" (24h) in the account's timezone, or null for no specific time (midnight).","example":"09:00"},"start_date":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"\"yyyy-MM-dd\" in the account's timezone before which no occurrences materialize, or null for no lower bound.","example":null},"end_date":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"\"yyyy-MM-dd\" in the account's timezone after which occurrence materialization stops, or null for no end date.","example":null},"custom_dates":{"type":"array","items":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"description":"Explicit list of \"yyyy-MM-dd\" dates. Required (non-empty) when schedule_type is 'custom'; ignored otherwise.","example":["2026-08-01","2026-09-05"]},"contact_ids":{"type":"array","items":{"type":"integer"},"description":"Volunteer roster for this schedule. Ids not owned by this account are silently dropped.","example":[101,102]},"notes":{"type":["string","null"],"description":"Free-text internal notes.","example":null},"next_occurrence_at":{"type":["number","null"],"description":"Epoch ms (UTC) of the next upcoming, attendable occurrence, or null."},"is_muted":{"type":"boolean","description":"Whether reminders for this schedule are muted."},"created_at":{"type":"number","description":"Epoch ms (UTC) this schedule was created."}},"required":["id","description","job_name","group_id","group_name","schedule_type","days_of_week","nth_weekday","week_of_month","day_of_month","start_month","time","start_date","end_date","custom_dates","contact_ids","notes","next_occurrence_at","is_muted","created_at"]},"ScheduleCreateRequest":{"type":"object","properties":{"description":{"type":"string","minLength":1,"description":"Short label shown on the schedule list and in reminder messages, e.g. \"Sunday morning greeters\".","example":"Sunday morning greeters"},"job_name":{"type":"string","minLength":1,"description":"The role/task volunteers are reminded about. Matched case-insensitively against this account's existing jobs and created if new.","example":"Greeter"},"group_id":{"type":["integer","null"],"description":"Id of an existing volunteer group to attach, or null. An id not owned by this account is silently ignored (treated as null).","example":null},"schedule_type":{"type":"string","enum":["weekly","monthly_fixed","monthly_nth","bimonthly_fixed","bimonthly_nth","custom"],"description":"Recurrence shape. 'weekly' recurs on one or more days of the week; 'monthly_fixed' / 'bimonthly_fixed' recur on a fixed day of the month; 'monthly_nth' / 'bimonthly_nth' recur on the Nth weekday of the month; the 'bimonthly_*' variants additionally alternate months starting from start_month; 'custom' uses an explicit list of dates.","example":"weekly"},"days_of_week":{"type":"array","items":{"type":"string","enum":["sunday","monday","tuesday","wednesday","thursday","friday","saturday"]},"description":"Days of the week this recurs on. Required (non-empty) when schedule_type is 'weekly'; ignored for every other schedule_type.","example":["sunday"]},"nth_weekday":{"type":["string","null"],"enum":["sunday","monday","tuesday","wednesday","thursday","friday","saturday",null],"description":"The weekday that recurs. Used by schedule_type 'monthly_nth' / 'bimonthly_nth'; defaults to Sunday if omitted for those types.","example":"tuesday"},"week_of_month":{"type":"integer","minimum":1,"maximum":5,"description":"Which occurrence of the weekday in the month (1st through 5th). Used by 'monthly_nth' / 'bimonthly_nth'; defaults to 1.","example":2},"day_of_month":{"type":"integer","minimum":1,"maximum":31,"description":"Fixed day of the month. Used by 'monthly_fixed' / 'bimonthly_fixed'; defaults to 1. Clamped to the last day of shorter months (e.g. 31 in February becomes the 28th/29th).","example":15},"start_month":{"type":"integer","minimum":1,"maximum":12,"description":"Anchor month (1=January .. 12=December) the every-other-month cadence steps from. Used by 'bimonthly_fixed' / 'bimonthly_nth'; defaults to the account's current month.","example":1},"time":{"type":["string","null"],"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Wall-clock time of day \"HH:mm\" (24h) in the account's timezone, or null for no specific time (midnight).","example":"09:00"},"start_date":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"\"yyyy-MM-dd\" in the account's timezone before which no occurrences materialize, or null for no lower bound.","example":null},"end_date":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"\"yyyy-MM-dd\" in the account's timezone after which occurrence materialization stops, or null for no end date.","example":null},"custom_dates":{"type":"array","items":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"description":"Explicit list of \"yyyy-MM-dd\" dates. Required (non-empty) when schedule_type is 'custom'; ignored otherwise.","example":["2026-08-01","2026-09-05"]},"contact_ids":{"type":"array","items":{"type":"integer"},"description":"Volunteer roster for this schedule. Ids not owned by this account are silently dropped.","example":[101,102]},"notes":{"type":["string","null"],"description":"Free-text internal notes.","example":null}},"required":["description","job_name","schedule_type"]},"ScheduleOccurrencesResponse":{"type":"object","properties":{"schedule_id":{"type":"number","description":"The schedule these occurrences belong to."},"time_zone":{"type":"string","description":"IANA timezone used to render dates.","example":"America/Chicago"},"occurrences":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleOccurrence"}}},"required":["schedule_id","time_zone","occurrences"]},"ScheduleOccurrence":{"type":"object","properties":{"id":{"type":"number","description":"Occurrence id."},"date":{"type":"string","description":"\"yyyy-MM-dd\" civil date in the account timezone."},"time":{"type":["string","null"],"description":"Wall-clock \"HH:mm\", or null when the schedule has no time-of-day."},"occurrence_at":{"type":"number","description":"Epoch ms (UTC) instant of this occurrence."},"label":{"type":"string","description":"Human-readable label.","example":"Tue Jul 14 9:00 AM"},"attending":{"type":"boolean","description":"False when this specific date has been marked as an exception (the roster is not attending this occurrence)."},"assigned_contacts":{"type":"array","items":{"$ref":"#/components/schemas/AssignedContact"},"description":"The schedule's roster. Assignment is schedule-level, not per-occurrence."}},"required":["id","date","time","occurrence_at","label","attending","assigned_contacts"]},"AssignedContact":{"type":"object","properties":{"id":{"type":"number","description":"Contact id."},"full_name":{"type":"string","description":"Contact's full name."}},"required":["id","full_name"]},"ScheduleUpdateRequest":{"type":"object","properties":{"description":{"type":"string","minLength":1,"description":"Short label shown on the schedule list and in reminder messages, e.g. \"Sunday morning greeters\".","example":"Sunday morning greeters"},"job_name":{"type":"string","minLength":1,"description":"The role/task volunteers are reminded about. Matched case-insensitively against this account's existing jobs and created if new.","example":"Greeter"},"group_id":{"type":["integer","null"],"description":"Id of an existing volunteer group to attach, or null. An id not owned by this account is silently ignored (treated as null).","example":null},"schedule_type":{"type":"string","enum":["weekly","monthly_fixed","monthly_nth","bimonthly_fixed","bimonthly_nth","custom"],"description":"Recurrence shape. 'weekly' recurs on one or more days of the week; 'monthly_fixed' / 'bimonthly_fixed' recur on a fixed day of the month; 'monthly_nth' / 'bimonthly_nth' recur on the Nth weekday of the month; the 'bimonthly_*' variants additionally alternate months starting from start_month; 'custom' uses an explicit list of dates.","example":"weekly"},"days_of_week":{"type":"array","items":{"type":"string","enum":["sunday","monday","tuesday","wednesday","thursday","friday","saturday"]},"description":"Days of the week this recurs on. Required (non-empty) when schedule_type is 'weekly'; ignored for every other schedule_type.","example":["sunday"]},"nth_weekday":{"type":["string","null"],"enum":["sunday","monday","tuesday","wednesday","thursday","friday","saturday",null],"description":"The weekday that recurs. Used by schedule_type 'monthly_nth' / 'bimonthly_nth'; defaults to Sunday if omitted for those types.","example":"tuesday"},"week_of_month":{"type":"integer","minimum":1,"maximum":5,"description":"Which occurrence of the weekday in the month (1st through 5th). Used by 'monthly_nth' / 'bimonthly_nth'; defaults to 1.","example":2},"day_of_month":{"type":"integer","minimum":1,"maximum":31,"description":"Fixed day of the month. Used by 'monthly_fixed' / 'bimonthly_fixed'; defaults to 1. Clamped to the last day of shorter months (e.g. 31 in February becomes the 28th/29th).","example":15},"start_month":{"type":"integer","minimum":1,"maximum":12,"description":"Anchor month (1=January .. 12=December) the every-other-month cadence steps from. Used by 'bimonthly_fixed' / 'bimonthly_nth'; defaults to the account's current month.","example":1},"time":{"type":["string","null"],"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Wall-clock time of day \"HH:mm\" (24h) in the account's timezone, or null for no specific time (midnight).","example":"09:00"},"start_date":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"\"yyyy-MM-dd\" in the account's timezone before which no occurrences materialize, or null for no lower bound.","example":null},"end_date":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"\"yyyy-MM-dd\" in the account's timezone after which occurrence materialization stops, or null for no end date.","example":null},"custom_dates":{"type":"array","items":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"description":"Explicit list of \"yyyy-MM-dd\" dates. Required (non-empty) when schedule_type is 'custom'; ignored otherwise.","example":["2026-08-01","2026-09-05"]},"notes":{"type":["string","null"],"description":"Free-text internal notes.","example":null}}},"ScheduleArchiveResponse":{"type":"object","properties":{"id":{"type":"number","description":"The archived schedule's id."},"archived":{"type":"boolean","enum":[true],"description":"Always true — the schedule was soft-deleted (archived), not hard-deleted."}},"required":["id","archived"]},"Contact":{"type":"object","properties":{"id":{"type":"number","description":"Contact id.","example":4021},"full_name":{"type":"string","description":"Display name, shown on the roster and in reminder text.","example":"Ann Adams"},"first_name":{"type":["string","null"],"description":"First name, or `null` if not recorded separately.","example":"Ann"},"last_name":{"type":["string","null"],"description":"Last name, or `null` if not recorded separately.","example":"Adams"},"email_address":{"type":["string","null"],"description":"Email address reminders may be sent to, or `null`.","example":"ann@example.org"},"mobile_phone":{"type":["string","null"],"description":"Mobile phone in E.164 format, used for SMS reminders, or `null`.","example":"+15125550100"},"land_phone":{"type":["string","null"],"description":"Landline phone number (not SMS-capable, informational only), or `null`.","example":"+15125550199"},"group_id":{"type":["number","null"],"description":"Id of the group this contact belongs to, or `null`.","example":12},"group_name":{"type":["string","null"],"description":"Display name of the group, or `null` when ungrouped.","example":"Sunday greeters"},"notes":{"type":["string","null"],"description":"Free-text notes about the contact, or `null`."},"created_at":{"type":"number","description":"Epoch milliseconds (UTC) the contact was created."}},"required":["id","full_name","first_name","last_name","email_address","mobile_phone","land_phone","group_id","group_name","notes","created_at"]},"ContactCreateRequest":{"type":"object","properties":{"full_name":{"type":"string","minLength":1,"maxLength":200,"description":"Display name, shown on the roster and in reminder text. Required.","example":"Ann Adams"},"first_name":{"type":["string","null"],"maxLength":120,"description":"First name, or `null`/omit if not recorded separately.","example":"Ann"},"last_name":{"type":["string","null"],"maxLength":120,"description":"Last name, or `null`/omit if not recorded separately.","example":"Adams"},"email_address":{"type":["string","null"],"maxLength":320,"format":"email","description":"Email address reminders may be sent to, or `null`/omit.","example":"ann@example.org"},"mobile_phone":{"type":["string","null"],"maxLength":32,"description":"Mobile phone number, used for SMS reminders. Accepts E.164 (`+15125550100`) or a plain 10-digit US/Canada number; normalized to E.164 on save. Send `null` or `\"\"` to clear it.","example":"+15125550100"},"land_phone":{"type":["string","null"],"maxLength":32,"description":"Landline phone number (not SMS-capable, informational only), or `null`/omit.","example":"+15125550199"},"group_id":{"type":["integer","null"],"exclusiveMinimum":0,"description":"Id of an existing group on this account to file the contact under, or `null` for no group. Must reference a group already owned by this account.","example":12},"notes":{"type":["string","null"],"maxLength":2000,"description":"Free-text notes about the contact.","example":"Prefers Saturday shifts."}},"required":["full_name"]},"BulkCreateContactsResponse":{"type":"object","properties":{"created":{"type":"array","items":{"$ref":"#/components/schemas/Contact"},"description":"The newly created contacts, in the same order as submitted."}},"required":["created"]},"BulkCreateContactsRequest":{"type":"object","properties":{"contacts":{"type":"array","items":{"$ref":"#/components/schemas/ContactCreateRequest"},"minItems":1,"maxItems":200,"description":"Contacts to add, up to 200 per request."}},"required":["contacts"]},"ContactUpdateRequest":{"type":"object","properties":{"full_name":{"type":"string","minLength":1,"maxLength":200,"description":"Display name. Omit to leave unchanged.","example":"Ann Adams"},"first_name":{"type":["string","null"],"maxLength":120,"description":"First name. Omit to leave unchanged; `null` clears it.","example":"Ann"},"last_name":{"type":["string","null"],"maxLength":120,"description":"Last name. Omit to leave unchanged; `null` clears it.","example":"Adams"},"email_address":{"type":["string","null"],"maxLength":320,"format":"email","description":"Email address. Omit to leave unchanged; `null` clears it.","example":"ann@example.org"},"mobile_phone":{"type":["string","null"],"maxLength":32,"description":"Mobile phone number, used for SMS reminders. Accepts E.164 (`+15125550100`) or a plain 10-digit US/Canada number; normalized to E.164 on save. Send `null` or `\"\"` to clear it.","example":"+15125550100"},"land_phone":{"type":["string","null"],"maxLength":32,"description":"Landline phone number. Omit to leave unchanged; `null` clears it.","example":"+15125550199"},"group_id":{"type":["integer","null"],"exclusiveMinimum":0,"description":"Id of an existing group on this account. Omit to leave unchanged; `null` un-groups the contact.","example":12},"notes":{"type":["string","null"],"maxLength":2000,"description":"Free-text notes about the contact.","example":"Prefers Saturday shifts."}}},"ContactDeleteResponse":{"type":"object","properties":{"id":{"type":"number","description":"The deleted contact's id.","example":4021},"deleted":{"type":"boolean","enum":[true],"description":"Always `true` on success."}},"required":["id","deleted"]},"AssignRosterResponse":{"type":"object","properties":{"schedule_id":{"type":"number","description":"The schedule the roster was assigned to.","example":55},"contact_ids":{"type":"array","items":{"type":"number"},"description":"The schedule's roster after this change (deduplicated)."}},"required":["schedule_id","contact_ids"]},"AssignRosterRequest":{"type":"object","properties":{"contact_ids":{"type":"array","items":{"type":"integer","exclusiveMinimum":0},"maxItems":500,"description":"The complete roster for this schedule — REPLACES any existing assignment (not additive). Pass an empty array to clear the roster. Every id must belong to this account.","example":[101,102,103]}},"required":["contact_ids"]},"ReminderSettingsResponse":{"type":"object","properties":{"global_default":{"$ref":"#/components/schemas/ReminderGlobalDefault"},"job_overrides":{"type":"array","items":{"$ref":"#/components/schemas/ReminderJobOverride"},"description":"Per-job lead-time overrides. Overrides are per JOB (a job may back several schedules), the only override granularity db/reminderConfigQueries.ts supports today — not per individual schedule."},"channels":{"$ref":"#/components/schemas/ReminderChannels"}},"required":["global_default","job_overrides","channels"]},"ReminderGlobalDefault":{"type":["object","null"],"properties":{"id":{"type":"number","description":"reminder_schedule_item id."},"days_before":{"type":"integer","minimum":0,"description":"Whole days before the shift to send this reminder.","example":1},"hours_before":{"type":"integer","minimum":0,"description":"Additional whole hours before the shift.","example":0},"minutes_before":{"type":"integer","minimum":0,"description":"Additional whole minutes before the shift.","example":0}},"required":["id","days_before","hours_before","minutes_before"],"description":"The account-wide default lead time, or null if never configured."},"ReminderJobOverride":{"type":"object","properties":{"id":{"type":"number","description":"reminder_schedule_item id."},"job_id":{"type":"number"},"job_name":{"type":["string","null"]},"days_before":{"type":"integer","minimum":0,"description":"Whole days before the shift to send this reminder.","example":1},"hours_before":{"type":"integer","minimum":0,"description":"Additional whole hours before the shift.","example":0},"minutes_before":{"type":"integer","minimum":0,"description":"Additional whole minutes before the shift.","example":0}},"required":["id","job_id","job_name","days_before","hours_before","minutes_before"]},"ReminderChannels":{"type":"object","properties":{"email":{"type":"object","properties":{"available":{"type":"boolean","description":"Email reminders are always available."}},"required":["available"]},"sms":{"type":"object","properties":{"available":{"type":"boolean","description":"Whether this account's current plan includes SMS/text reminders (`isSmsAllowed` in the effective entitlements — the same gate domain/reminderSelection.ts enforces at send time)."},"reason":{"type":["string","null"],"description":"Why SMS is unavailable, or null when it is available."}},"required":["available","reason"]}},"required":["email","sms"]},"PutReminderSettingsRequest":{"type":"object","properties":{"global_default":{"type":"object","properties":{"days_before":{"type":"integer","minimum":0,"description":"Whole days before the shift to send this reminder.","example":1},"hours_before":{"type":"integer","minimum":0,"description":"Additional whole hours before the shift.","example":0},"minutes_before":{"type":"integer","minimum":0,"description":"Additional whole minutes before the shift.","example":0}},"required":["days_before","hours_before","minutes_before"],"description":"Set (or replace) the account-wide default lead time."},"job_override":{"type":"object","properties":{"job_id":{"type":"integer","exclusiveMinimum":0,"description":"The job this override applies to. Must belong to this account."},"days_before":{"type":"integer","minimum":0,"description":"Whole days before the shift to send this reminder.","example":1},"hours_before":{"type":"integer","minimum":0,"description":"Additional whole hours before the shift.","example":0},"minutes_before":{"type":"integer","minimum":0,"description":"Additional whole minutes before the shift.","example":0}},"required":["job_id","days_before","hours_before","minutes_before"],"description":"Create or replace the lead-time override for one job."},"channels":{"type":"object","properties":{"require_sms":{"type":"boolean","description":"Assert that SMS/text reminders should be used. There is no separate stored channel preference to persist — SMS eligibility is always computed at send time from plan entitlement plus whether each contact has a mobile number. Setting this to `true` is a guard: the whole request is REJECTED (no fields are saved) if the account's plan does not include SMS, so an agent gets an actionable upgrade-hint error instead of a silent no-op. Omit it, or set it to `false`, to make no assertion."}},"required":["require_sms"]}}},"OpenDaysResponse":{"type":"object","properties":{"open_dates":{"type":"array","items":{"$ref":"#/components/schemas/OpenDate"},"description":"Calendar days in the window with no attendable occurrence yet."},"open_schedules":{"type":"array","items":{"$ref":"#/components/schemas/OpenScheduleSlot"},"description":"Recurring Nth-weekday-of-month slots not yet claimed by a schedule."}},"required":["open_dates","open_schedules"]},"OpenDate":{"type":"object","properties":{"date":{"type":"string","description":"ISO calendar date (yyyy-MM-dd), account-local.","example":"2026-08-03"},"weekday":{"type":"string","example":"Monday"}},"required":["date","weekday"]},"OpenScheduleSlot":{"type":"object","properties":{"occurrence_number":{"type":"integer","minimum":1,"maximum":5,"description":"Nth occurrence of the weekday in the month (1-5)."},"day_of_week":{"type":"integer","minimum":0,"maximum":6,"description":"0 = Sunday .. 6 = Saturday."},"is_every_other_month":{"type":"boolean","description":"True when the slot is only open in alternating months."}},"required":["occurrence_number","day_of_week","is_every_other_month"]},"SendLogResponse":{"type":"object","properties":{"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer","description":"Total matching rows across both sources."},"has_more":{"type":"boolean"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/SendLogEntry"}}},"required":["page","per_page","total","has_more","entries"]},"SendLogEntry":{"type":"object","properties":{"type":{"type":"string","enum":["sent","failed"],"description":"'sent' = a reminder was recorded as sent; 'failed' = a send definitively failed."},"occurred_at":{"type":"number","description":"Epoch milliseconds (UTC)."},"channels":{"type":"array","items":{"type":"string","enum":["email","text"]},"description":"Channels involved. A 'sent' row may have both; a 'failed' row has at most one (empty when the failed channel could not be determined)."},"contact_id":{"type":["number","null"],"description":"The volunteer this outcome is about. Null for 'failed' (not attributable)."},"contact_name":{"type":["string","null"],"description":"The volunteer's name. Null for 'failed'."},"schedule_occurrence_id":{"type":["number","null"],"description":"The shift instance. Only present for 'sent'."},"result":{"type":["string","null"],"description":"Free-text send result, when recorded. Only present for 'sent'."},"detail":{"type":["string","null"],"description":"Short failure reason (IDs only, no message content). Only present for 'failed'."}},"required":["type","occurred_at","channels","contact_id","contact_name","schedule_occurrence_id","result","detail"]},"TestReminderResponse":{"type":"object","properties":{"delivered_to":{"type":"string","description":"The account owner's own email the test reminder was sent to.","example":"director@riversidefoodpantry.org"},"channel":{"type":"string","enum":["email"],"description":"Test reminders are always email, always to the owner."},"schedule_id":{"type":["number","null"],"description":"The schedule previewed, or null for a generic sample."},"note":{"type":"string","description":"Human/LLM-readable confirmation of what happened."}},"required":["delivered_to","channel","schedule_id","note"]},"TestReminderRequest":{"type":"object","properties":{"schedule_id":{"type":"integer","exclusiveMinimum":0,"description":"Preview the reminder for this schedule (its job name is used in the sample). Omit for a generic sample. Must belong to this account.","example":7}}},"CheckoutUrlResponse":{"type":"object","properties":{"checkout_url":{"type":"string","description":"A Stripe-hosted Checkout URL. Give this to the human to open and complete payment — the agent never takes payment itself.","example":"https://checkout.stripe.com/c/pay/cs_test_..."},"plan":{"type":"string","example":"deluxe"},"interval":{"type":"string","enum":["monthly","annual"],"example":"monthly"}},"required":["checkout_url","plan","interval"]},"CheckoutUrlRequest":{"type":"object","properties":{"plan":{"type":"string","enum":["basic","deluxe","pro"],"description":"The plan to subscribe to (the public plan ids from GET /pricing).","example":"deluxe"},"interval":{"type":"string","enum":["monthly","annual"],"description":"Billing interval. Defaults to monthly.","example":"monthly"}},"required":["plan"]},"CreatedApiKey":{"type":"object","properties":{"id":{"type":"integer","description":"The new key's id (use it to revoke)."},"name":{"type":"string","description":"The label you supplied."},"key_prefix":{"type":"string","description":"Non-secret display prefix (`vrk_` + 4 hex). Safe to log/show.","example":"vrk_a1b2"},"scopes":{"type":"array","items":{"$ref":"#/components/schemas/Scope"},"description":"The granted scopes, de-duplicated and in canonical order."},"created_at":{"type":"integer","description":"Epoch milliseconds (UTC)."},"api_key":{"type":"string","description":"The full secret — SHOWN ONCE, right here. Store it now; it is never retrievable again. Send it on subsequent calls as `Authorization: Bearer <api_key>`.","example":"vrk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}},"required":["id","name","key_prefix","scopes","created_at","api_key"]},"Scope":{"type":"string","enum":["read","schedules:write","contacts:write","reminders:write","keys:manage"],"description":"A capability an API key can carry. `read` covers every read-only GET; the `*:write` scopes gate the matching mutations; `keys:manage` mints/revokes keys and is never in the default grant.","example":"read"},"NewApiKeyRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80,"description":"A human-readable label so you can tell keys apart (e.g. the agent or script that will use it). 1–80 characters.","example":"Claude food-pantry assistant"},"scopes":{"type":"array","items":{"$ref":"#/components/schemas/Scope"},"minItems":1,"description":"The scopes to grant. MUST be a subset of the calling key's own scopes — a key can only ever mint a narrower-or-equal key, never a broader one. Duplicates are ignored.","example":["read","schedules:write"]}},"required":["name","scopes"]},"ApiKeyList":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeySummary"}},"ApiKeySummary":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"key_prefix":{"type":"string","example":"vrk_a1b2"},"scopes":{"type":"array","items":{"$ref":"#/components/schemas/Scope"}},"created_at":{"type":"integer","description":"Epoch ms (UTC)."},"last_used_at":{"type":["integer","null"],"description":"Epoch ms (UTC) of the last authenticated use, or null."},"revoked_at":{"type":["integer","null"],"description":"Epoch ms (UTC) when revoked, or null if still active."}},"required":["id","name","key_prefix","scopes","created_at","last_used_at","revoked_at"]},"RevokedApiKey":{"type":"object","properties":{"id":{"type":"integer"},"revoked":{"type":"boolean","enum":[true],"description":"Always true on success."}},"required":["id","revoked"]}},"parameters":{}},"paths":{"/api/v1/pricing":{"get":{"tags":["Pricing"],"summary":"Get the public plan catalog","description":"Public, unauthenticated plan/pricing/entitlement catalog. The payload an agent comparison-shopping on a user's behalf reads; no API key required.","security":[],"responses":{"200":{"description":"The plan catalog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricingResponse"}}}}}}},"/api/v1/account":{"get":{"tags":["Account"],"summary":"Get the current account","description":"Plan, entitlements, current reminder mode, and usage vs limits for the account the API key belongs to. Requires the `read` scope.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"The account overview.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/signups":{"post":{"tags":["Signups"],"summary":"Create an account (pending email verification)","description":"Public, unauthenticated. Creates an inert trial account for the supplied email and sends the owner a verification link — no API key is returned. The human must click the link to activate; agent-created accounts start with reminders in the safe test posture and can never reach real volunteers until a human turns live sending on from the dashboard. Idempotent on a repeat email (same response, no second account) and heavily rate-limited.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"}}}},"responses":{"200":{"description":"The account exists (created now, or already present) and a verification email was sent. No credential is returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupResponse"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many signup attempts from this client. Back off and retry after a short wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/schedules/preview":{"post":{"tags":["Schedules"],"summary":"Preview upcoming occurrence dates for a recurrence config","description":"PURE and side-effect-free: no schedule is read or written. Wraps domain/recurrence.ts + domain/timezone.ts to show the next occurrence dates a recurrence configuration would produce, so an agent can confirm with the human before calling POST /schedules. Requires the `read` scope.","security":[{"BearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchedulePreviewRequest"}}}},"responses":{"200":{"description":"The computed occurrence dates.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchedulePreviewResponse"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/schedules":{"get":{"tags":["Schedules"],"summary":"List schedules","description":"All active (non-archived) schedules for the account. Requires the `read` scope.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"The account's schedules.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleSummary"}}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Schedules"],"summary":"Create a schedule","description":"Creates a schedule and materializes its upcoming occurrences. Validation mirrors the dashboard's add-schedule form. Requires the `schedules:write` scope.","security":[{"BearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleCreateRequest"}}}},"responses":{"201":{"description":"The created schedule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Schedule"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/schedules/{id}/occurrences":{"get":{"tags":["Schedules"],"summary":"List upcoming shifts for a schedule","description":"Occurrences in [from, to] plus the schedule's roster and each date's attendance/exception status. Requires the `read` scope.","security":[{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"The schedule's numeric id.","example":"42"},"required":true,"description":"The schedule's numeric id.","name":"id","in":"path"},{"schema":{"type":"string","description":"Start of the range: \"yyyy-MM-dd\" (interpreted in the account timezone) or a full ISO 8601 datetime. Defaults to now.","example":"2026-07-01"},"required":false,"description":"Start of the range: \"yyyy-MM-dd\" (interpreted in the account timezone) or a full ISO 8601 datetime. Defaults to now.","name":"from","in":"query"},{"schema":{"type":"string","description":"End of the range, same formats as `from`. Defaults to 90 days from now.","example":"2026-09-30"},"required":false,"description":"End of the range, same formats as `from`. Defaults to 90 days from now.","name":"to","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Maximum rows to return (default 20, max 100).","example":20},"required":false,"description":"Maximum rows to return (default 20, max 100).","name":"limit","in":"query"}],"responses":{"200":{"description":"Upcoming occurrences.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleOccurrencesResponse"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/schedules/{id}":{"get":{"tags":["Schedules"],"summary":"Get a schedule","description":"Requires the `read` scope.","security":[{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"The schedule's numeric id.","example":"42"},"required":true,"description":"The schedule's numeric id.","name":"id","in":"path"}],"responses":{"200":{"description":"The schedule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Schedule"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Schedules"],"summary":"Update a schedule","description":"Partial update — only send the fields you want to change. Unset fields keep their current value. Recomputes and re-materializes future occurrences when recurrence fields change. Does NOT change the roster: use `POST /schedules/{id}/roster` for that. Requires the `schedules:write` scope.","security":[{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"The schedule's numeric id.","example":"42"},"required":true,"description":"The schedule's numeric id.","name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleUpdateRequest"}}}},"responses":{"200":{"description":"The updated schedule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Schedule"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Schedules"],"summary":"Archive a schedule","description":"Soft-deletes (archives) the schedule; past reminder history is preserved. Requires the `schedules:write` scope.","security":[{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"The schedule's numeric id.","example":"42"},"required":true,"description":"The schedule's numeric id.","name":"id","in":"path"}],"responses":{"200":{"description":"Archive confirmation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleArchiveResponse"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/contacts":{"get":{"tags":["Contacts"],"summary":"List volunteers","description":"All non-deleted contacts on this account's roster, ordered by name. Requires `read`.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"The roster.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Contacts"],"summary":"Add a volunteer","description":"Add one contact to the roster. Blocked with `validation_error` when the account is already at its plan's volunteer limit (domain/volunteerCap.ts, the same rule the dashboard enforces). Requires `contacts:write`.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactCreateRequest"}}}},"responses":{"201":{"description":"The created contact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/contacts/bulk":{"post":{"tags":["Contacts"],"summary":"Add volunteers in bulk","description":"Add up to 200 contacts in one call. ALL-OR-NOTHING: if the batch would push the account's active-volunteer count past its plan's limit, or any contact references a `group_id` that doesn't belong to this account, NOTHING is created and a `validation_error` names the limit, the account's current count, or the bad group id. Requires `contacts:write`.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateContactsRequest"}}}},"responses":{"201":{"description":"The created contacts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateContactsResponse"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/contacts/{id}":{"patch":{"tags":["Contacts"],"summary":"Update a volunteer","description":"Partial update — only the fields present in the body are changed. Requires `contacts:write`.","security":[{"BearerAuth":[]}],"parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Contact id.","example":4021},"required":true,"description":"Contact id.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactUpdateRequest"}}}},"responses":{"200":{"description":"The updated contact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Contacts"],"summary":"Remove a volunteer","description":"Soft-deletes the contact (frees a volunteer-cap slot; removes it from any schedule rosters it was on). Requires `contacts:write`.","security":[{"BearerAuth":[]}],"parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Contact id.","example":4021},"required":true,"description":"Contact id.","name":"id","in":"path"}],"responses":{"200":{"description":"Confirmation of the delete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactDeleteResponse"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/schedules/{id}/roster":{"post":{"tags":["Contacts"],"summary":"Replace a schedule's roster","description":"Assign contacts to a schedule. REPLACES the schedule's whole roster (matches the dashboard's save-the-form semantics) — send the complete desired set of contact ids, not a delta. Every id must belong to this account or the whole request is rejected. Requires `contacts:write`.","security":[{"BearerAuth":[]}],"parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"Schedule id.","example":55},"required":true,"description":"Schedule id.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRosterRequest"}}}},"responses":{"200":{"description":"The roster after the change.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRosterResponse"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/reminder-settings":{"get":{"tags":["Reminders"],"summary":"Get the reminder lead times and channel availability","description":"The account-wide default lead time, any per-job overrides, and which channels (email/SMS) are available on the current plan. Requires the `read` scope.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Reminder configuration for the account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReminderSettingsResponse"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Reminders"],"summary":"Update the reminder lead times","description":"Set the account-wide default lead time and/or a per-job override. Requires the `reminders:write` scope. Fails validation (no writes performed) if neither `global_default` nor `job_override` is provided, if a lead time leaves all three fields at zero, if `job_override.job_id` is not a job on this account, or if `channels.require_sms` is true on a plan without SMS.","security":[{"BearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutReminderSettingsRequest"}}}},"responses":{"200":{"description":"The updated reminder configuration.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReminderSettingsResponse"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/open-days":{"get":{"tags":["Reminders"],"summary":"Get unfilled slots (who do we still need?)","description":"Wraps domain/openDays.ts (the same logic behind the public sign-up widget): calendar days with no attendable shift yet, plus recurring monthly slots not yet claimed by a schedule. Requires the `read` scope.","security":[{"BearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":24,"description":"How many months to look ahead (1-24). Default 12.","example":3},"required":false,"description":"How many months to look ahead (1-24). Default 12.","name":"months","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"When true (the default), clamp the window to the end of the current calendar year instead of crossing into next year.","example":"true"},"required":false,"description":"When true (the default), clamp the window to the end of the current calendar year instead of crossing into next year.","name":"current_year_only","in":"query"}],"responses":{"200":{"description":"Open days and open recurring slots.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenDaysResponse"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/send-log":{"get":{"tags":["Reminders"],"summary":"Get recent reminder send outcomes","description":"Recent reminder outcomes merged from the sent log and the dead-letter failure log, newest first. Requires the `read` scope.","security":[{"BearerAuth":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"description":"1-indexed page number. Default 1.","example":1},"required":false,"description":"1-indexed page number. Default 1.","name":"page","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page (max 100). Default 20.","example":20},"required":false,"description":"Rows per page (max 100). Default 20.","name":"per_page","in":"query"}],"responses":{"200":{"description":"A page of send-outcome entries.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendLogResponse"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/reminder-settings/job-overrides/{id}":{"delete":{"tags":["Reminders"],"summary":"Remove a per-job reminder override","description":"Soft-delete the reminder lead-time override for one job (addressed by JOB id). Returns the updated reminder configuration. 404 if the job has no active override on this account. Requires the `reminders:write` scope.","security":[{"BearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^\\d+$","description":"The JOB id whose lead-time override should be removed.","example":"42"},"required":true,"description":"The JOB id whose lead-time override should be removed.","name":"id","in":"path"}],"responses":{"200":{"description":"The override was removed; the updated configuration.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReminderSettingsResponse"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/reminder-settings/global-default":{"delete":{"tags":["Reminders"],"summary":"Clear the account-wide default reminder","description":"Soft-delete the account-wide default reminder lead time. Returns the updated reminder configuration. 404 if no active default is set. Requires the `reminders:write` scope.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"The default was cleared; the updated configuration.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReminderSettingsResponse"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/reminders/test":{"post":{"tags":["Reminders"],"summary":"Send a test reminder to the account owner","description":"Send ONE sample reminder to the account OWNER's own email so they can preview what a reminder looks like. It NEVER reaches volunteers — it always targets the owner — so it respects the test-mode safety model trivially. Requires the `reminders:write` scope.","security":[{"BearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestReminderRequest"}}}},"responses":{"200":{"description":"A test reminder was queued to the owner's email.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestReminderResponse"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/billing/checkout-url":{"post":{"tags":["Billing"],"summary":"Get a Stripe Checkout URL for the human to complete","description":"Create a Stripe-hosted Checkout session for a named plan/interval and return its URL for the ACCOUNT OWNER to open and pay — the agent never takes payment. Reuses the dashboard's session-creation logic, including the already-subscribed guard and idempotency key. Requires the `read` scope (this endpoint returns a URL and mutates no agent-managed data; the irreversible act, payment, stays with the human — agent-channel §3.4).","security":[{"BearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutUrlRequest"}}}},"responses":{"200":{"description":"A hosted Checkout URL for the human to complete payment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutUrlResponse"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The account already has an active subscription.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Billing is temporarily unavailable (Stripe not configured/reachable).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/keys":{"post":{"tags":["Keys"],"summary":"Mint a new API key","description":"Create a new API key for this account. The requested scopes must be a subset of the calling key's scopes (a key can never widen authority). Requires the `keys:manage` scope. The secret is returned exactly once.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewApiKeyRequest"}}}},"responses":{"201":{"description":"The key was created; the secret is in `api_key` (shown once).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedApiKey"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Key issuance is being rate-limited for this account. Retry shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Keys"],"summary":"List API keys","description":"List this account's API keys (including revoked ones, so their status is visible). The secret is NEVER returned. Requires the `keys:manage` scope.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"The account's keys, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyList"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/keys/{id}":{"delete":{"tags":["Keys"],"summary":"Revoke an API key","description":"Soft-revoke one of this account's API keys. Idempotent per key; a revoked key can never authenticate again. Requires the `keys:manage` scope. Returns 404 for an unknown id, an already-revoked key, or another account's key.","security":[{"BearerAuth":[]}],"parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"description":"The id of the key to revoke (from POST/GET /api/v1/keys).","example":42},"required":true,"description":"The id of the key to revoke (from POST/GET /api/v1/keys).","name":"id","in":"path"}],"responses":{"200":{"description":"The key was revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokedApiKey"}}}},"400":{"description":"The request failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key lacks a scope required by this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{}}