diff --git a/.changeset/scheduling-delegates.md b/.changeset/scheduling-delegates.md new file mode 100644 index 000000000..858f62a36 --- /dev/null +++ b/.changeset/scheduling-delegates.md @@ -0,0 +1,6 @@ +--- +"coss": minor +"coss-mcp": minor +--- + +Name who covers while a scheduling host is away: `delegates` on `coss.scheduling.outOfOffice` entries can be booked in their place, inside the same event type, keeping to how a booking's routed. The MCP server's `create_scheduling_out_of_office` takes `delegates` and `existing_bookings`. diff --git a/.changeset/scheduling-holidays.md b/.changeset/scheduling-holidays.md new file mode 100644 index 000000000..9c7164a42 --- /dev/null +++ b/.changeset/scheduling-holidays.md @@ -0,0 +1,5 @@ +--- +"coss": minor +--- + +Holiday calendars for scheduling: `coss.scheduling.holidayCalendars` applies a country's public holidays, or your own feed, to some hosts as out of office entries, kept up to date by coss.com. A host who removes a day keeps it removed. diff --git a/.changeset/scheduling-manager-mcp.md b/.changeset/scheduling-manager-mcp.md new file mode 100644 index 000000000..2f3ae55cc --- /dev/null +++ b/.changeset/scheduling-manager-mcp.md @@ -0,0 +1,6 @@ +--- +"coss-mcp": minor +"coss-cli": minor +--- + +The MCP server's `create_scheduling_session` can make a team lead's page with `manager_hosts`, and `coss open` knows the out of office, holidays and reassignments pages. diff --git a/.changeset/scheduling-manager.md b/.changeset/scheduling-manager.md new file mode 100644 index 000000000..7a0e7d3ab --- /dev/null +++ b/.changeset/scheduling-manager.md @@ -0,0 +1,5 @@ +--- +"coss": minor +--- + +Team pages for scheduling: `coss.scheduling.sessions.createForManager` gives a team lead a hosted, embeddable page (`mountTeamPortal` in `coss/scheduling`) for the hosts you name or everyone matching some attributes, showing who's away and what needs cover, and moving it. `host_reassignment` in settings lets hosts give away their own bookings. diff --git a/.changeset/scheduling-out-of-office.md b/.changeset/scheduling-out-of-office.md new file mode 100644 index 000000000..404efff02 --- /dev/null +++ b/.changeset/scheduling-out-of-office.md @@ -0,0 +1,5 @@ +--- +"coss": minor +--- + +Add `coss.scheduling.outOfOffice`: mark hosts as away, for part of a day or whole dates, one at a time or many together. They stop being offered, existing bookings stay and are listed as conflicts, and `scheduling.out_of_office.*` webhooks say when it's created, changed, starts and ends. diff --git a/.changeset/scheduling-reassign.md b/.changeset/scheduling-reassign.md new file mode 100644 index 000000000..b4f3dd839 --- /dev/null +++ b/.changeset/scheduling-reassign.md @@ -0,0 +1,7 @@ +--- +"coss": minor +"coss-mcp": minor +"coss-cli": minor +--- + +Reassign scheduling bookings: `coss.scheduling.bookings.reassign` gives any kind of booking to another host, keeping its id and time, and `reassignCandidates` says who could take it and why not. Adds a `scheduling.booking.reassigned` webhook with `previous_host_ids`, `reassignment_enabled` on event types and `name_host_in_attendee_emails` in settings. The MCP server gets reassign and out of office tools, and the CLI `coss scheduling reassign`, `candidates` and `ooo`. diff --git a/.changeset/scheduling-reassignments.md b/.changeset/scheduling-reassignments.md new file mode 100644 index 000000000..25bad1f41 --- /dev/null +++ b/.changeset/scheduling-reassignments.md @@ -0,0 +1,7 @@ +--- +"coss": minor +"coss-mcp": minor +"coss-cli": minor +--- + +Move many scheduling bookings at once: `coss.scheduling.reassignments` previews who each would go to, runs it in the background with an outcome per booking, and can cancel and undo. Out of office entries take `existing_bookings: "reassign"` to start one. Adds a `scheduling.reassignment.completed` webhook. The MCP server gets preview, create, get, cancel and undo tools, and the CLI `coss scheduling reassign-many`, `reassignments` and `reassignment`. diff --git a/AGENTS.md b/AGENTS.md index b5ed55436..e6daa64d7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,7 +64,7 @@ text it shows is a gist that must match `CLA.md`: change both together, as a new | `packages/sign` | `@coss/sign` | Sign (e-signatures): uploaded PDFs (`pdf.ts`: pdf-lib + pdf.js, page sizes and text anchors), signature requests with signers and fields placed by position or anchor, the hash-chained audit trail (`events.ts`) with its webhooks and log lines (`record.ts`), signing sessions (`sessions.ts`), the signer's actions (`signing.ts`: view, consent, sign, decline), email delivery (`notifications.ts`: invitations in signing order, reminders, expiry, signed copies through an injected `SignMailer`), completion (`complete.ts`: the completed document with its certificate pages, `completed-pdf.ts`, sealed as PAdES B-LT by `seal/`: KMS, a CA's CSC API or a local key, RFC 3161 timestamps, OCSP/CRLs in a DSS), templates (`templates.ts`: roles, merge fields, requests from a template) and builder sessions (`builder.ts`), settings (`settings.ts`: embedding origins, branding, consent text), the document store (`store.ts`: `DocumentStore`, `FakeDocumentStore`); `./schemas` (zod only, also `SIGN_WEBHOOK_EVENT_TYPES`), `./env` (zod only), `./runtime` (`sealerFromEnv`), `./testing` (`buildTestPdf`, `TEST_SIGNATURE_PNG`). See `packages/sign/AGENTS.md`. | | `packages/verify` | `@coss/verify` | Verify (one-time codes): `createVerifyService` (`start`: the fraud guard — `@coss/risk`'s `checkVerificationRisk` — before anything is paid for, then a code by SMS from the environment's own number or coss.com's shared numbers, or by email through an injected sender; `check` against an HMAC, never the code; `cancel`), settings (`settings.ts`), reads, shared-number metering (`recordDueVerifySmsUsage`), purges; `./schemas` (zod only, also `VERIFY_WEBHOOK_EVENT_TYPES`), `./testing`. Never imports `@coss/email` (email imports its schemas). See `packages/verify/AGENTS.md`. | | `packages/forms` | `@coss/forms` | Forms (hosted form endpoints): forms and their settings (`forms.ts`: field definitions, origins, spam, submitter verification, notifications, contacts mapping, uploads, hosted pages), taking a submission (`submit.ts`, `parse.ts`: origin, fields, honeypot, a Risk assessment, the content check, files, then `record.ts`: usage, `form.submission.*` webhooks, log lines, deliveries), the worker's deliveries (`deliveries.ts`: notification emails, contacts, verification links through an injected `FormMailer`/`FormContactSink`), release and confirmation (`release.ts`), reads (`submissions.ts`), hosted pages and their CSP (`hosted.ts`, `pages.ts`), the file store (`store.ts`, S3 under `./runtime`, cleanup), purges; `./schemas` (zod only, also `FORM_WEBHOOK_EVENT_TYPES`), `./env` (zod only), `./runtime`, `./testing`. See `packages/forms/AGENTS.md`. | -| `packages/scheduling` | `@coss/scheduling` | Scheduling: hosts, schedules, event types (`one_on_one`, `pool`, `collective`, `group`), availability, the booking path (`claims.ts`: the only writer of `scheduling_claim`, whose exclusion constraint prevents double-booking), holds, approval, recurring series, sessions, the notification outbox with injected `SchedulingNotifier`, purges; `./engine` (pure: time zones, `isBookable`, ranking, recurrence), `calendar/` (`fetch`-based clients for Google, Microsoft and CalDAV, sync, write-back); `./schemas` (zod only, also `SCHEDULING_WEBHOOK_EVENT_TYPES`), `./env` (zod only), `./runtime`, `./testing`. See `packages/scheduling/AGENTS.md`. | +| `packages/scheduling` | `@coss/scheduling` | Scheduling: hosts, schedules, out of office (`out-of-office.ts`), event types (`one_on_one`, `pool`, `collective`, `group`), availability, the booking path (`claims.ts`: the only writer of `scheduling_claim`, whose exclusion constraint prevents double-booking), holds, approval, recurring series, sessions, the notification outbox with injected `SchedulingNotifier`, purges; `./engine` (pure: time zones, `isBookable`, ranking, recurrence), `calendar/` (`fetch`-based clients for Google, Microsoft and CalDAV, sync, write-back); `./schemas` (zod only, also `SCHEDULING_WEBHOOK_EVENT_TYPES`), `./env` (zod only), `./runtime`, `./testing`. See `packages/scheduling/AGENTS.md`. | | `packages/support` | `@coss/support` | coss.com's support desk: tickets from email (`intake.ts`, reading `received_email` of `SUPPORT_ENVIRONMENT_ID`) and the dashboard, sender matching and trust (`matching.ts`, `sender.ts`: a match never grants access by itself), the SLA clock (`sla.ts`, targets in `@coss/billing/catalog`'s `SUPPORT_TIERS`), delivery through an injected `SupportMailer` (`delivery.ts`), the worker's housekeeping and rep alerts (`sweep.ts`), attachments (`store.ts`); `./admin` (reps' queue and actions, platform-level), `./format` and `./schemas` (client-safe), `./env` (zod only), `./testing`. See `packages/support/AGENTS.md`. | | `packages/push` | `@coss/push` | Push notifications to the iOS app: devices (one per app session), the outbox products enqueue into (`enqueuePush`, in the deciding transaction), recipients (role + `orgAccess`) and content resolved at send time, the worker's send loop; `./apns` (APNs over `node:http2`, ES256 provider tokens), `./schemas` (zod only: categories, kinds, registration), `./testing` (`FakePushSender`, `createTestPushDevice`). See `packages/push/AGENTS.md`. | | `packages/notifications` | `@coss/notifications` | coss.com's notification emails to organizations: the outbox products enqueue into (`enqueueNotification`, in the deciding transaction; once per dedupe key, optionally delayed), kinds rendered at send time (stale news is skipped), the worker's `notifications` loop, owner/admin recipients, retention; `./testing` (`recordingNotifier`). See `packages/notifications/AGENTS.md`. | @@ -533,13 +533,13 @@ notification emails) and, if there is useful advice, to `ADVICE` (`packages/repu `recordOpen`/`recordClick` (from the email's tracking token), `trackingBaseUrlFor` (dispatch; reads domain names across orgs like `authorizeLiveSender`), `receiveEmail`/`routeRecipients` (receiving; from the envelope recipients), `recordSmsEvent`/`ingestSmsEvents` (from the `sms_id` of the callback URL), `receiveSms` (from the receiving number), `handleStatusCallback`/`handleInboundCallback` (from the callback's subaccount), `authorizeLiveSmsSender`, `ingestDmarcReports` (from the report address's token), `reencryptWebhookSecrets`, `reencryptSchedulingData` (operator scripts), `authenticateHookPortalSession` (from the session), `ingestSupportEmails` (one environment, across senders) and the rest of `@coss/support`'s worker functions, `submitForm`/`getHostedForm` (from the form id of `/f/:id`), `confirmFormSubmission`/`findConfirmation` (from the verification token), and the worker scans `runDueWebhookDeliveries`, `runDueLogDrainDeliveries`, `runDueHookDeliveries`, `parkHeldHookDeliveries`, `runDueHookRecoveries`, the Hooks purges, `recheckDueDomains`, `recheckDueTrackingDomains`, `recheckDueReceivingDomains`, `reconcileDedicatedIpPools`, `sweepStaleClaims`, `sweepUnclaimedEmails`, `runDueReputationEvaluations`, `runDueSubaccountEvaluations`, `runDueOrganizationDeletions`, `runDueOrganizationLogos`, `runDueSubaccountDeletions`, `runDueDmarcNotifications`, `runDueMigrations`, `EmailService.dispatchDue`, - `unclaimedEmailBacklog`, `unclaimedSmsBacklog` (the `dispatch-backlog` loop: counts only), `SmsService.dispatchDue`, `sweepStaleSmsClaims`, `sweepUnclaimedSms`, `advanceDueSmsRegistrations`, `chargeDueSmsFees`, `chargeDueInboxFees`, `chargeDueHostFees`, `recordDueVerifySmsUsage` (bills each organization for its codes from coss.com's shared numbers, reading coss.com's messages), - `authenticateSchedulingSession` (from the token), `handleCalendarPush`, `completeCalendarAuthorization` (from the push's channel and the consent's state), `runDueCalendarDelegations`, `handleZoomDeauthorization` (from Zoom's signed event), `refreshIdleZoomConnections`, `runDueCalendarSyncs`, `runDueCalendarWrites`, `renewDueCalendarChannels`, `checkHostEditedEvents`, `runDueSchedulingNotifications`, `runDueReminderSyncs`, `expireDueBookings`, + `unclaimedEmailBacklog`, `unclaimedSmsBacklog` (the `dispatch-backlog` loop: counts only), `SmsService.dispatchDue`, `sweepStaleSmsClaims`, `sweepUnclaimedSms`, `advanceDueSmsRegistrations`, `chargeDueSmsFees`, `chargeDueInboxFees`, `runDueReassignments`, `runDueHolidayCalendars`, `chargeDueHostFees`, `recordDueVerifySmsUsage` (bills each organization for its codes from coss.com's shared numbers, reading coss.com's messages), + `authenticateSchedulingSession` (from the token), `handleCalendarPush`, `completeCalendarAuthorization` (from the push's channel and the consent's state), `handleZoomDeauthorization` (from Zoom's signed event), `refreshIdleZoomConnections`, `runDueCalendarSyncs`, `runDueCalendarWrites`, `renewDueCalendarChannels`, `checkHostEditedEvents`, `runDueSchedulingNotifications`, `runDueCalendarDelegations`, `runDueReminderSyncs`, `expireDueBookings`, `announceDueOutOfOffice`, `runDueFormDeliveries`, `runDueFormStoreCleanup` (and the dispatch jobs, from the job's `organizationId`); `@coss/sms/admin` is platform-level like `@coss/email/admin`. Server actions take only slugs + target ids and derive the org from `requireOrgPermission`; every Better Auth organization call passes `organizationId` explicitly (never rely on `session.activeOrganizationId`). -- **Client-safe modules** never import `@coss/db`: `@coss/compliance/catalog`, `@coss/dashboard-routes`, `@coss/risk/{schemas,env}`, `@coss/observability/*`, `@coss/auth/{permissions,slugs,constants,api-key-permissions,org-access}`, `@coss/push/schemas`, `@coss/forms/{schemas,env}`, `@coss/scheduling/branding`, `@coss/email/schemas`, +- **Client-safe modules** never import `@coss/db`: `@coss/compliance/catalog`, `@coss/dashboard-routes`, `@coss/risk/{schemas,env}`, `@coss/observability/*`, `@coss/auth/{permissions,slugs,constants,api-key-permissions,org-access}`, `@coss/push/schemas`, `@coss/forms/{schemas,env}`, `@coss/scheduling/{branding,holiday-countries}`, `@coss/email/schemas`, `@coss/email/{broadcast-schemas,contact-schemas,inbox-schemas,subaccount-schemas,test-event-schemas,env,templates}`, `@coss/sms/{schemas,segments,env}`, `@coss/verify/schemas`, `@coss/hooks/{schemas,env}`, `@coss/reputation/schemas`, `@coss/screening/env`, `@coss/webhooks/{schemas,env}`, `@coss/ratelimit/buckets`, `@coss/versioning`, `@coss/retention`, `@coss/logs/{schemas,query}`, `@coss/billing/{catalog,schemas,engine,money,env,gate,wire}` (`gate` is types only), `@coss/ui/*`, `@coss/ui/theme.css`, `@coss/companion`, `@coss/migrate/templates`, `@coss/email-layout`. `@coss/webhooks`, `@coss/db/jobs`, `@coss/screening/{ai,runtime}`, `@coss/email/{runtime,ses,dispatch,domain-connect}`, `@coss/risk/{runtime,datasets}`, `@coss/signals/*`, `@coss/sms` (root, `./twilio`, `./runtime`, `./dispatch`, `./admin`), `@coss/push` (root, `./apns`), `@coss/notifications` and `@coss/billing` (root, `./metering`, `./runtime`, `./stripe`) are server-only; only `./stripe` (and `./runtime`) import the Stripe SDK. Server-only web modules start with `import "server-only"`. - **Rate limits** (`@coss/ratelimit`): buckets are defined only in `BUCKETS` (`src/buckets.ts`); GCRA state is one @@ -702,6 +702,8 @@ notification emails) and, if there is useful advice, to `ADVICE` (`packages/repu addresses on verified domains; hosted pages refuse fields asking for credentials and are screened when published; `/f/*` is served only on `FORMS_PUBLIC_URL`'s host in production, never coss.com's. - **Scheduling** (`packages/scheduling/AGENTS.md`): only `claims.ts` writes claims, in the fixed lock order; + out of office stops new bookings and only moves an existing one if asked to, and its reason stays out of webhooks, + logs, calendars and anything a booker sees; a bulk reassignment never cancels a booking; slots are computed only in Bun, through `isBookable`; calendars contribute busy intervals and nothing else, and an unreadable one never reads as free; answers never leave `getBooking`; coss.com never auto-cancels a booking; plan gates and billing holds apply to live environments only, and refuse new setup and new bookings, never what @@ -798,7 +800,7 @@ notification emails) and, if there is useful advice, to `ADVICE` (`packages/repu **Not built yet** (don't assume these exist; delete a line when it ships): OAuth for the hosted MCP server (it takes an API key in a header) and `COSS_MCP_TOOLSETS` there, these parts of Support: on-call paging (so 24x7 can't be sold yet), a holiday calendar, automatic SLA credits, a support role narrower than platform admin, satisfaction ratings, AI drafts, presence, iOS screens, a public API, MCP and CLI tools, -these parts of Scheduling: public booking pages, built-in payments, customers' own OAuth apps, routing forms, a workflow builder, waitlists, travel schedules, imports from Cal.com Platform, reminders at a local time of day, reminder copy per locale, quiet hours, a built-in rating page, fixed SMS numbers as reminder recipients, sending a test reminder, undoing a no-show, a per-attendee opt-out of reminder emails, reminders in the iOS app, +these parts of Scheduling: iOS screens for out of office and reassignment, recurring out of office, out of office detected from a calendar, delegates for required hosts, Slack and HR apps, adding a host to a booking without replacing one, public booking pages, built-in payments, customers' own OAuth apps, routing forms, a workflow builder, waitlists, travel schedules, imports from Cal.com Platform, reminders at a local time of day, reminder copy per locale, quiet hours, a built-in rating page, fixed SMS numbers as reminder recipients, sending a test reminder, undoing a no-show, a per-attendee opt-out of reminder emails, reminders in the iOS app, these parts of calendar delegation: automatic Microsoft secret rotation, coss.com-owned or keyless (`signJwt`) service accounts, certificate credentials, account-level Zoom, excluding one host while `match_host_email` is on, and delegation in the iOS app, MCP and CLI, customers granting staff temporary access in HIPAA mode, Verify's voice and WhatsApp channels, locales other than English, custom message templates, diff --git a/apps/api/openapi.json b/apps/api/openapi.json index a552227d8..07c47426e 100644 --- a/apps/api/openapi.json +++ b/apps/api/openapi.json @@ -3830,6 +3830,7 @@ "scheduling.booking.external_conflict", "scheduling.booking.calendar_write_failed", "scheduling.booking.location_ready", + "scheduling.booking.reassigned", "scheduling.booking.started", "scheduling.booking.ended", "scheduling.booking.reminder" @@ -3928,6 +3929,17 @@ "type": "string", "description": "`scheduling.booking.rescheduled`: when it used to start." }, + "previous_host_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "On `scheduling.booking.reassigned`: who had it before." + }, + "reassignment_id": { + "type": "string", + "description": "On `scheduling.booking.reassigned`: the bulk reassignment it was part of." + }, "reminder": { "type": "object", "properties": { @@ -4165,6 +4177,229 @@ } ] }, + "SchedulingOutOfOfficeWebhookEvent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The event id." + }, + "object": { + "type": "string", + "enum": [ + "event" + ] + }, + "api_version": { + "type": "string", + "description": "The API version this payload is rendered in: the endpoint's." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the event was created." + }, + "type": { + "type": "string", + "enum": [ + "scheduling.out_of_office.created", + "scheduling.out_of_office.updated", + "scheduling.out_of_office.deleted", + "scheduling.out_of_office.started", + "scheduling.out_of_office.ended" + ] + }, + "data": { + "type": "object", + "properties": { + "out_of_office_id": { + "type": "string" + }, + "host_id": { + "type": "string" + }, + "start": { + "type": "string" + }, + "end": { + "type": "string" + }, + "group": { + "type": [ + "string", + "null" + ] + }, + "occurred_at": { + "type": "string" + } + }, + "required": [ + "out_of_office_id", + "host_id", + "start", + "end", + "group", + "occurred_at" + ] + } + }, + "required": [ + "id", + "object", + "api_version", + "created_at", + "type", + "data" + ], + "examples": [ + { + "id": "evt_01kd2m9a0b1c2d3e4f5g6h7j8m", + "object": "event", + "type": "scheduling.out_of_office.created", + "api_version": "2026-10-10", + "created_at": "2027-01-01T10:00:00.000Z", + "data": { + "out_of_office_id": "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m", + "host_id": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "start": "2027-08-02T04:00:00.000Z", + "end": "2027-08-14T04:00:00.000Z", + "group": null, + "occurred_at": "2027-01-01T10:00:00.000Z" + } + } + ] + }, + "SchedulingReassignmentWebhookEvent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The event id." + }, + "object": { + "type": "string", + "enum": [ + "event" + ] + }, + "api_version": { + "type": "string", + "description": "The API version this payload is rendered in: the endpoint's." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the event was created." + }, + "type": { + "type": "string", + "enum": [ + "scheduling.reassignment.completed" + ] + }, + "data": { + "type": "object", + "properties": { + "reassignment_id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "completed", + "cancelled" + ] + }, + "out_of_office_id": { + "type": [ + "string", + "null" + ] + }, + "counts": { + "type": "object", + "properties": { + "total": { + "type": "number" + }, + "pending": { + "type": "number" + }, + "reassigned": { + "type": "number" + }, + "skipped": { + "type": "number", + "description": "They'd moved on since it was selected." + }, + "no_candidate": { + "type": "number", + "description": "Nobody could take them: they stayed." + }, + "failed": { + "type": "number" + }, + "cancelled": { + "type": "number" + } + }, + "required": [ + "total", + "pending", + "reassigned", + "skipped", + "no_candidate", + "failed", + "cancelled" + ] + }, + "occurred_at": { + "type": "string" + } + }, + "required": [ + "reassignment_id", + "status", + "out_of_office_id", + "counts", + "occurred_at" + ] + } + }, + "required": [ + "id", + "object", + "api_version", + "created_at", + "type", + "data" + ], + "examples": [ + { + "id": "evt_01kd2m9a0b1c2d3e4f5g6h7j8m", + "object": "event", + "type": "scheduling.reassignment.completed", + "api_version": "2026-10-10", + "created_at": "2027-01-01T10:00:00.000Z", + "data": { + "reassignment_id": "reasn_01kd2m9a0b1c2d3e4f5g6h7j8m", + "status": "completed", + "out_of_office_id": null, + "counts": { + "total": 12, + "pending": 0, + "reassigned": 11, + "skipped": 0, + "no_candidate": 1, + "failed": 0, + "cancelled": 0 + }, + "occurred_at": "2027-01-01T10:00:00.000Z" + } + } + ] + }, "Environment": { "type": "object", "properties": { @@ -9729,6 +9964,7 @@ "scheduling.booking.external_conflict", "scheduling.booking.calendar_write_failed", "scheduling.booking.location_ready", + "scheduling.booking.reassigned", "scheduling.booking.started", "scheduling.booking.ended", "scheduling.booking.reminder", @@ -9736,10 +9972,16 @@ "scheduling.calendar.needs_reauth", "scheduling.calendar.disconnected", "scheduling.calendar_delegation.failing", - "scheduling.calendar_delegation.restored" + "scheduling.calendar_delegation.restored", + "scheduling.out_of_office.created", + "scheduling.out_of_office.updated", + "scheduling.out_of_office.deleted", + "scheduling.out_of_office.started", + "scheduling.out_of_office.ended", + "scheduling.reassignment.completed" ] }, - "maxItems": 91, + "maxItems": 98, "description": "Event types delivered to this endpoint. An empty array means every event type, including ones added later. Defaults to every event type." }, "api_version": { @@ -10052,6 +10294,7 @@ "scheduling.booking.external_conflict", "scheduling.booking.calendar_write_failed", "scheduling.booking.location_ready", + "scheduling.booking.reassigned", "scheduling.booking.started", "scheduling.booking.ended", "scheduling.booking.reminder", @@ -10059,10 +10302,16 @@ "scheduling.calendar.needs_reauth", "scheduling.calendar.disconnected", "scheduling.calendar_delegation.failing", - "scheduling.calendar_delegation.restored" + "scheduling.calendar_delegation.restored", + "scheduling.out_of_office.created", + "scheduling.out_of_office.updated", + "scheduling.out_of_office.deleted", + "scheduling.out_of_office.started", + "scheduling.out_of_office.ended", + "scheduling.reassignment.completed" ] }, - "maxItems": 91, + "maxItems": 98, "description": "Event types delivered to this endpoint. An empty array means every event type, including ones added later." }, "api_version": { @@ -10628,6 +10877,7 @@ "scheduling.booking.external_conflict", "scheduling.booking.calendar_write_failed", "scheduling.booking.location_ready", + "scheduling.booking.reassigned", "scheduling.booking.started", "scheduling.booking.ended", "scheduling.booking.reminder", @@ -10635,10 +10885,16 @@ "scheduling.calendar.needs_reauth", "scheduling.calendar.disconnected", "scheduling.calendar_delegation.failing", - "scheduling.calendar_delegation.restored" + "scheduling.calendar_delegation.restored", + "scheduling.out_of_office.created", + "scheduling.out_of_office.updated", + "scheduling.out_of_office.deleted", + "scheduling.out_of_office.started", + "scheduling.out_of_office.ended", + "scheduling.reassignment.completed" ] }, - "maxItems": 91, + "maxItems": 98, "description": "Event types to receive. Defaults to every event type." }, "api_version": { @@ -10969,7 +11225,10 @@ "$ref": "#/components/schemas/SchedulingCalendarDelegationWebhookEvent" }, { - "$ref": "#/components/schemas/SchedulingCalendarDelegationWebhookEvent" + "$ref": "#/components/schemas/SchedulingOutOfOfficeWebhookEvent" + }, + { + "$ref": "#/components/schemas/SchedulingReassignmentWebhookEvent" } ], "description": "The event, as an endpoint on the request's API version receives it. Its `webhook-id` header is `event.id`." @@ -11431,6 +11690,7 @@ "scheduling.booking.external_conflict", "scheduling.booking.calendar_write_failed", "scheduling.booking.location_ready", + "scheduling.booking.reassigned", "scheduling.booking.started", "scheduling.booking.ended", "scheduling.booking.reminder", @@ -11438,7 +11698,13 @@ "scheduling.calendar.needs_reauth", "scheduling.calendar.disconnected", "scheduling.calendar_delegation.failing", - "scheduling.calendar_delegation.restored" + "scheduling.calendar_delegation.restored", + "scheduling.out_of_office.created", + "scheduling.out_of_office.updated", + "scheduling.out_of_office.deleted", + "scheduling.out_of_office.started", + "scheduling.out_of_office.ended", + "scheduling.reassignment.completed" ], "description": "The event type. `domain.*`, `account.*`, `subaccount.*` and `billing.*` events only reach live environments, so they can't be tested in a sandbox one." }, @@ -18541,100 +18807,231 @@ } ] }, - "SchedulingSchedule": { + "SchedulingBooking": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "scheduling.schedule" + "scheduling.booking" ] }, "id": { "type": "string" }, - "host": { + "event_type": { "type": "string", - "description": "The id of the host this schedule belongs to." + "description": "The id of the booked event type." }, - "name": { + "status": { "type": "string", - "description": "A label for the schedule. `Default` when none was given." + "enum": [ + "held", + "pending_approval", + "confirmed", + "cancelled", + "declined" + ], + "description": "`held`: a hold keeping the slot until `hold_expires_at`, uncharged and known to nobody. `pending_approval`: holds the time until approved, declined or `approval_expires_at`. `confirmed`: booked. `cancelled`: cancelled, or a hold released or expired. `declined`: refused, or its approval deadline passed." }, - "time_zone": { + "title": { "type": "string", - "description": "The IANA time zone its rules and overrides are local to." + "description": "The event type's title when this was booked." }, - "is_default": { - "type": "boolean", - "description": "Whether this is the host's default schedule, used by event types that don't name one. A host's first schedule always is." + "start": { + "type": "string", + "description": "When the booking starts." }, - "rules": { + "end": { + "type": "string", + "description": "When the booking ends, without buffers." + }, + "hosts": { "type": "array", "items": { - "type": "object", - "properties": { - "days": { - "type": "array", - "items": { - "type": "number" - }, - "description": "Weekdays, with Sunday as 0." - }, - "start": { - "type": "string", - "description": "Local start time, `HH:MM`." - }, - "end": { - "type": "string", - "description": "Local end time, `HH:MM`. `24:00` is the end of the day." - } - }, - "required": [ - "days", - "start", - "end" - ] + "type": "string" }, - "description": "Weekly hours in local time, unchanged across daylight saving." + "description": "The ids of the hosts taking it." }, - "overrides": { + "attendees": { "type": "array", "items": { "type": "object", "properties": { - "date": { + "id": { + "type": "string" + }, + "role": { "type": "string", - "description": "The local date, `YYYY-MM-DD`." + "enum": [ + "attendee", + "guest" + ], + "description": "`attendee` booked it and comes first; a `guest` was added by them." }, - "ranges": { - "type": "array", - "items": { - "type": "object", - "properties": { - "start": { - "type": "string", - "description": "Local start time, `HH:MM`." + "uid": { + "type": [ + "string", + "null" + ], + "description": "Your own id for this person. `null` when none was given." + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "`null` when not given, or after their details are erased." + }, + "email": { + "type": [ + "string", + "null" + ], + "description": "`null` when not given, or after their details are erased." + }, + "phone": { + "type": [ + "string", + "null" + ], + "description": "`null` when not given, or after their details are erased." + }, + "time_zone": { + "type": "string", + "description": "Their IANA time zone." + }, + "locale": { + "type": [ + "string", + "null" + ], + "description": "Their locale. `null` when not given." + }, + "answers": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" }, - "end": { - "type": "string", - "description": "Local end time, `HH:MM`. `24:00` is the end of the day." + { + "type": "array", + "items": { + "type": "string" + } } - }, - "required": [ - "start", - "end" ] }, - "description": "That day's hours. Empty means unavailable all day." + "description": "Only when retrieving one booking." } }, "required": [ - "date", - "ranges" + "id", + "role", + "uid", + "name", + "email", + "phone", + "time_zone", + "locale" ] }, - "description": "Dates whose hours replace the weekly rules." + "description": "Who's booked: the attendee first, then any guests they added. A group session's seat is its own booking." + }, + "location": { + "$ref": "#/components/schemas/SchedulingLocation" + }, + "hold_expires_at": { + "type": [ + "string", + "null" + ], + "description": "When a `held` booking gives up its slot. `null` otherwise." + }, + "approval_expires_at": { + "type": [ + "string", + "null" + ], + "description": "When a `pending_approval` booking is declined if nobody decides. `null` otherwise." + }, + "series": { + "type": [ + "string", + "null" + ], + "description": "The id of the series it belongs to. `null`: not part of one." + }, + "series_index": { + "type": [ + "number", + "null" + ], + "description": "Its place in the series, from 0. `null`: not part of one." + }, + "group_session": { + "type": [ + "string", + "null" + ], + "description": "The id of the group session whose seat this is. `null`: not a seat." + }, + "calendar_unverified": { + "type": "boolean", + "description": "Reserved for flagging a booking made on last known calendar data. Nothing sets it yet, so it's always `false`." + }, + "external_conflict_at": { + "type": [ + "string", + "null" + ], + "description": "When something else landed on this time in the host's calendar, or the host changed the event. The booking stands. `null`: no conflict known." + }, + "no_show_at": { + "type": [ + "string", + "null" + ], + "description": "When the attendee was marked as a no-show. `null`: not marked." + }, + "cancelled_at": { + "type": [ + "string", + "null" + ], + "description": "When it was cancelled. `null`: not cancelled." + }, + "cancelled_by": { + "type": [ + "string", + "null" + ], + "enum": [ + "api_key", + "user", + "host_session", + "booker_session", + "manage_session", + "manager_session", + "system", + null + ], + "description": "Who cancelled it: `api_key`, `user` (the dashboard), `host_session`, `booker_session`, `manage_session` (the attendee's link) or `system`. `null`: not cancelled, or cancelled before this was recorded." + }, + "cancellation_reason": { + "type": [ + "string", + "null" + ], + "description": "Why, if they said. Only when retrieving one booking. It's erased with the attendee's details, and it's never in webhooks, logs or emails." + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Your own key-value pairs." }, "created_at": { "type": "string" @@ -18646,191 +19043,300 @@ "required": [ "object", "id", - "host", - "name", - "time_zone", - "is_default", - "rules", - "overrides", + "event_type", + "status", + "title", + "start", + "end", + "hosts", + "attendees", + "location", + "hold_expires_at", + "approval_expires_at", + "series", + "series_index", + "group_session", + "calendar_unverified", + "external_conflict_at", + "no_show_at", + "cancelled_at", + "cancelled_by", + "metadata", "created_at", "updated_at" ], "examples": [ { - "object": "scheduling.schedule", - "id": "sched_01kd2m9a0b1c2d3e4f5g6h7j8m", - "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", - "name": "Clinic hours", - "time_zone": "America/New_York", - "is_default": true, - "rules": [ - { - "days": [ - 1, - 2, - 3, - 4, - 5 - ], - "start": "09:00", - "end": "17:00" - } + "object": "scheduling.booking", + "id": "book_01kd2m9a0b1c2d3e4f5g6h7j8m", + "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "status": "confirmed", + "title": "Initial consultation", + "start": "2027-01-04T15:00:00.000Z", + "end": "2027-01-04T15:30:00.000Z", + "hosts": [ + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ], - "overrides": [ + "attendees": [ { - "date": "2027-12-24", - "ranges": [] + "id": "attn_01kd2m9a0b1c2d3e4f5g6h7j8m", + "role": "attendee", + "uid": "patient_77", + "name": "Sam Lee", + "email": "sam@example.com", + "phone": null, + "time_zone": "UTC", + "locale": null } ], + "location": null, + "hold_expires_at": null, + "approval_expires_at": null, + "series": null, + "series_index": null, + "group_session": null, + "calendar_unverified": false, + "external_conflict_at": null, + "no_show_at": null, + "cancelled_at": null, + "cancelled_by": null, + "metadata": {}, "created_at": "2027-01-01T10:00:00.000Z", "updated_at": "2027-01-01T10:00:00.000Z" } ] }, - "UpsertScheduleRequest": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100, - "description": "A label for the schedule. Default on creation: `Default`." - }, - "time_zone": { - "type": "string", - "minLength": 1, - "maxLength": 64, - "description": "Default: the host's.", - "examples": [ - "America/New_York" - ] - }, - "is_default": { - "type": "boolean", - "description": "`true` makes this the host's default schedule, in place of the current one. A host's first schedule is their default anyway." - }, - "rules": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SchedulingWeeklyRule" + "SchedulingLocation": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "link" + ] + }, + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "format": "uri", + "default": null, + "description": "The meeting link. `null` on an event type means the booking supplies its own." + } }, - "maxItems": 50, - "description": "Weekly hours in local time. A range can't cross midnight: use two." + "required": [ + "type" + ], + "additionalProperties": false }, - "overrides": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SchedulingScheduleOverride" + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone" + ] + }, + "number": { + "type": [ + "string", + "null" + ], + "maxLength": 32, + "default": null, + "description": "The phone number to call. `null` on an event type means the booking supplies its own." + } }, - "maxItems": 366, - "description": "Replaces the schedule's overrides when given." - } - }, - "required": [ - "rules" - ], - "additionalProperties": false, - "examples": [ + "required": [ + "type" + ], + "additionalProperties": false + }, { - "name": "Clinic hours", - "rules": [ - { - "days": [ - 1, - 2, - 3, - 4, - 5 + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "in_person" + ] + }, + "address": { + "type": [ + "string", + "null" ], - "start": "09:00", - "end": "17:00" + "maxLength": 500, + "default": null, + "description": "The address to meet at. `null` on an event type means the booking supplies its own." } + }, + "required": [ + "type" ], - "overrides": [ - { - "date": "2027-12-24", - "ranges": [] + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "google_meet" + ] } - ] - } - ] - }, - "SchedulingWeeklyRule": { - "type": "object", - "properties": { - "days": { - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "maximum": 6 }, - "minItems": 1, - "maxItems": 7, - "description": "Weekdays, with Sunday as 0." + "required": [ + "type" + ], + "additionalProperties": false }, - "start": { - "type": "string", - "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$|^24:00$", - "description": "A local time, `HH:MM`. `24:00` is the end of the day." + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "microsoft_teams" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false }, - "end": { - "type": "string", - "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$|^24:00$", - "description": "A local time, `HH:MM`. `24:00` is the end of the day." + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "zoom" + ] + }, + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "format": "uri", + "default": null, + "description": "The meeting's link, once it's created on the host's Zoom account." + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "null" } - }, - "required": [ - "days", - "start", - "end" ], - "additionalProperties": false + "description": "Where a booking meets. `link`, `phone` and `in_person` carry their details, and on an event type, left blank, use the booked host's own if they have one; `google_meet`, `microsoft_teams` and `zoom` get a meeting made on the host's connected account." }, - "SchedulingScheduleOverride": { + "ReassignSchedulingBookingRequest": { "type": "object", "properties": { - "date": { - "type": "string", - "pattern": "^\\d{4}-\\d{2}-\\d{2}$", - "description": "A local date in the schedule's time zone.", - "examples": [ - "2027-12-24" - ] - }, - "ranges": { + "changes": { "type": "array", "items": { "type": "object", "properties": { - "start": { + "from_host": { "type": "string", - "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$|^24:00$", - "description": "A local time, `HH:MM`. `24:00` is the end of the day." + "description": "The host leaving, by id or uid. Needed when it could be several." }, - "end": { + "to_host": { "type": "string", - "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$|^24:00$", - "description": "A local time, `HH:MM`. `24:00` is the end of the day." + "description": "The host taking over, by id or uid. Or set `auto`." + }, + "auto": { + "type": "boolean", + "description": "Let coss.com pick, as a pool would: the fairest who's free and fits what the booking was routed with." + }, + "preferred_hosts": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 20, + "description": "With `auto`: try these first, in order." } }, - "required": [ - "start", - "end" - ], "additionalProperties": false }, + "minItems": 1, "maxItems": 10, - "description": "Replaces the weekly rules that day. Empty means unavailable all day." + "description": "A session or a one-on-one has one. A collective can change several." + }, + "reason_code": { + "type": "string", + "enum": [ + "host_unavailable", + "out_of_office", + "workload", + "customer_request", + "offboarding", + "other" + ] + }, + "note": { + "type": "string", + "maxLength": 500, + "description": "Why, in your words: required with an override of someone's availability. Encrypted, and only in the booking's history." + }, + "overrides": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "any_host", + "out_of_office", + "outside_availability", + "host_busy", + "limit_reached", + "calendar_unverified" + ] + }, + "maxItems": 6, + "description": "Go ahead anyway: `any_host` for someone who doesn't take this event type, and the rest for what a candidate's `reason` said. A host who already has something at this time can never be overridden." + }, + "notify": { + "type": "object", + "properties": { + "attendee": { + "type": "boolean", + "description": "Tell the attendee. Default: true." + } + }, + "additionalProperties": false } }, "required": [ - "date", - "ranges" + "changes", + "reason_code" ], - "additionalProperties": false + "additionalProperties": false, + "examples": [ + { + "changes": [ + { + "to_host": "doctor_518" + } + ], + "reason_code": "out_of_office" + } + ] }, - "SchedulingScheduleList": { + "SchedulingReassignCandidateList": { "type": "object", "properties": { "object": { @@ -18842,7 +19348,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingSchedule" + "$ref": "#/components/schemas/SchedulingReassignCandidate" } }, "has_more": { @@ -18859,1024 +19365,841 @@ "object": "list", "data": [ { - "object": "scheduling.schedule", - "id": "sched_01kd2m9a0b1c2d3e4f5g6h7j8m", + "object": "scheduling.reassign_candidate", "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", - "name": "Clinic hours", - "time_zone": "America/New_York", - "is_default": true, - "rules": [ - { - "days": [ - 1, - 2, - 3, - 4, - 5 - ], - "start": "09:00", - "end": "17:00" - } - ], - "overrides": [ - { - "date": "2027-12-24", - "ranges": [] - } - ], - "created_at": "2027-01-01T10:00:00.000Z", - "updated_at": "2027-01-01T10:00:00.000Z" + "uid": "doctor_518", + "name": "Dr. Priya Raman", + "member": true, + "available": true, + "reason": null, + "reasons": [], + "load": 4, + "rank": 1, + "meeting_provider_missing": false } ], "has_more": false } ] }, - "DeletedSchedulingSchedule": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "scheduling.schedule" - ] - }, - "id": { - "type": "string" - }, - "deleted": { - "type": "boolean", - "enum": [ - true - ] - } - }, - "required": [ - "object", - "id", - "deleted" - ], - "examples": [ - { - "object": "scheduling.schedule", - "id": "sched_01kd2m9a0b1c2d3e4f5g6h7j8m", - "deleted": true - } - ] - }, - "SchedulingEventType": { + "SchedulingReassignCandidate": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "scheduling.event_type" + "scheduling.reassign_candidate" ] }, - "id": { + "host": { "type": "string" }, "uid": { "type": [ "string", "null" - ], - "description": "Your own id for this event type, accepted wherever its id is. `null` when none is set." - }, - "description": { - "type": [ - "string", - "null" - ], - "description": "Plain text shown on hosted booking pages. `null` when there's none." - }, - "title": { - "type": "string", - "description": "Shown to attendees and in emails. Host calendars show it only with `host_calendar_details: full`." - }, - "kind": { - "type": "string", - "enum": [ - "one_on_one", - "pool", - "collective", - "group" - ], - "description": "Who a booking needs. `one_on_one`: one host. `pool`: any one of its hosts. `collective`: every `required` host, plus at most one `member`. `group`: one host, with attendees taking seats up to `capacity`." - }, - "status": { - "type": "string", - "enum": [ - "active", - "archived" - ], - "description": "`active`: can be booked. `archived`: new bookings answer `not_found`; existing ones stay." + ] }, - "duration_minutes": { - "type": "number", - "description": "How long a booking lasts by default, in minutes." + "name": { + "type": "string" }, - "duration_options": { - "type": "array", - "items": { - "type": "number" - }, - "description": "Other lengths an attendee may choose, in minutes. Empty: only `duration_minutes`." + "member": { + "type": "boolean", + "description": "Takes this event type, and fits what the booking was routed with. Otherwise it needs `any_host`." }, - "step_minutes": { - "type": "number", - "description": "How far apart slots start, in minutes." + "available": { + "type": "boolean" }, - "grid_time_zone": { + "reason": { "type": [ "string", "null" ], - "description": "The zone slots align in. `null`: each host's own." + "description": "Why they aren't available, the first of `reasons`." }, - "buffer_before_minutes": { - "type": "number", - "description": "Minutes kept free before each booking." - }, - "buffer_after_minutes": { - "type": "number", - "description": "Minutes kept free after each booking." - }, - "min_notice_minutes": { - "type": "number", - "description": "Slots starting sooner than this many minutes from now aren't offered." - }, - "booking_window": { - "$ref": "#/components/schemas/SchedulingBookingWindow" - }, - "limits": { + "reasons": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingLimit" + "type": "string" }, - "description": "Caps on bookings per day or week." - }, - "capacity": { - "type": [ - "number", - "null" - ], - "description": "Attendees per session of a `group` event type. `null` for every other kind." - }, - "requires_approval": { - "type": "boolean", - "description": "Whether bookings start as `pending_approval`, holding the time until approved or declined." - }, - "approval_ttl_minutes": { - "type": "number", - "description": "How long a booking can wait for approval before it's declined, in minutes." - }, - "hold_ttl_seconds": { - "type": "number", - "description": "How long a hold keeps its slot, in seconds." - }, - "cancel_notice_minutes": { - "type": "number", - "description": "Attendees can't cancel closer to the start than this. API keys can." - }, - "reschedule_notice_minutes": { - "type": "number", - "description": "Attendees can't reschedule closer to the start than this. API keys can." + "description": "Everything in the way, each needing its override. `already_claimed` can't be overridden." }, - "max_guests": { + "load": { "type": "number", - "description": "How many people an attendee can add to a booking. Not on `group`." + "description": "Their live bookings in the week from this one." }, - "max_active_bookings_per_attendee": { + "rank": { "type": [ "number", "null" ], - "description": "How many upcoming bookings of this event type one person can have at a time. `null`: no limit." + "description": "Their place in who a pool would pick next, from 1. Null for a non-member." }, - "host_calendar_details": { + "meeting_provider_missing": { + "type": "boolean", + "description": "They haven't connected what this booking's meeting needs, so it'd have none." + } + }, + "required": [ + "object", + "host", + "uid", + "name", + "member", + "available", + "reason", + "reasons", + "load", + "rank", + "meeting_provider_missing" + ], + "examples": [ + { + "object": "scheduling.reassign_candidate", + "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "uid": "doctor_518", + "name": "Dr. Priya Raman", + "member": true, + "available": true, + "reason": null, + "reasons": [], + "load": 4, + "rank": 1, + "meeting_provider_missing": false + } + ] + }, + "SchedulingReassignmentPreview": { + "type": "object", + "properties": { + "object": { "type": "string", "enum": [ - "neutral", - "full" - ], - "description": "What hosts' calendars say about a booking. `neutral`: a private event titled \"Appointment\". `full`: a private event with this title and the attendee's and guests' details." - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SchedulingQuestion" - }, - "description": "What attendees are asked when booking." + "scheduling.reassignment_preview" + ] }, - "locations": { + "items": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingLocation" - }, - "description": "Where bookings can meet. A browser only chooses among these." - }, - "host_filter": { - "type": [ - "object", - "null" - ], - "additionalProperties": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "description": "Pool members also include active hosts matching all of these attributes. `null`: only `hosts`." - }, - "notifications": { - "type": [ - "object", - "null" - ], - "additionalProperties": { "type": "object", "properties": { - "email_attendee": { - "type": "boolean" + "booking": { + "type": "string" }, - "email_host": { - "type": "boolean" + "event_type": { + "type": "string" }, - "sms_attendee": { - "type": "boolean" - } - } - }, - "description": "Per event, overriding your settings for this event type. `null`: only those." - }, - "reminders": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/SchedulingReminder" - }, - "description": "What goes out around a booking, in place of your settings' list. `null` inherits it; an empty list means none." - }, - "hosts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "host": { - "type": "string", - "description": "The host's id." + "title": { + "type": "string" }, - "role": { + "status": { "type": "string", "enum": [ - "required", - "member" - ], - "description": "`required` hosts must all be free; one `member` is chosen for a booking." + "held", + "pending_approval", + "confirmed", + "cancelled", + "declined" + ] }, - "weight": { - "type": "number", - "description": "Share of a pool's bookings relative to other members." + "start": { + "type": "string" }, - "schedule": { + "end": { + "type": "string" + }, + "series": { + "type": [ + "string", + "null" + ] + }, + "group_session": { + "type": [ + "string", + "null" + ] + }, + "from_host": { + "type": [ + "string", + "null" + ] + }, + "to_host": { "type": [ "string", "null" ], - "description": "The id of the schedule used for this event type. `null`: the host's default schedule." + "description": "Null when nobody could take it." + }, + "reason": { + "type": [ + "string", + "null" + ], + "description": "Why it would stay where it is." } }, "required": [ - "host", - "role", - "weight", - "schedule" + "booking", + "event_type", + "title", + "status", + "start", + "end", + "series", + "group_session", + "from_host", + "to_host", + "reason" ] - }, - "description": "The hosts assigned by name." - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Your own key-value pairs." - }, - "created_at": { - "type": "string" + } }, - "updated_at": { - "type": "string" + "truncated": { + "type": "boolean", + "description": "There are more than a preview covers." } }, "required": [ "object", - "id", - "uid", - "description", - "title", - "kind", - "status", - "duration_minutes", - "duration_options", - "step_minutes", - "grid_time_zone", - "buffer_before_minutes", - "buffer_after_minutes", - "min_notice_minutes", - "booking_window", - "limits", - "capacity", - "requires_approval", - "approval_ttl_minutes", - "hold_ttl_seconds", - "cancel_notice_minutes", - "reschedule_notice_minutes", - "max_guests", - "max_active_bookings_per_attendee", - "host_calendar_details", - "questions", - "locations", - "host_filter", - "notifications", - "reminders", - "hosts", - "metadata", - "created_at", - "updated_at" + "items", + "truncated" ], "examples": [ { - "object": "scheduling.event_type", - "id": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", - "uid": null, - "title": "Initial consultation", - "description": null, - "kind": "one_on_one", - "status": "active", - "duration_minutes": 30, - "duration_options": [], - "step_minutes": 30, - "grid_time_zone": null, - "buffer_before_minutes": 0, - "buffer_after_minutes": 10, - "min_notice_minutes": 120, - "booking_window": null, - "limits": [], - "capacity": null, - "requires_approval": false, - "approval_ttl_minutes": 1440, - "hold_ttl_seconds": 600, - "cancel_notice_minutes": 0, - "reschedule_notice_minutes": 0, - "max_guests": 0, - "max_active_bookings_per_attendee": null, - "host_calendar_details": "neutral", - "questions": [], - "locations": [], - "host_filter": null, - "notifications": null, - "reminders": null, - "hosts": [ + "object": "scheduling.reassignment_preview", + "items": [ { - "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", - "role": "member", - "weight": 100, - "schedule": null + "booking": "book_01kd2m9a0b1c2d3e4f5g6h7j8m", + "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "title": "Consultation", + "status": "confirmed", + "start": "2027-08-03T15:00:00.000Z", + "end": "2027-08-03T15:30:00.000Z", + "series": null, + "group_session": null, + "from_host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "to_host": "host_01kd2m9a0b1c2d3e4f5g6h7j8n", + "reason": null } ], - "metadata": {}, - "created_at": "2027-01-01T10:00:00.000Z", - "updated_at": "2027-01-01T10:00:00.000Z" + "truncated": false } ] }, - "SchedulingBookingWindow": { - "oneOf": [ - { - "type": "object", - "properties": { - "kind": { - "type": "string", - "enum": [ - "rolling" - ] - }, - "days": { - "type": "integer", - "minimum": 1, - "maximum": 730, - "description": "Days ahead of now that slots are offered." - } - }, - "required": [ - "kind", - "days" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "kind": { - "type": "string", - "enum": [ - "range" - ] - }, - "from": { - "type": "string", - "pattern": "^\\d{4}-\\d{2}-\\d{2}$", - "description": "The first local date offered, `YYYY-MM-DD`.", - "examples": [ - "2027-12-24" - ] - }, - "to": { - "type": "string", - "pattern": "^\\d{4}-\\d{2}-\\d{2}$", - "description": "The last local date offered, `YYYY-MM-DD`.", - "examples": [ - "2027-12-24" - ] - } - }, - "required": [ - "kind", - "from", - "to" - ], - "additionalProperties": false - }, - { - "type": "null" - } - ], - "description": "How far ahead a slot can be booked: `rolling` for a number of days from now, or `range` for fixed dates. On an event type, `null` means no window of its own." - }, - "SchedulingLimit": { + "PreviewSchedulingReassignmentRequest": { "type": "object", "properties": { - "period": { - "type": "string", - "enum": [ - "day", - "week" - ], - "description": "In the host's time zone." + "selection": { + "$ref": "#/components/schemas/SchedulingReassignmentSelection" }, - "max": { - "type": "integer", - "minimum": 1, - "maximum": 1000, - "description": "The most bookings allowed in one period." + "strategy": { + "$ref": "#/components/schemas/SchedulingReassignmentStrategy" }, - "scope": { - "type": "string", - "enum": [ - "event_type", - "host" - ], - "default": "event_type", - "description": "Count this event type's bookings, or all of the host's." + "overrides": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "any_host", + "out_of_office", + "outside_availability", + "host_busy", + "limit_reached", + "calendar_unverified" + ] + }, + "maxItems": 6 } }, "required": [ - "period", - "max" + "selection" ], "additionalProperties": false, - "description": "A cap on bookings in a day or a week." + "examples": [ + { + "selection": { + "host": "doctor_482", + "start_before": "2027-08-14T00:00:00.000Z" + }, + "strategy": { + "preferred_hosts": [ + "doctor_518" + ] + } + } + ] }, - "SchedulingQuestion": { + "SchedulingReassignmentSelection": { "type": "object", "properties": { - "key": { + "host": { "type": "string", - "pattern": "^[a-z][a-z0-9_]{0,63}$", - "description": "The answer's key in `answers`. Unique within the event type." + "description": "This host's bookings, by id or uid." }, - "type": { + "out_of_office": { "type": "string", - "enum": [ - "text", - "textarea", - "select", - "radio", - "checkbox", - "phone", - "email", - "number" - ], - "description": "How it's asked and which answers are valid." + "description": "The bookings that fall in this entry." }, - "label": { + "booking_ids": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 5000 + }, + "series": { "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "The question as attendees see it." + "description": "A series' occurrences still to come." }, - "required": { - "type": "boolean", - "default": false, - "description": "Whether a booking must answer it." + "start_after": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] }, - "options": { + "start_before": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + }, + "event_types": { "type": "array", "items": { - "type": "string", - "minLength": 1, - "maxLength": 200 + "type": "string" }, - "maxItems": 50, - "default": [], - "description": "The choices of a `select`, `radio` or `checkbox` question. `select` and `radio` need at least one." + "maxItems": 100 + }, + "external_conflict": { + "type": "boolean", + "description": "Only those with something else on the host's calendar at that time." } }, - "required": [ - "key", - "type", - "label" - ], - "additionalProperties": false, - "description": "Something attendees are asked when booking. Answers are encrypted, and come back only when you retrieve one booking." + "additionalProperties": false }, - "SchedulingLocation": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "link" - ] - }, - "url": { - "type": [ - "string", - "null" - ], - "maxLength": 2000, - "format": "uri", - "default": null, - "description": "The meeting link. `null` on an event type means the booking supplies its own." - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "phone" - ] - }, - "number": { - "type": [ - "string", - "null" - ], - "maxLength": 32, - "default": null, - "description": "The phone number to call. `null` on an event type means the booking supplies its own." - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "in_person" - ] - }, - "address": { - "type": [ - "string", - "null" - ], - "maxLength": 500, - "default": null, - "description": "The address to meet at. `null` on an event type means the booking supplies its own." - } - }, - "required": [ - "type" - ], - "additionalProperties": false + "SchedulingReassignmentStrategy": { + "type": "object", + "properties": { + "to_host": { + "type": "string", + "description": "Give them all to this host." }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "google_meet" - ] - } + "preferred_hosts": { + "type": "array", + "items": { + "type": "string" }, - "required": [ - "type" - ], - "additionalProperties": false + "maxItems": 20, + "description": "Try these first, in order." }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "microsoft_teams" - ] - } - }, - "required": [ - "type" + "otherwise": { + "type": "string", + "enum": [ + "auto", + "leave" ], - "additionalProperties": false + "description": "After `preferred_hosts`: `auto` picks the fairest who's free, `leave` keeps it where it is." }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "zoom" - ] + "assignments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "booking": { + "type": "string" + }, + "to_host": { + "type": "string" + }, + "from_host": { + "type": "string" + } }, - "url": { - "type": [ - "string", - "null" - ], - "maxLength": 2000, - "format": "uri", - "default": null, - "description": "The meeting's link, once it's created on the host's Zoom account." - } + "required": [ + "booking", + "to_host" + ], + "additionalProperties": false }, - "required": [ - "type" - ], - "additionalProperties": false + "maxItems": 5000, + "description": "Who each goes to: a preview, with your changes." } - ], - "description": "Where a booking meets. `link`, `phone` and `in_person` carry their details, and on an event type, left blank, use the booked host's own if they have one; `google_meet`, `microsoft_teams` and `zoom` get a meeting made on the host's connected account." + }, + "default": {}, + "additionalProperties": false }, - "SchedulingReminder": { + "SchedulingReassignment": { "type": "object", "properties": { - "key": { + "object": { "type": "string", - "description": "What webhooks and the booking call this reminder." + "enum": [ + "scheduling.reassignment" + ] }, - "anchor": { + "id": { + "type": "string" + }, + "status": { "type": "string", "enum": [ - "start", - "end", - "confirmed", - "rescheduled", - "cancelled", - "declined", - "pending_approval", - "no_show" - ], - "description": "The moment this is timed from." + "pending", + "running", + "completed", + "cancelled" + ] }, - "before_minutes": { + "reason_code": { + "type": "string", + "enum": [ + "host_unavailable", + "out_of_office", + "workload", + "customer_request", + "offboarding", + "other" + ] + }, + "out_of_office": { "type": [ - "number", + "string", "null" - ], - "description": "How long before the anchor. `null` when it's after." + ] }, - "after_minutes": { + "undo_of": { "type": [ - "number", + "string", "null" - ], - "description": "How long after the anchor. `null` when it's before." + ] }, - "send": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "email_attendee", - "email_host", - "sms_attendee", - "sms_host", - "webhook" - ] - }, - "description": "Who hears about it. `webhook` fires `scheduling.booking.reminder`." - }, - "email_to": { - "type": "array", - "items": { - "type": "string" + "counts": { + "type": "object", + "properties": { + "total": { + "type": "number" + }, + "pending": { + "type": "number" + }, + "reassigned": { + "type": "number" + }, + "skipped": { + "type": "number", + "description": "They'd moved on since it was selected." + }, + "no_candidate": { + "type": "number", + "description": "Nobody could take them: they stayed." + }, + "failed": { + "type": "number" + }, + "cancelled": { + "type": "number" + } }, - "description": "Fixed addresses that are emailed as well." + "required": [ + "total", + "pending", + "reassigned", + "skipped", + "no_candidate", + "failed", + "cancelled" + ] }, - "subject": { - "type": [ - "string", - "null" - ], - "description": "Your own subject. `null`: the standard one." + "created_at": { + "type": "string" }, - "message": { + "started_at": { "type": [ "string", "null" - ], - "description": "Your own words in the email. `null`: the standard ones." + ] }, - "sms_message": { + "finished_at": { "type": [ "string", "null" - ], - "description": "Your own text. `null`: the standard one." - }, - "button": { - "type": [ - "object", - "null" - ], - "properties": { - "label": { - "type": "string", - "description": "What the button says." - }, - "url": { - "type": "string", - "description": "Where it goes." - } + ] + } + }, + "required": [ + "object", + "id", + "status", + "reason_code", + "out_of_office", + "undo_of", + "counts", + "created_at", + "started_at", + "finished_at" + ], + "examples": [ + { + "object": "scheduling.reassignment", + "id": "reasn_01kd2m9a0b1c2d3e4f5g6h7j8m", + "status": "completed", + "reason_code": "out_of_office", + "out_of_office": "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m", + "undo_of": null, + "counts": { + "total": 12, + "pending": 0, + "reassigned": 11, + "skipped": 0, + "no_candidate": 1, + "failed": 0, + "cancelled": 0 }, - "required": [ - "label", - "url" - ], - "description": "One button in the email. `null`: none." + "created_at": "2027-01-01T10:00:00.000Z", + "started_at": "2027-01-01T10:00:01.000Z", + "finished_at": "2027-01-01T10:00:09.000Z" + } + ] + }, + "CreateSchedulingReassignmentRequest": { + "type": "object", + "properties": { + "selection": { + "$ref": "#/components/schemas/SchedulingReassignmentSelection" }, - "include_invite": { - "type": "boolean", - "description": "Whether the emails carry the invitation." + "strategy": { + "$ref": "#/components/schemas/SchedulingReassignmentStrategy" }, - "send_if_late": { - "type": "boolean", - "description": "Whether it goes right away if booked after its moment, instead of being skipped." + "reason_code": { + "type": "string", + "enum": [ + "host_unavailable", + "out_of_office", + "workload", + "customer_request", + "offboarding", + "other" + ] }, - "skip_no_show": { - "type": "boolean", - "description": "Whether it's dropped once the booking is marked a no-show." + "note": { + "type": "string", + "maxLength": 500, + "description": "Why, in your words: required with an override of someone's availability." }, - "only_if": { - "type": [ - "object", - "null" - ], + "overrides": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "any_host", + "out_of_office", + "outside_availability", + "host_busy", + "limit_reached", + "calendar_unverified" + ] + }, + "maxItems": 6 + }, + "notify": { + "type": "object", "properties": { - "metadata": { - "type": [ - "object", - "null" - ], - "additionalProperties": { - "type": "string" - }, - "description": "The booking's metadata must have all of these. `null`: any." + "attendees": { + "type": "boolean", + "description": "Tell attendees. Default: true." }, - "location_types": { - "type": [ - "array", - "null" + "hosts": { + "type": "string", + "enum": [ + "digest", + "each", + "none" ], - "items": { - "type": "string" - }, - "description": "The booking's location must be one of these. `null`: any." + "description": "`digest` (the default) sends each host one email for the run." } }, - "required": [ - "metadata", - "location_types" - ], - "description": "Only for bookings matching all of this. `null`: every booking." + "additionalProperties": false } }, "required": [ - "key", - "anchor", - "before_minutes", - "after_minutes", - "send", - "email_to", - "subject", - "message", - "sms_message", - "button", - "include_invite", - "send_if_late", - "skip_no_show", - "only_if" + "selection", + "reason_code" + ], + "additionalProperties": false, + "examples": [ + { + "selection": { + "out_of_office": "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m" + }, + "strategy": { + "preferred_hosts": [ + "doctor_518" + ] + }, + "reason_code": "out_of_office" + } ] }, - "CreateEventTypeRequest": { + "SchedulingReassignmentList": { "type": "object", "properties": { - "uid": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "description": "Your own id for this event type, unique in the environment, accepted wherever its id is." - }, - "kind": { + "object": { "type": "string", "enum": [ - "one_on_one", - "pool", - "collective", - "group" - ], - "default": "one_on_one", - "description": "Who a booking needs. `one_on_one`: one host. `pool`: any one of its hosts. `collective`: every `required` host, plus at most one `member`. `group`: one host, with attendees taking seats up to `capacity`. Fixed once created." + "list" + ] }, - "description": { - "type": [ - "string", - "null" - ], - "maxLength": 2000, - "description": "Plain text about the event type, shown on hosted booking pages and never in emails. Links, and anything shaped like a domain, must be on your organization's verified domains or their subdomains, and use `https`. `null` removes it." + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingReassignment" + } }, - "title": { + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ + { + "object": "scheduling.reassignment", + "id": "reasn_01kd2m9a0b1c2d3e4f5g6h7j8m", + "status": "completed", + "reason_code": "out_of_office", + "out_of_office": "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m", + "undo_of": null, + "counts": { + "total": 12, + "pending": 0, + "reassigned": 11, + "skipped": 0, + "no_candidate": 1, + "failed": 0, + "cancelled": 0 + }, + "created_at": "2027-01-01T10:00:00.000Z", + "started_at": "2027-01-01T10:00:01.000Z", + "finished_at": "2027-01-01T10:00:09.000Z" + } + ], + "has_more": false + } + ] + }, + "SchedulingReassignmentItemList": { + "type": "object", + "properties": { + "object": { "type": "string", - "minLength": 1, - "maxLength": 120, - "description": "Shown to attendees and in emails. Host calendars show it only with `host_calendar_details: full`." - }, - "duration_minutes": { - "type": "integer", - "minimum": 5, - "maximum": 1440, - "description": "How long a booking lasts by default, in minutes." + "enum": [ + "list" + ] }, - "duration_options": { + "data": { "type": "array", "items": { - "type": "integer", - "minimum": 5, - "maximum": 1440 - }, - "maxItems": 10, - "description": "Other lengths an attendee may choose, in minutes. Empty: only `duration_minutes`." + "$ref": "#/components/schemas/SchedulingReassignmentItem" + } }, - "step_minutes": { - "type": "integer", - "minimum": 5, - "maximum": 1440, - "description": "How far apart slots start. Default: the duration." + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ + { + "object": "scheduling.reassignment_item", + "id": "reasi_01kd2m9a0b1c2d3e4f5g6h7j8m", + "booking": "book_01kd2m9a0b1c2d3e4f5g6h7j8m", + "from_host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "to_host": "host_01kd2m9a0b1c2d3e4f5g6h7j8n", + "outcome": "reassigned", + "error_code": null + } + ], + "has_more": false + } + ] + }, + "SchedulingReassignmentItem": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "scheduling.reassignment_item" + ] }, - "grid_time_zone": { + "id": { + "type": "string" + }, + "booking": { + "type": "string" + }, + "from_host": { "type": [ "string", "null" - ], - "minLength": 1, - "maxLength": 64, - "description": "The zone slots align in. Default: each host's own.", - "examples": [ - "America/New_York" ] }, - "buffer_before_minutes": { - "type": "integer", - "minimum": 0, - "maximum": 480, - "description": "Minutes kept free before each booking. Default: 0." + "to_host": { + "type": [ + "string", + "null" + ] }, - "buffer_after_minutes": { - "type": "integer", - "minimum": 0, - "maximum": 480, - "description": "Minutes kept free after each booking. Default: 0." + "outcome": { + "type": "string", + "enum": [ + "pending", + "reassigned", + "skipped_changed", + "no_candidate", + "failed", + "cancelled" + ] }, - "min_notice_minutes": { - "type": "integer", - "minimum": 0, - "maximum": 525600, - "description": "Slots starting sooner than this many minutes from now aren't offered. Default: 0." + "error_code": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "object", + "id", + "booking", + "from_host", + "to_host", + "outcome", + "error_code" + ], + "examples": [ + { + "object": "scheduling.reassignment_item", + "id": "reasi_01kd2m9a0b1c2d3e4f5g6h7j8m", + "booking": "book_01kd2m9a0b1c2d3e4f5g6h7j8m", + "from_host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "to_host": "host_01kd2m9a0b1c2d3e4f5g6h7j8n", + "outcome": "reassigned", + "error_code": null + } + ] + }, + "UndoSchedulingReassignmentRequest": { + "type": "object", + "properties": { + "reason_code": { + "type": "string", + "enum": [ + "host_unavailable", + "out_of_office", + "workload", + "customer_request", + "offboarding", + "other" + ] }, - "booking_window": { - "$ref": "#/components/schemas/SchedulingBookingWindow" + "note": { + "type": "string", + "maxLength": 500 }, - "limits": { + "overrides": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingLimit" + "type": "string", + "enum": [ + "any_host", + "out_of_office", + "outside_availability", + "host_busy", + "limit_reached", + "calendar_unverified" + ] }, - "maxItems": 4, - "description": "Caps on bookings per day or week. Default: none." - }, - "capacity": { - "type": [ - "integer", - "null" - ], - "minimum": 2, - "maximum": 10000, - "description": "Attendees per session. Required for, and only allowed on, `group`." - }, - "requires_approval": { - "type": "boolean", - "description": "Bookings start as `pending_approval`, holding the time until approved or declined. Default: `false`." - }, - "approval_ttl_minutes": { - "type": "integer", - "minimum": 5, - "maximum": 43200, - "description": "How long a booking can wait for approval before it's declined, in minutes. Default: 1440." + "maxItems": 6 }, - "hold_ttl_seconds": { - "type": "integer", - "minimum": 60, - "maximum": 86400, - "description": "How long a hold keeps its slot, in seconds. Default: 600." + "notify": { + "type": "object", + "properties": { + "attendees": { + "type": "boolean", + "description": "Tell attendees. Default: true." + }, + "hosts": { + "type": "string", + "enum": [ + "digest", + "each", + "none" + ], + "description": "`digest` (the default) sends each host one email for the run." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "examples": [ + { + "reason_code": "other" + } + ] + }, + "SchedulingHolidayCalendar": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "scheduling.holiday_calendar" + ] }, - "cancel_notice_minutes": { - "type": "integer", - "minimum": 0, - "maximum": 525600, - "description": "Attendees can't cancel closer to the start than this. API keys can." + "id": { + "type": "string" }, - "reschedule_notice_minutes": { - "type": "integer", - "minimum": 0, - "maximum": 525600, - "description": "Attendees can't reschedule closer to the start than this. API keys can. Default: 0." + "name": { + "type": "string" }, - "max_guests": { - "type": "integer", - "minimum": 0, - "maximum": 10, - "description": "How many people an attendee can add to a booking. Default: 0. Guests are emailed about it. Not on `group`." + "source": { + "type": "string", + "enum": [ + "country", + "ics" + ] }, - "max_active_bookings_per_attendee": { + "country": { "type": [ - "integer", + "string", "null" - ], - "minimum": 1, - "maximum": 1000, - "description": "How many upcoming bookings of this event type one person can have at a time, known by `uid` or email. Default: `null`, no limit. Past this, booking answers `attendee_limit_reached`. Guests don't count, and each occurrence of a series does." + ] }, - "host_calendar_details": { - "type": "string", - "enum": [ - "neutral", - "full" + "hosts": { + "type": [ + "array", + "null" ], - "description": "What hosts' calendars say about a booking. `neutral` (the default): a private event titled \"Appointment\". `full`: a private event with this title and the attendee's name, plus names, emails and phone numbers in its description, which go to the host's calendar provider. Never answers. Refused in HIPAA mode." - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SchedulingQuestion" - }, - "maxItems": 25, - "description": "What attendees are asked when booking. Keys must be unique." - }, - "locations": { - "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingLocation" - }, - "maxItems": 10, - "description": "Where bookings can meet. A browser only chooses among these. A `group` event type can't use `zoom`." + "type": "string" + } }, "host_filter": { "type": [ @@ -19886,309 +20209,386 @@ "additionalProperties": { "anyOf": [ { - "type": "string", - "maxLength": 200 + "type": "string" }, { "type": "array", "items": { - "type": "string", - "maxLength": 200 - }, - "maxItems": 50 + "type": "string" + } } ] - }, - "description": "Pool members also include active hosts matching all of these attributes." - }, - "notifications": { - "type": [ - "object", - "null" - ], - "additionalProperties": { - "type": "object", - "properties": { - "email_attendee": { - "type": "boolean" - }, - "email_host": { - "type": "boolean" - }, - "sms_attendee": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "description": "Per event, overriding your settings for this event type." + } }, - "reminders": { + "regions": { "type": [ "array", "null" ], "items": { - "$ref": "#/components/schemas/SchedulingReminderInput" - }, - "maxItems": 10, - "description": "Replaces your settings' `reminders` for this event type. `null` inherits them; an empty list means none." - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string", - "maxLength": 500 - }, - "description": "Your own key-value pairs." + "type": "string" + } }, - "hosts": { + "delegates": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingEventTypeHostInput" - }, - "maxItems": 500, - "default": [], - "description": "Who can be booked. `one_on_one` and `group` take one; `collective` needs at least one `required` host." + "type": "string" + } + }, + "existing_bookings": { + "type": "string", + "enum": [ + "leave", + "reassign" + ] + }, + "last_read_at": { + "type": [ + "string", + "null" + ] + }, + "last_error": { + "type": [ + "string", + "null" + ], + "description": "Why its feed couldn't be read." + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" } }, "required": [ - "title", - "duration_minutes" + "object", + "id", + "name", + "source", + "country", + "hosts", + "host_filter", + "regions", + "delegates", + "existing_bookings", + "last_read_at", + "last_error", + "created_at", + "updated_at" ], - "additionalProperties": false, "examples": [ { - "title": "Initial consultation", - "kind": "one_on_one", - "duration_minutes": 30, - "buffer_after_minutes": 10, - "min_notice_minutes": 120, - "hosts": [ - { - "host": "doctor_482" - } - ] + "object": "scheduling.holiday_calendar", + "id": "hcal_01kd2m9a0b1c2d3e4f5g6h7j8m", + "name": "UK team", + "source": "country", + "country": "GB", + "hosts": null, + "host_filter": { + "team": "london" + }, + "regions": [ + "England" + ], + "delegates": [], + "existing_bookings": "leave", + "last_read_at": "2027-01-01T10:00:00.000Z", + "last_error": null, + "created_at": "2027-01-01T10:00:00.000Z", + "updated_at": "2027-01-01T10:00:00.000Z" } ] }, - "SchedulingReminderInput": { + "CreateSchedulingHolidayCalendarRequest": { "type": "object", "properties": { - "key": { + "name": { "type": "string", - "pattern": "^[a-z0-9_-]{1,64}$", - "description": "Names this reminder in webhooks and on the booking. Default: from its timing, like `start-1440`." + "minLength": 1, + "maxLength": 200 }, - "anchor": { + "country": { "type": "string", - "enum": [ - "start", - "end", - "confirmed", - "rescheduled", - "cancelled", - "declined", - "pending_approval", - "no_show" - ], - "description": "The moment this is timed from." - }, - "before_minutes": { - "type": [ - "integer", - "null" - ], - "minimum": 0, - "maximum": 525600, - "description": "How long before the anchor. Only for `start` and `end`." + "description": "A code from the list of holiday calendars, such as `GB`." }, - "after_minutes": { - "type": [ - "integer", - "null" - ], - "minimum": 0, - "maximum": 86400, - "description": "How long after the anchor, up to 60 days. Default for other anchors: 0." + "ics_url": { + "type": "string", + "maxLength": 2000, + "format": "uri", + "description": "Your own feed, https or webcal, read in live environments. Stored encrypted." }, - "send": { + "hosts": { "type": "array", "items": { - "type": "string", - "enum": [ - "email_attendee", - "email_host", - "sms_attendee", - "sms_host", - "webhook" + "type": "string" + }, + "maxItems": 1000, + "description": "These hosts, by id or uid." + }, + "host_filter": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } ] }, - "maxItems": 5, - "description": "Who hears about it. `webhook` fires `scheduling.booking.reminder`." + "description": "Everyone matching these attributes, asked again at every sync." }, - "email_to": { + "regions": { "type": "array", "items": { "type": "string", - "maxLength": 320, - "format": "email" + "minLength": 1, + "maxLength": 100 }, - "maxItems": 5, - "description": "Fixed addresses to email as well: verified members of your organization, or addresses on your verified domains (live environments)." + "maxItems": 50, + "description": "Which regional holidays apply, matched against how the calendar names them, such as `England`. Without this, all of them do; empty, only nationwide ones." }, - "subject": { - "type": [ - "string", - "null" - ], - "minLength": 1, - "maxLength": 200, - "description": "Your own subject, with `{{variables}}`. Needs `email_from`." + "delegates": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 10, + "description": "Who can be booked in their place on those days, by id or uid, in order." }, - "message": { - "type": [ - "string", - "null" + "existing_bookings": { + "type": "string", + "enum": [ + "leave", + "reassign" ], - "minLength": 1, - "maxLength": 2000, - "description": "Your own words in place of the standard first line, with `{{variables}}` and no links. The booking's details stay below it. Needs `email_from`." + "description": "What happens to bookings already on a holiday. `leave` (the default) only reports them. `reassign` moves them." + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "examples": [ + { + "name": "UK team", + "country": "GB", + "host_filter": { + "team": "london" + } + } + ] + }, + "SchedulingHolidayCalendarList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] }, - "sms_message": { - "type": [ - "string", - "null" - ], - "minLength": 1, - "maxLength": 320, - "description": "Your own text, with `{{variables}}` other than names, and no links. `Reply STOP to opt out.` is always added." + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingHolidayCalendar" + } }, - "button": { - "type": [ - "object", - "null" - ], - "properties": { - "label": { - "type": "string", - "minLength": 1, - "maxLength": 40 - }, - "url": { - "type": "string", - "minLength": 1, - "maxLength": 500, - "description": "https, on one of your verified domains (live environments). May use `{{booking_id}}` in its path or query.", - "examples": [ - "https://example.com/feedback?booking={{booking_id}}" - ] + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ + { + "object": "scheduling.holiday_calendar", + "id": "hcal_01kd2m9a0b1c2d3e4f5g6h7j8m", + "name": "UK team", + "source": "country", + "country": "GB", + "hosts": null, + "host_filter": { + "team": "london" + }, + "regions": [ + "England" + ], + "delegates": [], + "existing_bookings": "leave", + "last_read_at": "2027-01-01T10:00:00.000Z", + "last_error": null, + "created_at": "2027-01-01T10:00:00.000Z", + "updated_at": "2027-01-01T10:00:00.000Z" } - }, - "required": [ - "label", - "url" ], - "additionalProperties": false, - "description": "One button in the email. Needs `email_from`." - }, - "include_invite": { - "type": "boolean", - "description": "Attach the calendar invitation to the emails." - }, - "send_if_late": { - "type": "boolean", - "description": "If it was booked after this reminder's moment, send it right away instead of skipping it, as long as the meeting is still ahead. Only for `before_minutes` on `start`." - }, - "skip_no_show": { - "type": "boolean", - "description": "Don't send it once the booking is marked a no-show. Default: true on `end`." + "has_more": false + } + ] + }, + "SchedulingHolidayList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] }, - "only_if": { - "type": [ - "object", - "null" - ], - "properties": { - "metadata": { - "type": [ - "object", - "null" - ], - "additionalProperties": { - "type": "string", - "maxLength": 500 + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string" }, - "description": "The booking's metadata must have all of these." - }, - "location_types": { - "type": [ - "array", - "null" - ], - "items": { - "type": "string", - "enum": [ - "link", - "phone", - "in_person", - "google_meet", - "microsoft_teams", - "zoom" - ] + "name": { + "type": "string" }, - "minItems": 1, - "maxItems": 6, - "description": "The booking's location must be one of these." - } - }, - "additionalProperties": false, - "description": "Only for bookings matching all of this, checked when it's due." + "region": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "date", + "name", + "region" + ] + } + }, + "has_more": { + "type": "boolean" } }, "required": [ - "anchor" + "object", + "data", + "has_more" ], - "additionalProperties": false + "examples": [ + { + "object": "list", + "data": [ + { + "date": "2027-12-25", + "name": "Christmas Day", + "region": null + } + ], + "has_more": false + } + ] }, - "SchedulingEventTypeHostInput": { + "UpdateSchedulingHolidayCalendarRequest": { "type": "object", "properties": { - "host": { + "name": { "type": "string", - "description": "The host's id or uid." + "minLength": 1, + "maxLength": 200 }, - "role": { + "hosts": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 1000, + "description": "These hosts, by id or uid." + }, + "host_filter": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "description": "Everyone matching these attributes, asked again at every sync." + }, + "regions": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxItems": 50, + "description": "Which regional holidays apply, matched against how the calendar names them, such as `England`. Without this, all of them do; empty, only nationwide ones." + }, + "delegates": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 10, + "description": "Who can be booked in their place on those days, by id or uid, in order." + }, + "existing_bookings": { "type": "string", "enum": [ - "required", - "member" + "leave", + "reassign" ], - "default": "member", - "description": "`required` hosts must all be free; one `member` is chosen for a booking." - }, - "weight": { - "type": "integer", - "minimum": 1, - "maximum": 10000, - "default": 100, - "description": "Share of a pool's bookings relative to other members." + "description": "What happens to bookings already on a holiday. `leave` (the default) only reports them. `reassign` moves them." + } + }, + "additionalProperties": false, + "examples": [ + { + "host_filter": { + "team": "london" + } + } + ] + }, + "DeletedSchedulingHolidayCalendar": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - "schedule": { - "type": [ - "string", - "null" - ], - "default": null, - "description": "A schedule of this host's. Default: their default schedule." + "deleted": { + "type": "boolean", + "enum": [ + true + ] } }, "required": [ - "host" + "id", + "deleted" ], - "additionalProperties": false + "examples": [ + { + "id": "hcal_01kd2m9a0b1c2d3e4f5g6h7j8m", + "deleted": true + } + ] }, - "SchedulingEventTypeList": { + "SchedulingOutOfOfficeList": { "type": "object", "properties": { "object": { @@ -20200,7 +20600,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingEventType" + "$ref": "#/components/schemas/SchedulingOutOfOffice" } }, "has_more": { @@ -20217,45 +20617,27 @@ "object": "list", "data": [ { - "object": "scheduling.event_type", - "id": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", - "uid": null, - "title": "Initial consultation", - "description": null, - "kind": "one_on_one", - "status": "active", - "duration_minutes": 30, - "duration_options": [], - "step_minutes": 30, - "grid_time_zone": null, - "buffer_before_minutes": 0, - "buffer_after_minutes": 10, - "min_notice_minutes": 120, - "booking_window": null, - "limits": [], - "capacity": null, - "requires_approval": false, - "approval_ttl_minutes": 1440, - "hold_ttl_seconds": 600, - "cancel_notice_minutes": 0, - "reschedule_notice_minutes": 0, - "max_guests": 0, - "max_active_bookings_per_attendee": null, - "host_calendar_details": "neutral", - "questions": [], - "locations": [], - "host_filter": null, - "notifications": null, - "reminders": null, - "hosts": [ - { - "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", - "role": "member", - "weight": 100, - "schedule": null - } + "object": "scheduling.out_of_office", + "id": "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m", + "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "start": "2027-08-02T04:00:00.000Z", + "end": "2027-08-14T04:00:00.000Z", + "all_day": { + "start_date": "2027-08-02", + "end_date": "2027-08-13", + "time_zone": "America/New_York" + }, + "reason": "vacation", + "notes": null, + "public_message": "Back on the 16th.", + "show_on_calendar": true, + "delegates": [ + "host_01kd2m9a0b1c2d3e4f5g6h7j8n" ], - "metadata": {}, + "existing_bookings": "leave", + "external_id": null, + "group": null, + "source": "api", "created_at": "2027-01-01T10:00:00.000Z", "updated_at": "2027-01-01T10:00:00.000Z" } @@ -20264,176 +20646,176 @@ } ] }, - "UpdateEventTypeRequest": { + "SchedulingOutOfOffice": { "type": "object", "properties": { - "uid": { + "object": { + "type": "string", + "enum": [ + "scheduling.out_of_office" + ] + }, + "id": { + "type": "string" + }, + "host": { + "type": "string" + }, + "start": { + "type": "string" + }, + "end": { + "type": "string" + }, + "all_day": { "type": [ - "string", + "object", "null" ], - "minLength": 1, - "maxLength": 255, - "description": "Your own id for this event type. `null` removes it." + "properties": { + "start_date": { + "type": "string" + }, + "end_date": { + "type": "string" + }, + "time_zone": { + "type": "string" + } + }, + "required": [ + "start_date", + "end_date", + "time_zone" + ], + "description": "How it was given, when it was as whole dates." }, - "description": { + "reason": { + "type": "string" + }, + "notes": { "type": [ "string", "null" - ], - "maxLength": 2000, - "description": "Plain text about the event type, shown on hosted booking pages and never in emails. Links, and anything shaped like a domain, must be on your organization's verified domains or their subdomains, and use `https`. `null` removes it." + ] }, - "title": { - "type": "string", - "minLength": 1, - "maxLength": 120, - "description": "Shown to attendees and in emails. Host calendars show it only with `host_calendar_details: full`." + "public_message": { + "type": [ + "string", + "null" + ] }, - "duration_minutes": { - "type": "integer", - "minimum": 5, - "maximum": 1440, - "description": "How long a booking lasts by default, in minutes." + "show_on_calendar": { + "type": "boolean" }, - "duration_options": { + "delegates": { "type": "array", "items": { - "type": "integer", - "minimum": 5, - "maximum": 1440 + "type": "string" }, - "maxItems": 10, - "description": "Other lengths an attendee may choose, in minutes. Empty: only `duration_minutes`." + "description": "Who can be booked in their place." }, - "step_minutes": { - "type": "integer", - "minimum": 5, - "maximum": 1440, - "description": "How far apart slots start. Default: the duration." + "existing_bookings": { + "type": "string", + "enum": [ + "leave", + "reassign" + ] }, - "grid_time_zone": { + "external_id": { "type": [ "string", "null" - ], - "minLength": 1, - "maxLength": 64, - "description": "The zone slots align in. Default: each host's own.", - "examples": [ - "America/New_York" ] }, - "buffer_before_minutes": { - "type": "integer", - "minimum": 0, - "maximum": 480, - "description": "Minutes kept free before each booking. Default: 0." - }, - "buffer_after_minutes": { - "type": "integer", - "minimum": 0, - "maximum": 480, - "description": "Minutes kept free after each booking. Default: 0." - }, - "min_notice_minutes": { - "type": "integer", - "minimum": 0, - "maximum": 525600, - "description": "Slots starting sooner than this many minutes from now aren't offered. Default: 0." - }, - "booking_window": { - "$ref": "#/components/schemas/SchedulingBookingWindow" - }, - "limits": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SchedulingLimit" - }, - "maxItems": 4, - "description": "Caps on bookings per day or week. Default: none." - }, - "capacity": { - "type": [ - "integer", - "null" - ], - "minimum": 2, - "maximum": 10000, - "description": "Attendees per session. Required for, and only allowed on, `group`." - }, - "requires_approval": { - "type": "boolean", - "description": "Bookings start as `pending_approval`, holding the time until approved or declined. Default: `false`." - }, - "approval_ttl_minutes": { - "type": "integer", - "minimum": 5, - "maximum": 43200, - "description": "How long a booking can wait for approval before it's declined, in minutes. Default: 1440." - }, - "hold_ttl_seconds": { - "type": "integer", - "minimum": 60, - "maximum": 86400, - "description": "How long a hold keeps its slot, in seconds. Default: 600." - }, - "cancel_notice_minutes": { - "type": "integer", - "minimum": 0, - "maximum": 525600, - "description": "Attendees can't cancel closer to the start than this. API keys can." - }, - "reschedule_notice_minutes": { - "type": "integer", - "minimum": 0, - "maximum": 525600, - "description": "Attendees can't reschedule closer to the start than this. API keys can. Default: 0." - }, - "max_guests": { - "type": "integer", - "minimum": 0, - "maximum": 10, - "description": "How many people an attendee can add to a booking. Default: 0. Guests are emailed about it. Not on `group`." - }, - "max_active_bookings_per_attendee": { + "group": { "type": [ - "integer", + "string", "null" ], - "minimum": 1, - "maximum": 1000, - "description": "How many upcoming bookings of this event type one person can have at a time, known by `uid` or email. Default: `null`, no limit. Past this, booking answers `attendee_limit_reached`. Guests don't count, and each occurrence of a series does." + "description": "Shared by the entries one request made for several hosts." }, - "host_calendar_details": { + "source": { "type": "string", "enum": [ - "neutral", - "full" - ], - "description": "What hosts' calendars say about a booking. `neutral` (the default): a private event titled \"Appointment\". `full`: a private event with this title and the attendee's name, plus names, emails and phone numbers in its description, which go to the host's calendar provider. Never answers. Refused in HIPAA mode." + "api", + "host", + "manager", + "dashboard", + "holiday" + ] }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SchedulingQuestion" + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "object", + "id", + "host", + "start", + "end", + "all_day", + "reason", + "notes", + "public_message", + "show_on_calendar", + "delegates", + "existing_bookings", + "external_id", + "group", + "source", + "created_at", + "updated_at" + ], + "examples": [ + { + "object": "scheduling.out_of_office", + "id": "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m", + "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "start": "2027-08-02T04:00:00.000Z", + "end": "2027-08-14T04:00:00.000Z", + "all_day": { + "start_date": "2027-08-02", + "end_date": "2027-08-13", + "time_zone": "America/New_York" }, - "maxItems": 25, - "description": "What attendees are asked when booking. Keys must be unique." + "reason": "vacation", + "notes": null, + "public_message": "Back on the 16th.", + "show_on_calendar": true, + "delegates": [ + "host_01kd2m9a0b1c2d3e4f5g6h7j8n" + ], + "existing_bookings": "leave", + "external_id": null, + "group": null, + "source": "api", + "created_at": "2027-01-01T10:00:00.000Z", + "updated_at": "2027-01-01T10:00:00.000Z" + } + ] + }, + "CreateSchedulingOutOfOfficeRequest": { + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "One host, by id or uid." }, - "locations": { + "hosts": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingLocation" + "type": "string" }, - "maxItems": 10, - "description": "Where bookings can meet. A browser only chooses among these. A `group` event type can't use `zoom`." + "maxItems": 500, + "description": "Several hosts, by id or uid. Each gets an entry of their own." }, "host_filter": { - "type": [ - "object", - "null" - ], + "type": "object", "additionalProperties": { "anyOf": [ { @@ -20450,289 +20832,605 @@ } ] }, - "description": "Pool members also include active hosts matching all of these attributes." + "description": "Every active host matching these attributes right now. A host added later isn't covered." }, - "notifications": { - "type": [ - "object", - "null" + "start": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + }, + "end": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + }, + "start_date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "The first whole day away. Use instead of `start` and `end`.", + "examples": [ + "2027-08-02" + ] + }, + "end_date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "The last whole day away, which is included.", + "examples": [ + "2027-08-13" + ] + }, + "time_zone": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "What the dates mean in. Default: the host's zone when this is made.", + "examples": [ + "America/New_York" + ] + }, + "delegates": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 10, + "description": "Who can be booked in this host's place during it, by id or uid, in the order to try them. They still have to fit how a booking's routed." + }, + "reason": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,63}$", + "description": "One of `unspecified`, `vacation`, `travel`, `sick`, `public_holiday`, `parental_leave`, `training` and `other`, or a key from the settings' `out_of_office_reasons`. Only people who manage this host see it: never bookers, webhooks or calendars.", + "examples": [ + "vacation" + ] + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "For people who manage this host. Encrypted, and never shown to a booker." + }, + "public_message": { + "type": "string", + "maxLength": 300, + "description": "Shown to bookers while this host is away. Can't contain a link.", + "examples": [ + "Back on the 16th. Priya is seeing my patients until then." + ] + }, + "show_on_calendar": { + "type": "boolean", + "description": "Put a private \"Out of office\" event on the host's calendar. Default: true." + }, + "existing_bookings": { + "type": "string", + "enum": [ + "leave", + "reassign" ], - "additionalProperties": { - "type": "object", - "properties": { - "email_attendee": { - "type": "boolean" - }, - "email_host": { - "type": "boolean" + "description": "What happens to bookings already in the way. `leave` (the default) only reports them. `reassign` moves them, and any nobody can take stay where they are." + }, + "reassign": { + "type": "object", + "properties": { + "to_host": { + "type": "string", + "description": "Give them all to this host." + }, + "preferred_hosts": { + "type": "array", + "items": { + "type": "string" }, - "sms_attendee": { - "type": "boolean" - } + "maxItems": 20, + "description": "Try these first, in order." }, - "additionalProperties": false + "otherwise": { + "type": "string", + "enum": [ + "auto", + "leave" + ], + "description": "After `preferred_hosts`: `auto` picks the fairest who's free (the default), `leave` keeps it where it is." + } }, - "description": "Per event, overriding your settings for this event type." + "additionalProperties": false, + "description": "With `existing_bookings: reassign`: who they go to. Default: the fairest who's free." }, - "reminders": { + "external_id": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Your own id for this entry, unique in the environment. Needs one host." + } + }, + "additionalProperties": false, + "examples": [ + { + "host": "doctor_482", + "start_date": "2027-08-02", + "end_date": "2027-08-13", + "reason": "vacation", + "public_message": "Back on the 16th." + } + ] + }, + "UpdateSchedulingOutOfOfficeRequest": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + }, + "end": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + }, + "start_date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "The first whole day away. Use instead of `start` and `end`.", + "examples": [ + "2027-08-02" + ] + }, + "end_date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "The last whole day away, which is included.", + "examples": [ + "2027-08-13" + ] + }, + "time_zone": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "What the dates mean in. Default: the host's zone when this is made.", + "examples": [ + "America/New_York" + ] + }, + "reason": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,63}$", + "description": "One of `unspecified`, `vacation`, `travel`, `sick`, `public_holiday`, `parental_leave`, `training` and `other`, or a key from the settings' `out_of_office_reasons`. Only people who manage this host see it: never bookers, webhooks or calendars.", + "examples": [ + "vacation" + ] + }, + "notes": { "type": [ - "array", + "string", + "null" + ], + "maxLength": 2000 + }, + "public_message": { + "type": [ + "string", "null" ], + "maxLength": 300 + }, + "show_on_calendar": { + "type": "boolean" + }, + "delegates": { + "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingReminderInput" + "type": "string" }, "maxItems": 10, - "description": "Replaces your settings' `reminders` for this event type. `null` inherits them; an empty list means none." + "description": "Who can be booked in this host's place during it, by id or uid, in the order to try them. They still have to fit how a booking's routed." }, - "metadata": { + "existing_bookings": { + "type": "string", + "enum": [ + "leave", + "reassign" + ], + "description": "What happens to bookings already in the way. `leave` (the default) only reports them. `reassign` moves them, and any nobody can take stay where they are." + }, + "reassign": { "type": "object", - "additionalProperties": { - "type": "string", - "maxLength": 500 + "properties": { + "to_host": { + "type": "string", + "description": "Give them all to this host." + }, + "preferred_hosts": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 20, + "description": "Try these first, in order." + }, + "otherwise": { + "type": "string", + "enum": [ + "auto", + "leave" + ], + "description": "After `preferred_hosts`: `auto` picks the fairest who's free (the default), `leave` keeps it where it is." + } }, - "description": "Your own key-value pairs." + "additionalProperties": false, + "description": "With `existing_bookings: reassign`: who they go to. Default: the fairest who's free." + } + }, + "additionalProperties": false, + "examples": [ + { + "end_date": "2027-08-20" + } + ] + }, + "PutSchedulingOutOfOfficeRequest": { + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "The host, by id or uid." }, - "status": { + "start": { "type": "string", - "enum": [ - "active", - "archived" - ], - "description": "`archived` stops new bookings, and keeps existing ones." + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] }, - "hosts": { + "end": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + }, + "start_date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "The first whole day away. Use instead of `start` and `end`.", + "examples": [ + "2027-08-02" + ] + }, + "end_date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "The last whole day away, which is included.", + "examples": [ + "2027-08-13" + ] + }, + "time_zone": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "What the dates mean in. Default: the host's zone when this is made.", + "examples": [ + "America/New_York" + ] + }, + "delegates": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingEventTypeHostInput" + "type": "string" }, - "maxItems": 500, - "description": "Replaces the assigned hosts. Removed hosts keep their existing bookings." + "maxItems": 10, + "description": "Who can be booked in this host's place during it, by id or uid, in the order to try them. They still have to fit how a booking's routed." + }, + "reason": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,63}$", + "description": "One of `unspecified`, `vacation`, `travel`, `sick`, `public_holiday`, `parental_leave`, `training` and `other`, or a key from the settings' `out_of_office_reasons`. Only people who manage this host see it: never bookers, webhooks or calendars.", + "examples": [ + "vacation" + ] + }, + "notes": { + "type": "string", + "maxLength": 2000, + "description": "For people who manage this host. Encrypted, and never shown to a booker." + }, + "public_message": { + "type": "string", + "maxLength": 300, + "description": "Shown to bookers while this host is away. Can't contain a link.", + "examples": [ + "Back on the 16th. Priya is seeing my patients until then." + ] + }, + "show_on_calendar": { + "type": "boolean", + "description": "Put a private \"Out of office\" event on the host's calendar. Default: true." + }, + "existing_bookings": { + "type": "string", + "enum": [ + "leave", + "reassign" + ], + "description": "What happens to bookings already in the way. `leave` (the default) only reports them. `reassign` moves them, and any nobody can take stay where they are." + }, + "reassign": { + "type": "object", + "properties": { + "to_host": { + "type": "string", + "description": "Give them all to this host." + }, + "preferred_hosts": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 20, + "description": "Try these first, in order." + }, + "otherwise": { + "type": "string", + "enum": [ + "auto", + "leave" + ], + "description": "After `preferred_hosts`: `auto` picks the fairest who's free (the default), `leave` keeps it where it is." + } + }, + "additionalProperties": false, + "description": "With `existing_bookings: reassign`: who they go to. Default: the fairest who's free." } }, + "required": [ + "host" + ], "additionalProperties": false, "examples": [ { - "min_notice_minutes": 240 + "host": "doctor_482", + "start_date": "2027-08-02", + "end_date": "2027-08-13", + "reason": "vacation" } ] }, - "SchedulingSlotList": { + "DeletedSchedulingOutOfOffice": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "list" + "scheduling.out_of_office" ] }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SchedulingSlot" - } + "id": { + "type": "string" }, - "has_more": { + "deleted": { "type": "boolean", "enum": [ - false + true ] } }, "required": [ "object", - "data", - "has_more" + "id", + "deleted" ], "examples": [ { - "object": "list", - "data": [ - { - "start": "2027-01-04T15:00:00.000Z", - "end": "2027-01-04T15:30:00.000Z", - "hosts": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" - ], - "seats_left": null - } - ], - "has_more": false + "object": "scheduling.out_of_office", + "id": "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m", + "deleted": true } ] }, - "SchedulingSlot": { + "SchedulingOutOfOfficeConflictList": { "type": "object", "properties": { - "start": { - "type": "string", - "description": "When the slot starts." - }, - "end": { + "object": { "type": "string", - "description": "When the slot ends." + "enum": [ + "list" + ] }, - "hosts": { + "data": { "type": "array", "items": { - "type": "string" - }, - "description": "Who could take it." + "$ref": "#/components/schemas/SchedulingOutOfOfficeConflict" + } }, - "seats_left": { - "type": [ - "number", - "null" - ], - "description": "Free seats at this start, for a `group` event type. `null` for the others." + "has_more": { + "type": "boolean" } }, "required": [ - "start", - "end", - "hosts", - "seats_left" + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ + { + "object": "scheduling.out_of_office_conflict", + "booking": "book_01kd2m9a0b1c2d3e4f5g6h7j8m", + "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "title": "Consultation", + "status": "confirmed", + "start": "2027-08-03T15:00:00.000Z", + "end": "2027-08-03T15:30:00.000Z", + "series": null, + "group_session": null + } + ], + "has_more": false + } ] }, - "SchedulingSlotExplanation": { + "SchedulingOutOfOfficeConflict": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "scheduling.slot_explanation" + "scheduling.out_of_office_conflict" ] }, - "start": { - "type": "string", - "description": "The start asked about." + "booking": { + "type": "string" }, - "end": { - "type": "string", - "description": "When it would end." + "event_type": { + "type": "string" }, - "available": { - "type": "boolean", - "description": "Whether it can be booked right now." + "title": { + "type": "string" }, - "reason": { + "status": { + "type": "string", + "enum": [ + "held", + "pending_approval", + "confirmed", + "cancelled", + "declined" + ] + }, + "start": { + "type": "string" + }, + "end": { + "type": "string" + }, + "series": { "type": [ "string", "null" - ], - "description": "Why it can't: `not_on_grid`, `too_soon`, `outside_booking_window`, `calendar_unverified`, `outside_availability`, `host_busy`, `already_claimed`, `limit_reached` or `session_full`, or `event_type_inactive` or `no_hosts`. `null` when it can. With several hosts refused, the first one's reason: see `hosts`." + ] }, - "hosts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "host": { - "type": "string", - "description": "The host's id." - }, - "role": { - "type": "string", - "enum": [ - "required", - "member" - ], - "description": "`required` hosts must all be free. Only one `member` needs to be." - }, - "available": { - "type": "boolean", - "description": "Whether this host could take it." - }, - "reason": { - "type": [ - "string", - "null" - ], - "description": "Why not: `not_on_grid`, `too_soon`, `outside_booking_window`, `calendar_unverified`, `outside_availability`, `host_busy`, `already_claimed`, `limit_reached` or `session_full`. `null` when they could." - }, - "calendar_reason": { - "type": [ - "string", - "null" - ], - "description": "With `calendar_unverified`: `calendar_needs_reauth`, `calendar_stale` or `calendar_not_covered`." - } - }, - "required": [ - "host", - "role", - "available", - "reason", - "calendar_reason" - ] - }, - "description": "Everyone who could take this event type, narrowed by the query." + "group_session": { + "type": [ + "string", + "null" + ] } }, "required": [ "object", + "booking", + "event_type", + "title", + "status", "start", "end", - "available", - "reason", - "hosts" + "series", + "group_session" ], "examples": [ { - "object": "scheduling.slot_explanation", - "start": "2027-01-04T15:00:00.000Z", - "end": "2027-01-04T15:30:00.000Z", - "available": false, - "reason": "host_busy", - "hosts": [ - { - "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", - "role": "member", - "available": false, - "reason": "host_busy", - "calendar_reason": null - } - ] + "object": "scheduling.out_of_office_conflict", + "booking": "book_01kd2m9a0b1c2d3e4f5g6h7j8m", + "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "title": "Consultation", + "status": "confirmed", + "start": "2027-08-03T15:00:00.000Z", + "end": "2027-08-03T15:30:00.000Z", + "series": null, + "group_session": null } ] }, - "SchedulingAvailabilitySearchResult": { + "SchedulingSchedule": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "list" + "scheduling.schedule" ] }, - "data": { + "id": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The id of the host this schedule belongs to." + }, + "name": { + "type": "string", + "description": "A label for the schedule. `Default` when none was given." + }, + "time_zone": { + "type": "string", + "description": "The IANA time zone its rules and overrides are local to." + }, + "is_default": { + "type": "boolean", + "description": "Whether this is the host's default schedule, used by event types that don't name one. A host's first schedule always is." + }, + "rules": { "type": "array", "items": { "type": "object", "properties": { - "host": { + "days": { + "type": "array", + "items": { + "type": "number" + }, + "description": "Weekdays, with Sunday as 0." + }, + "start": { "type": "string", - "description": "The host's id." + "description": "Local start time, `HH:MM`." }, - "uid": { - "type": [ - "string", - "null" - ], - "description": "The host's own uid. `null` when none is set." + "end": { + "type": "string", + "description": "Local end time, `HH:MM`. `24:00` is the end of the day." + } + }, + "required": [ + "days", + "start", + "end" + ] + }, + "description": "Weekly hours in local time, unchanged across daylight saving." + }, + "overrides": { + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string", + "description": "The local date, `YYYY-MM-DD`." }, - "slots": { + "ranges": { "type": "array", "items": { "type": "object", "properties": { "start": { "type": "string", - "description": "When the slot starts." + "description": "Local start time, `HH:MM`." }, "end": { "type": "string", - "description": "When the slot ends." + "description": "Local end time, `HH:MM`. `24:00` is the end of the day." } }, "required": [ @@ -20740,344 +21438,548 @@ "end" ] }, - "description": "The host's soonest slots." + "description": "That day's hours. Empty means unavailable all day." } }, "required": [ - "host", - "uid", - "slots" + "date", + "ranges" ] }, - "description": "Each host with availability, soonest first." + "description": "Dates whose hours replace the weekly rules." }, - "has_more": { - "type": "boolean", - "description": "Whether more hosts matched than `limit` returned." + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" } }, "required": [ "object", - "data", - "has_more" + "id", + "host", + "name", + "time_zone", + "is_default", + "rules", + "overrides", + "created_at", + "updated_at" ], "examples": [ { - "object": "list", - "data": [ + "object": "scheduling.schedule", + "id": "sched_01kd2m9a0b1c2d3e4f5g6h7j8m", + "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "name": "Clinic hours", + "time_zone": "America/New_York", + "is_default": true, + "rules": [ { - "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", - "uid": "doctor_482", - "slots": [ - { - "start": "2027-01-04T15:00:00.000Z", - "end": "2027-01-04T15:30:00.000Z" - } - ] + "days": [ + 1, + 2, + 3, + 4, + 5 + ], + "start": "09:00", + "end": "17:00" } ], - "has_more": false + "overrides": [ + { + "date": "2027-12-24", + "ranges": [] + } + ], + "created_at": "2027-01-01T10:00:00.000Z", + "updated_at": "2027-01-01T10:00:00.000Z" } ] }, - "AvailabilitySearchRequest": { + "UpsertScheduleRequest": { "type": "object", "properties": { - "event_type": { + "name": { "type": "string", - "description": "The event type's id or uid." + "minLength": 1, + "maxLength": 100, + "description": "A label for the schedule. Default on creation: `Default`." }, - "start": { + "time_zone": { "type": "string", - "description": "An ISO 8601 instant with an offset.", + "minLength": 1, + "maxLength": 64, + "description": "Default: the host's.", "examples": [ - "2027-01-04T15:00:00.000Z" + "America/New_York" ] }, - "end": { - "type": "string", - "description": "An ISO 8601 instant with an offset.", - "examples": [ - "2027-01-04T15:00:00.000Z" - ] + "is_default": { + "type": "boolean", + "description": "`true` makes this the host's default schedule, in place of the current one. A host's first schedule is their default anyway." }, - "attributes": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "string", - "maxLength": 200 - }, - { - "type": "array", - "items": { - "type": "string", - "maxLength": 200 - }, - "maxItems": 50 - } - ] + "rules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingWeeklyRule" }, - "description": "Only hosts matching all of these." + "maxItems": 50, + "description": "Weekly hours in local time. A range can't cross midnight: use two." }, - "hosts": { + "overrides": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/SchedulingScheduleOverride" }, - "maxItems": 500, - "description": "Only these hosts (ids or uids). Default: everyone the event type could offer." - }, - "slots_per_host": { - "type": "integer", - "minimum": 1, - "maximum": 50, - "default": 3, - "description": "How many of each host's soonest slots to return." - }, - "limit": { - "type": "integer", - "minimum": 1, - "maximum": 500, - "default": 50, - "description": "Hosts to return, soonest availability first." + "maxItems": 366, + "description": "Replaces the schedule's overrides when given." } }, "required": [ - "event_type", - "start", - "end" + "rules" ], "additionalProperties": false, "examples": [ { - "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", - "start": "2027-01-04T00:00:00.000Z", - "end": "2027-01-18T00:00:00.000Z", - "attributes": { - "licensed_in": "CA" - } + "name": "Clinic hours", + "rules": [ + { + "days": [ + 1, + 2, + 3, + 4, + 5 + ], + "start": "09:00", + "end": "17:00" + } + ], + "overrides": [ + { + "date": "2027-12-24", + "ranges": [] + } + ] } ] }, - "SchedulingBooking": { + "SchedulingWeeklyRule": { "type": "object", "properties": { - "object": { - "type": "string", - "enum": [ - "scheduling.booking" - ] - }, - "id": { - "type": "string" - }, - "event_type": { - "type": "string", - "description": "The id of the booked event type." - }, - "status": { - "type": "string", - "enum": [ - "held", - "pending_approval", - "confirmed", - "cancelled", - "declined" - ], - "description": "`held`: a hold keeping the slot until `hold_expires_at`, uncharged and known to nobody. `pending_approval`: holds the time until approved, declined or `approval_expires_at`. `confirmed`: booked. `cancelled`: cancelled, or a hold released or expired. `declined`: refused, or its approval deadline passed." - }, - "title": { - "type": "string", - "description": "The event type's title when this was booked." + "days": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0, + "maximum": 6 + }, + "minItems": 1, + "maxItems": 7, + "description": "Weekdays, with Sunday as 0." }, "start": { "type": "string", - "description": "When the booking starts." + "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$|^24:00$", + "description": "A local time, `HH:MM`. `24:00` is the end of the day." }, "end": { "type": "string", - "description": "When the booking ends, without buffers." - }, - "hosts": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The ids of the hosts taking it." + "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$|^24:00$", + "description": "A local time, `HH:MM`. `24:00` is the end of the day." + } + }, + "required": [ + "days", + "start", + "end" + ], + "additionalProperties": false + }, + "SchedulingScheduleOverride": { + "type": "object", + "properties": { + "date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "A local date in the schedule's time zone.", + "examples": [ + "2027-12-24" + ] }, - "attendees": { + "ranges": { "type": "array", "items": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "role": { + "start": { "type": "string", - "enum": [ - "attendee", - "guest" - ], - "description": "`attendee` booked it and comes first; a `guest` was added by them." - }, - "uid": { - "type": [ - "string", - "null" - ], - "description": "Your own id for this person. `null` when none was given." - }, - "name": { - "type": [ - "string", - "null" - ], - "description": "`null` when not given, or after their details are erased." - }, - "email": { - "type": [ - "string", - "null" - ], - "description": "`null` when not given, or after their details are erased." - }, - "phone": { - "type": [ - "string", - "null" - ], - "description": "`null` when not given, or after their details are erased." + "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$|^24:00$", + "description": "A local time, `HH:MM`. `24:00` is the end of the day." }, - "time_zone": { + "end": { "type": "string", - "description": "Their IANA time zone." - }, - "locale": { - "type": [ - "string", - "null" - ], - "description": "Their locale. `null` when not given." - }, - "answers": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "description": "Only when retrieving one booking." + "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$|^24:00$", + "description": "A local time, `HH:MM`. `24:00` is the end of the day." } }, "required": [ - "id", - "role", - "uid", - "name", - "email", - "phone", - "time_zone", - "locale" - ] + "start", + "end" + ], + "additionalProperties": false }, - "description": "Who's booked: the attendee first, then any guests they added. A group session's seat is its own booking." - }, - "location": { - "$ref": "#/components/schemas/SchedulingLocation" + "maxItems": 10, + "description": "Replaces the weekly rules that day. Empty means unavailable all day." + } + }, + "required": [ + "date", + "ranges" + ], + "additionalProperties": false + }, + "SchedulingScheduleList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] }, - "hold_expires_at": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingSchedule" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ + { + "object": "scheduling.schedule", + "id": "sched_01kd2m9a0b1c2d3e4f5g6h7j8m", + "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "name": "Clinic hours", + "time_zone": "America/New_York", + "is_default": true, + "rules": [ + { + "days": [ + 1, + 2, + 3, + 4, + 5 + ], + "start": "09:00", + "end": "17:00" + } + ], + "overrides": [ + { + "date": "2027-12-24", + "ranges": [] + } + ], + "created_at": "2027-01-01T10:00:00.000Z", + "updated_at": "2027-01-01T10:00:00.000Z" + } + ], + "has_more": false + } + ] + }, + "DeletedSchedulingSchedule": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "scheduling.schedule" + ] + }, + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "object", + "id", + "deleted" + ], + "examples": [ + { + "object": "scheduling.schedule", + "id": "sched_01kd2m9a0b1c2d3e4f5g6h7j8m", + "deleted": true + } + ] + }, + "SchedulingEventType": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "scheduling.event_type" + ] + }, + "id": { + "type": "string" + }, + "uid": { "type": [ "string", "null" ], - "description": "When a `held` booking gives up its slot. `null` otherwise." + "description": "Your own id for this event type, accepted wherever its id is. `null` when none is set." }, - "approval_expires_at": { + "description": { "type": [ "string", "null" ], - "description": "When a `pending_approval` booking is declined if nobody decides. `null` otherwise." + "description": "Plain text shown on hosted booking pages. `null` when there's none." }, - "series": { + "title": { + "type": "string", + "description": "Shown to attendees and in emails. Host calendars show it only with `host_calendar_details: full`." + }, + "kind": { + "type": "string", + "enum": [ + "one_on_one", + "pool", + "collective", + "group" + ], + "description": "Who a booking needs. `one_on_one`: one host. `pool`: any one of its hosts. `collective`: every `required` host, plus at most one `member`. `group`: one host, with attendees taking seats up to `capacity`." + }, + "status": { + "type": "string", + "enum": [ + "active", + "archived" + ], + "description": "`active`: can be booked. `archived`: new bookings answer `not_found`; existing ones stay." + }, + "duration_minutes": { + "type": "number", + "description": "How long a booking lasts by default, in minutes." + }, + "duration_options": { + "type": "array", + "items": { + "type": "number" + }, + "description": "Other lengths an attendee may choose, in minutes. Empty: only `duration_minutes`." + }, + "step_minutes": { + "type": "number", + "description": "How far apart slots start, in minutes." + }, + "grid_time_zone": { "type": [ "string", "null" ], - "description": "The id of the series it belongs to. `null`: not part of one." + "description": "The zone slots align in. `null`: each host's own." }, - "series_index": { + "buffer_before_minutes": { + "type": "number", + "description": "Minutes kept free before each booking." + }, + "buffer_after_minutes": { + "type": "number", + "description": "Minutes kept free after each booking." + }, + "min_notice_minutes": { + "type": "number", + "description": "Slots starting sooner than this many minutes from now aren't offered." + }, + "booking_window": { + "$ref": "#/components/schemas/SchedulingBookingWindow" + }, + "limits": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingLimit" + }, + "description": "Caps on bookings per day or week." + }, + "capacity": { "type": [ "number", "null" ], - "description": "Its place in the series, from 0. `null`: not part of one." + "description": "Attendees per session of a `group` event type. `null` for every other kind." }, - "group_session": { - "type": [ - "string", - "null" - ], - "description": "The id of the group session whose seat this is. `null`: not a seat." + "requires_approval": { + "type": "boolean", + "description": "Whether bookings start as `pending_approval`, holding the time until approved or declined." }, - "calendar_unverified": { + "reassignment_enabled": { "type": "boolean", - "description": "Reserved for flagging a booking made on last known calendar data. Nothing sets it yet, so it's always `false`." + "description": "Whether bookings of this event type can go to another host." }, - "external_conflict_at": { + "approval_ttl_minutes": { + "type": "number", + "description": "How long a booking can wait for approval before it's declined, in minutes." + }, + "hold_ttl_seconds": { + "type": "number", + "description": "How long a hold keeps its slot, in seconds." + }, + "cancel_notice_minutes": { + "type": "number", + "description": "Attendees can't cancel closer to the start than this. API keys can." + }, + "reschedule_notice_minutes": { + "type": "number", + "description": "Attendees can't reschedule closer to the start than this. API keys can." + }, + "max_guests": { + "type": "number", + "description": "How many people an attendee can add to a booking. Not on `group`." + }, + "max_active_bookings_per_attendee": { "type": [ - "string", + "number", "null" ], - "description": "When something else landed on this time in the host's calendar, or the host changed the event. The booking stands. `null`: no conflict known." + "description": "How many upcoming bookings of this event type one person can have at a time. `null`: no limit." }, - "no_show_at": { - "type": [ - "string", - "null" + "host_calendar_details": { + "type": "string", + "enum": [ + "neutral", + "full" ], - "description": "When the attendee was marked as a no-show. `null`: not marked." + "description": "What hosts' calendars say about a booking. `neutral`: a private event titled \"Appointment\". `full`: a private event with this title and the attendee's and guests' details." }, - "cancelled_at": { + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingQuestion" + }, + "description": "What attendees are asked when booking." + }, + "locations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingLocation" + }, + "description": "Where bookings can meet. A browser only chooses among these." + }, + "host_filter": { "type": [ - "string", + "object", "null" ], - "description": "When it was cancelled. `null`: not cancelled." + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "description": "Pool members also include active hosts matching all of these attributes. `null`: only `hosts`." }, - "cancelled_by": { + "notifications": { "type": [ - "string", + "object", "null" ], - "enum": [ - "api_key", - "user", - "host_session", - "booker_session", - "manage_session", - "system", - null - ], - "description": "Who cancelled it: `api_key`, `user` (the dashboard), `host_session`, `booker_session`, `manage_session` (the attendee's link) or `system`. `null`: not cancelled, or cancelled before this was recorded." + "additionalProperties": { + "type": "object", + "properties": { + "email_attendee": { + "type": "boolean" + }, + "email_host": { + "type": "boolean" + }, + "sms_attendee": { + "type": "boolean" + } + } + }, + "description": "Per event, overriding your settings for this event type. `null`: only those." }, - "cancellation_reason": { + "reminders": { "type": [ - "string", + "array", "null" ], - "description": "Why, if they said. Only when retrieving one booking. It's erased with the attendee's details, and it's never in webhooks, logs or emails." + "items": { + "$ref": "#/components/schemas/SchedulingReminder" + }, + "description": "What goes out around a booking, in place of your settings' list. `null` inherits it; an empty list means none." + }, + "hosts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "The host's id." + }, + "role": { + "type": "string", + "enum": [ + "required", + "member" + ], + "description": "`required` hosts must all be free; one `member` is chosen for a booking." + }, + "weight": { + "type": "number", + "description": "Share of a pool's bookings relative to other members." + }, + "schedule": { + "type": [ + "string", + "null" + ], + "description": "The id of the schedule used for this event type. `null`: the host's default schedule." + } + }, + "required": [ + "host", + "role", + "weight", + "schedule" + ] + }, + "description": "The hosts assigned by name." }, "metadata": { "type": "object", @@ -21096,766 +21998,580 @@ "required": [ "object", "id", - "event_type", - "status", + "uid", + "description", "title", - "start", - "end", + "kind", + "status", + "duration_minutes", + "duration_options", + "step_minutes", + "grid_time_zone", + "buffer_before_minutes", + "buffer_after_minutes", + "min_notice_minutes", + "booking_window", + "limits", + "capacity", + "requires_approval", + "reassignment_enabled", + "approval_ttl_minutes", + "hold_ttl_seconds", + "cancel_notice_minutes", + "reschedule_notice_minutes", + "max_guests", + "max_active_bookings_per_attendee", + "host_calendar_details", + "questions", + "locations", + "host_filter", + "notifications", + "reminders", "hosts", - "attendees", - "location", - "hold_expires_at", - "approval_expires_at", - "series", - "series_index", - "group_session", - "calendar_unverified", - "external_conflict_at", - "no_show_at", - "cancelled_at", - "cancelled_by", "metadata", "created_at", "updated_at" ], "examples": [ { - "object": "scheduling.booking", - "id": "book_01kd2m9a0b1c2d3e4f5g6h7j8m", - "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", - "status": "confirmed", + "object": "scheduling.event_type", + "id": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "uid": null, "title": "Initial consultation", - "start": "2027-01-04T15:00:00.000Z", - "end": "2027-01-04T15:30:00.000Z", + "description": null, + "kind": "one_on_one", + "status": "active", + "duration_minutes": 30, + "duration_options": [], + "step_minutes": 30, + "grid_time_zone": null, + "buffer_before_minutes": 0, + "buffer_after_minutes": 10, + "min_notice_minutes": 120, + "booking_window": null, + "limits": [], + "capacity": null, + "requires_approval": false, + "reassignment_enabled": true, + "approval_ttl_minutes": 1440, + "hold_ttl_seconds": 600, + "cancel_notice_minutes": 0, + "reschedule_notice_minutes": 0, + "max_guests": 0, + "max_active_bookings_per_attendee": null, + "host_calendar_details": "neutral", + "questions": [], + "locations": [], + "host_filter": null, + "notifications": null, + "reminders": null, "hosts": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" - ], - "attendees": [ { - "id": "attn_01kd2m9a0b1c2d3e4f5g6h7j8m", - "role": "attendee", - "uid": "patient_77", - "name": "Sam Lee", - "email": "sam@example.com", - "phone": null, - "time_zone": "UTC", - "locale": null + "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "role": "member", + "weight": 100, + "schedule": null } ], - "location": null, - "hold_expires_at": null, - "approval_expires_at": null, - "series": null, - "series_index": null, - "group_session": null, - "calendar_unverified": false, - "external_conflict_at": null, - "no_show_at": null, - "cancelled_at": null, - "cancelled_by": null, "metadata": {}, "created_at": "2027-01-01T10:00:00.000Z", "updated_at": "2027-01-01T10:00:00.000Z" } ] }, - "CreateBookingRequest": { - "type": "object", - "properties": { - "event_type": { - "type": "string", - "description": "The event type's id or uid." - }, - "start": { - "type": "string", - "description": "An ISO 8601 instant with an offset.", - "examples": [ - "2027-01-04T15:00:00.000Z" - ] - }, - "host": { - "type": "string", - "description": "A specific host (id or uid). Default: chosen fairly from those free." - }, - "attributes": { + "SchedulingBookingWindow": { + "oneOf": [ + { "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "string", - "maxLength": 200 - }, - { - "type": "array", - "items": { - "type": "string", - "maxLength": 200 - }, - "maxItems": 50 - } - ] - }, - "description": "Only choose among pool hosts matching all of these." - }, - "duration_minutes": { - "type": "integer", - "minimum": 5, - "maximum": 1440, - "description": "One of the event type's `duration_options`." - }, - "attendee": { - "$ref": "#/components/schemas/SchedulingAttendeeInput" - }, - "guests": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SchedulingGuestInput" + "properties": { + "kind": { + "type": "string", + "enum": [ + "rolling" + ] + }, + "days": { + "type": "integer", + "minimum": 1, + "maximum": 730, + "description": "Days ahead of now that slots are offered." + } }, - "maxItems": 10, - "description": "Up to the event type's `max_guests`." + "required": [ + "kind", + "days" + ], + "additionalProperties": false }, - "answers": { + { "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "string", - "maxLength": 5000 - }, - { - "type": "array", - "items": { - "type": "string", - "maxLength": 500 - }, - "maxItems": 50 - } - ] - }, - "description": "Answers to the event type's questions. Stored encrypted, and returned only by retrieving the booking." - }, - "location": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "link" - ] - }, - "url": { - "type": [ - "string", - "null" - ], - "maxLength": 2000, - "format": "uri", - "default": null, - "description": "The meeting link. `null` on an event type means the booking supplies its own." - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "phone" - ] - }, - "number": { - "type": [ - "string", - "null" - ], - "maxLength": 32, - "default": null, - "description": "The phone number to call. `null` on an event type means the booking supplies its own." - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "in_person" - ] - }, - "address": { - "type": [ - "string", - "null" - ], - "maxLength": 500, - "default": null, - "description": "The address to meet at. `null` on an event type means the booking supplies its own." - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "google_meet" - ] - } - }, - "required": [ - "type" - ], - "additionalProperties": false + "properties": { + "kind": { + "type": "string", + "enum": [ + "range" + ] }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "microsoft_teams" - ] - } - }, - "required": [ - "type" - ], - "additionalProperties": false + "from": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "The first local date offered, `YYYY-MM-DD`.", + "examples": [ + "2027-12-24" + ] }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "zoom" - ] - }, - "url": { - "type": [ - "string", - "null" - ], - "maxLength": 2000, - "format": "uri", - "default": null, - "description": "The meeting's link, once it's created on the host's Zoom account." - } - }, - "required": [ - "type" - ], - "additionalProperties": false + "to": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "The last local date offered, `YYYY-MM-DD`.", + "examples": [ + "2027-12-24" + ] } + }, + "required": [ + "kind", + "from", + "to" ], - "description": "One of the event type's `locations`, with your link filled in if it has none." + "additionalProperties": false }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string", - "maxLength": 500 - }, - "description": "Your own key-value pairs. Don't put sensitive data here." - } - }, - "required": [ - "event_type", - "start", - "attendee" - ], - "additionalProperties": false, - "examples": [ { - "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", - "start": "2027-01-04T15:00:00.000Z", - "attendee": { - "uid": "patient_77", - "name": "Sam Lee", - "email": "sam@example.com", - "time_zone": "UTC" - } + "type": "null" } - ] + ], + "description": "How far ahead a slot can be booked: `rolling` for a number of days from now, or `range` for fixed dates. On an event type, `null` means no window of its own." }, - "SchedulingAttendeeInput": { + "SchedulingLimit": { "type": "object", "properties": { - "uid": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "description": "Your own id for this person, used to erase their data later." - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100, - "description": "Needed for coss.com to notify them. Can't contain a link." - }, - "email": { - "type": "string", - "maxLength": 320, - "format": "email", - "description": "Where coss.com emails them, if it does." - }, - "phone": { + "period": { "type": "string", - "pattern": "^\\+[1-9]\\d{6,14}$", - "description": "An E.164 number, for SMS reminders." + "enum": [ + "day", + "week" + ], + "description": "In the host's time zone." }, - "time_zone": { - "type": "string", - "minLength": 1, - "maxLength": 64, - "description": "Their IANA time zone. A series keeps their local time across clock changes.", - "examples": [ - "America/New_York" - ] + "max": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "The most bookings allowed in one period." }, - "locale": { + "scope": { "type": "string", - "minLength": 2, - "maxLength": 35, - "description": "Their locale, such as `en-US`. Emails and texts about the booking are written in it when coss.com has it, otherwise in English, and guests get the same." + "enum": [ + "event_type", + "host" + ], + "default": "event_type", + "description": "Count this event type's bookings, or all of the host's." } }, "required": [ - "time_zone" + "period", + "max" ], "additionalProperties": false, - "description": "Name and email are only needed if coss.com notifies this attendee." + "description": "A cap on bookings in a day or a week." }, - "SchedulingGuestInput": { + "SchedulingQuestion": { "type": "object", "properties": { - "email": { + "key": { "type": "string", - "maxLength": 320, - "format": "email", - "description": "Where they're emailed about the booking." + "pattern": "^[a-z][a-z0-9_]{0,63}$", + "description": "The answer's key in `answers`. Unique within the event type." }, - "name": { + "type": { + "type": "string", + "enum": [ + "text", + "textarea", + "select", + "radio", + "checkbox", + "phone", + "email", + "number" + ], + "description": "How it's asked and which answers are valid." + }, + "label": { "type": "string", "minLength": 1, - "maxLength": 100, - "description": "Their name, if you have it." + "maxLength": 200, + "description": "The question as attendees see it." + }, + "required": { + "type": "boolean", + "default": false, + "description": "Whether a booking must answer it." + }, + "options": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "maxItems": 50, + "default": [], + "description": "The choices of a `select`, `radio` or `checkbox` question. `select` and `radio` need at least one." } }, "required": [ - "email" + "key", + "type", + "label" ], "additionalProperties": false, - "description": "Someone else coming along. They're emailed about the booking, with no manage link." + "description": "Something attendees are asked when booking. Answers are encrypted, and come back only when you retrieve one booking." }, - "SchedulingBookingList": { + "SchedulingReminder": { "type": "object", "properties": { - "object": { + "key": { + "type": "string", + "description": "What webhooks and the booking call this reminder." + }, + "anchor": { "type": "string", "enum": [ - "list" - ] + "start", + "end", + "confirmed", + "rescheduled", + "cancelled", + "declined", + "pending_approval", + "no_show" + ], + "description": "The moment this is timed from." }, - "data": { + "before_minutes": { + "type": [ + "number", + "null" + ], + "description": "How long before the anchor. `null` when it's after." + }, + "after_minutes": { + "type": [ + "number", + "null" + ], + "description": "How long after the anchor. `null` when it's before." + }, + "send": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingBooking" - } + "type": "string", + "enum": [ + "email_attendee", + "email_host", + "sms_attendee", + "sms_host", + "webhook" + ] + }, + "description": "Who hears about it. `webhook` fires `scheduling.booking.reminder`." }, - "has_more": { - "type": "boolean" - } - }, - "required": [ - "object", - "data", - "has_more" - ], - "examples": [ - { - "object": "list", - "data": [ - { - "object": "scheduling.booking", - "id": "book_01kd2m9a0b1c2d3e4f5g6h7j8m", - "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", - "status": "confirmed", - "title": "Initial consultation", - "start": "2027-01-04T15:00:00.000Z", - "end": "2027-01-04T15:30:00.000Z", - "hosts": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" + "email_to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fixed addresses that are emailed as well." + }, + "subject": { + "type": [ + "string", + "null" + ], + "description": "Your own subject. `null`: the standard one." + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "Your own words in the email. `null`: the standard ones." + }, + "sms_message": { + "type": [ + "string", + "null" + ], + "description": "Your own text. `null`: the standard one." + }, + "button": { + "type": [ + "object", + "null" + ], + "properties": { + "label": { + "type": "string", + "description": "What the button says." + }, + "url": { + "type": "string", + "description": "Where it goes." + } + }, + "required": [ + "label", + "url" + ], + "description": "One button in the email. `null`: none." + }, + "include_invite": { + "type": "boolean", + "description": "Whether the emails carry the invitation." + }, + "send_if_late": { + "type": "boolean", + "description": "Whether it goes right away if booked after its moment, instead of being skipped." + }, + "skip_no_show": { + "type": "boolean", + "description": "Whether it's dropped once the booking is marked a no-show." + }, + "only_if": { + "type": [ + "object", + "null" + ], + "properties": { + "metadata": { + "type": [ + "object", + "null" ], - "attendees": [ - { - "id": "attn_01kd2m9a0b1c2d3e4f5g6h7j8m", - "role": "attendee", - "uid": "patient_77", - "name": "Sam Lee", - "email": "sam@example.com", - "phone": null, - "time_zone": "UTC", - "locale": null - } + "additionalProperties": { + "type": "string" + }, + "description": "The booking's metadata must have all of these. `null`: any." + }, + "location_types": { + "type": [ + "array", + "null" ], - "location": null, - "hold_expires_at": null, - "approval_expires_at": null, - "series": null, - "series_index": null, - "group_session": null, - "calendar_unverified": false, - "external_conflict_at": null, - "no_show_at": null, - "cancelled_at": null, - "cancelled_by": null, - "metadata": {}, - "created_at": "2027-01-01T10:00:00.000Z", - "updated_at": "2027-01-01T10:00:00.000Z" + "items": { + "type": "string" + }, + "description": "The booking's location must be one of these. `null`: any." } + }, + "required": [ + "metadata", + "location_types" ], - "has_more": false + "description": "Only for bookings matching all of this. `null`: every booking." } + }, + "required": [ + "key", + "anchor", + "before_minutes", + "after_minutes", + "send", + "email_to", + "subject", + "message", + "sms_message", + "button", + "include_invite", + "send_if_late", + "skip_no_show", + "only_if" ] }, - "RescheduleBookingRequest": { + "CreateEventTypeRequest": { "type": "object", "properties": { - "start": { + "uid": { "type": "string", - "description": "The new start. The old time stays booked unless this one's free.", - "examples": [ - "2027-01-04T15:00:00.000Z" - ] + "minLength": 1, + "maxLength": 255, + "description": "Your own id for this event type, unique in the environment, accepted wherever its id is." }, - "host": { + "kind": { "type": "string", - "description": "Move a pool booking to this host. Default: its current hosts." - } - }, - "required": [ - "start" - ], - "additionalProperties": false, - "examples": [ - { - "start": "2027-01-05T15:00:00.000Z" - } - ] - }, - "CancelSchedulingBookingRequest": { - "type": "object", - "properties": { - "reason": { + "enum": [ + "one_on_one", + "pool", + "collective", + "group" + ], + "default": "one_on_one", + "description": "Who a booking needs. `one_on_one`: one host. `pool`: any one of its hosts. `collective`: every `required` host, plus at most one `member`. `group`: one host, with attendees taking seats up to `capacity`. Fixed once created." + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "description": "Plain text about the event type, shown on hosted booking pages and never in emails. Links, and anything shaped like a domain, must be on your organization's verified domains or their subdomains, and use `https`. `null` removes it." + }, + "title": { "type": "string", "minLength": 1, - "maxLength": 500, - "description": "Why it's being cancelled. Only retrieving this booking returns it: it's never in webhooks, logs or emails." - } - }, - "additionalProperties": false, - "examples": [ - { - "reason": "Feeling better" - } - ] - }, - "SchedulingBookingEventList": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "list" - ] + "maxLength": 120, + "description": "Shown to attendees and in emails. Host calendars show it only with `host_calendar_details: full`." }, - "data": { + "duration_minutes": { + "type": "integer", + "minimum": 5, + "maximum": 1440, + "description": "How long a booking lasts by default, in minutes." + }, + "duration_options": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingBookingEvent" - } + "type": "integer", + "minimum": 5, + "maximum": 1440 + }, + "maxItems": 10, + "description": "Other lengths an attendee may choose, in minutes. Empty: only `duration_minutes`." }, - "has_more": { - "type": "boolean" - } - }, - "required": [ - "object", - "data", - "has_more" - ], - "examples": [ - { - "object": "list", - "data": [ - { - "object": "scheduling.booking_event", - "id": "bevt_01kd2m9a0b1c2d3e4f5g6h7j8m", - "sequence": 1, - "type": "scheduling.booking.confirmed", - "actor": { - "kind": "api_key", - "id": "key_01kd2m9a0b1c2d3e4f5g6h7j8m" - }, - "details": {}, - "occurred_at": "2027-01-02T09:00:00.000Z" - } + "step_minutes": { + "type": "integer", + "minimum": 5, + "maximum": 1440, + "description": "How far apart slots start. Default: the duration." + }, + "grid_time_zone": { + "type": [ + "string", + "null" ], - "has_more": false - } - ] - }, - "SchedulingBookingEvent": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "scheduling.booking_event" + "minLength": 1, + "maxLength": 64, + "description": "The zone slots align in. Default: each host's own.", + "examples": [ + "America/New_York" ] }, - "id": { - "type": "string" - }, - "sequence": { - "type": "number", - "description": "This booking's events count up from 1." - }, - "type": { - "type": "string", - "description": "What happened, such as `scheduling.booking.confirmed`, `scheduling.booking.rescheduled` or `scheduling.booking.cancelled`." + "buffer_before_minutes": { + "type": "integer", + "minimum": 0, + "maximum": 480, + "description": "Minutes kept free before each booking. Default: 0." }, - "actor": { - "type": "object", - "properties": { - "kind": { - "type": "string", - "enum": [ - "api_key", - "user", - "host_session", - "booker_session", - "manage_session", - "system" - ] - }, - "id": { - "type": [ - "string", - "null" - ], - "description": "The API key, user or session's id. `null` for `system`." - } - }, - "required": [ - "kind", - "id" - ], - "description": "Who did it." + "buffer_after_minutes": { + "type": "integer", + "minimum": 0, + "maximum": 480, + "description": "Minutes kept free after each booking. Default: 0." }, - "details": { - "type": "object", - "additionalProperties": {}, - "description": "More about it, such as `previous_start` on a reschedule. Never personal data." + "min_notice_minutes": { + "type": "integer", + "minimum": 0, + "maximum": 525600, + "description": "Slots starting sooner than this many minutes from now aren't offered. Default: 0." }, - "occurred_at": { - "type": "string", - "description": "When it happened." - } - }, - "required": [ - "object", - "id", - "sequence", - "type", - "actor", - "details", - "occurred_at" - ] - }, - "SchedulingBookingNotificationList": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "list" - ] + "booking_window": { + "$ref": "#/components/schemas/SchedulingBookingWindow" }, - "data": { + "limits": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingBookingNotification" - } + "$ref": "#/components/schemas/SchedulingLimit" + }, + "maxItems": 4, + "description": "Caps on bookings per day or week. Default: none." }, - "has_more": { - "type": "boolean" - } - }, - "required": [ - "object", - "data", - "has_more" - ], - "examples": [ - { - "object": "list", - "data": [ - { - "object": "scheduling.booking_notification", - "id": "sntf_01kd2m9a0b1c2d3e4f5g6h7j8m", - "kind": "reminder", - "reminder_key": "start-1440", - "channel": "email", - "recipient": "attendee", - "due_at": "2027-01-03T15:00:00.000Z", - "finished_at": null, - "outcome": null - } + "capacity": { + "type": [ + "integer", + "null" ], - "has_more": false - } - ] - }, - "SchedulingBookingNotification": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "scheduling.booking_notification" - ] + "minimum": 2, + "maximum": 10000, + "description": "Attendees per session. Required for, and only allowed on, `group`." }, - "id": { - "type": "string" + "requires_approval": { + "type": "boolean", + "description": "Bookings start as `pending_approval`, holding the time until approved or declined. Default: `false`." }, - "kind": { - "type": "string", - "description": "`reminder` for an entry of `reminders`; otherwise a standard notification, or `started`/`ended`." + "reassignment_enabled": { + "type": "boolean", + "description": "Whether bookings of this event type can go to another host. Default: true." }, - "reminder_key": { - "type": [ - "string", - "null" - ] + "approval_ttl_minutes": { + "type": "integer", + "minimum": 5, + "maximum": 43200, + "description": "How long a booking can wait for approval before it's declined, in minutes. Default: 1440." }, - "channel": { - "type": "string", - "enum": [ - "email", - "sms", - "webhook" - ] + "hold_ttl_seconds": { + "type": "integer", + "minimum": 60, + "maximum": 86400, + "description": "How long a hold keeps its slot, in seconds. Default: 600." }, - "recipient": { - "type": "string", - "enum": [ - "attendee", - "host", - "endpoint", - "address" - ] + "cancel_notice_minutes": { + "type": "integer", + "minimum": 0, + "maximum": 525600, + "description": "Attendees can't cancel closer to the start than this. API keys can." }, - "due_at": { - "type": "string" + "reschedule_notice_minutes": { + "type": "integer", + "minimum": 0, + "maximum": 525600, + "description": "Attendees can't reschedule closer to the start than this. API keys can. Default: 0." }, - "finished_at": { - "type": [ - "string", - "null" - ] + "max_guests": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "How many people an attendee can add to a booking. Default: 0. Guests are emailed about it. Not on `group`." }, - "outcome": { + "max_active_bookings_per_attendee": { "type": [ - "string", + "integer", "null" ], - "description": "`null` while it's waiting. `sent`, or why not: `too_late`, `filtered`, `no_show`, `no_address`, `no_sender`, `stale`, `superseded`, `failed`." - } - }, - "required": [ - "object", - "id", - "kind", - "reminder_key", - "channel", - "recipient", - "due_at", - "finished_at", - "outcome" - ], - "examples": [ - { - "object": "scheduling.booking_notification", - "id": "sntf_01kd2m9a0b1c2d3e4f5g6h7j8m", - "kind": "reminder", - "reminder_key": "start-1440", - "channel": "email", - "recipient": "attendee", - "due_at": "2027-01-03T15:00:00.000Z", - "finished_at": null, - "outcome": null - } - ] - }, - "UpdateBookingLocationRequest": { - "type": "object", - "properties": { - "location": { - "$ref": "#/components/schemas/SchedulingLocation" - } - }, - "required": [ - "location" - ], - "additionalProperties": false, - "examples": [ - { - "location": { - "type": "link", - "url": "https://meet.example.com/abc" - } - } - ] - }, - "CreateHoldRequest": { - "type": "object", - "properties": { - "event_type": { - "type": "string", - "description": "The event type's id or uid." + "minimum": 1, + "maximum": 1000, + "description": "How many upcoming bookings of this event type one person can have at a time, known by `uid` or email. Default: `null`, no limit. Past this, booking answers `attendee_limit_reached`. Guests don't count, and each occurrence of a series does." }, - "start": { + "host_calendar_details": { "type": "string", - "description": "An ISO 8601 instant with an offset.", - "examples": [ - "2027-01-04T15:00:00.000Z" - ] + "enum": [ + "neutral", + "full" + ], + "description": "What hosts' calendars say about a booking. `neutral` (the default): a private event titled \"Appointment\". `full`: a private event with this title and the attendee's name, plus names, emails and phone numbers in its description, which go to the host's calendar provider. Never answers. Refused in HIPAA mode." }, - "host": { - "type": "string", - "description": "A specific host (id or uid). Default: chosen fairly from those free." + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingQuestion" + }, + "maxItems": 25, + "description": "What attendees are asked when booking. Keys must be unique." }, - "attributes": { - "type": "object", + "locations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingLocation" + }, + "maxItems": 10, + "description": "Where bookings can meet. A browser only chooses among these. A `group` event type can't use `zoom`." + }, + "host_filter": { + "type": [ + "object", + "null" + ], "additionalProperties": { "anyOf": [ { @@ -21872,522 +22588,596 @@ } ] }, - "description": "Only choose among pool hosts matching all of these." - }, - "duration_minutes": { - "type": "integer", - "minimum": 5, - "maximum": 1440, - "description": "One of the event type's `duration_options`." + "description": "Pool members also include active hosts matching all of these attributes." }, - "attendee": { - "$ref": "#/components/schemas/SchedulingAttendeeInput" + "notifications": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "object", + "properties": { + "email_attendee": { + "type": "boolean" + }, + "email_host": { + "type": "boolean" + }, + "sms_attendee": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "description": "Per event, overriding your settings for this event type." }, - "guests": { - "type": "array", + "reminders": { + "type": [ + "array", + "null" + ], "items": { - "$ref": "#/components/schemas/SchedulingGuestInput" + "$ref": "#/components/schemas/SchedulingReminderInput" }, "maxItems": 10, - "description": "Up to the event type's `max_guests`." + "description": "Replaces your settings' `reminders` for this event type. `null` inherits them; an empty list means none." }, - "answers": { + "metadata": { "type": "object", "additionalProperties": { - "anyOf": [ - { - "type": "string", - "maxLength": 5000 - }, - { - "type": "array", - "items": { - "type": "string", - "maxLength": 500 - }, - "maxItems": 50 - } - ] + "type": "string", + "maxLength": 500 }, - "description": "Answers to the event type's questions. Stored encrypted, and returned only by retrieving the booking." + "description": "Your own key-value pairs." }, - "location": { - "oneOf": [ + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingEventTypeHostInput" + }, + "maxItems": 500, + "default": [], + "description": "Who can be booked. `one_on_one` and `group` take one; `collective` needs at least one `required` host." + } + }, + "required": [ + "title", + "duration_minutes" + ], + "additionalProperties": false, + "examples": [ + { + "title": "Initial consultation", + "kind": "one_on_one", + "duration_minutes": 30, + "buffer_after_minutes": 10, + "min_notice_minutes": 120, + "hosts": [ { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "link" - ] - }, - "url": { - "type": [ - "string", - "null" - ], - "maxLength": 2000, - "format": "uri", - "default": null, - "description": "The meeting link. `null` on an event type means the booking supplies its own." - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "phone" - ] - }, - "number": { - "type": [ - "string", - "null" - ], - "maxLength": 32, - "default": null, - "description": "The phone number to call. `null` on an event type means the booking supplies its own." - } - }, - "required": [ - "type" - ], - "additionalProperties": false + "host": "doctor_482" + } + ] + } + ] + }, + "SchedulingReminderInput": { + "type": "object", + "properties": { + "key": { + "type": "string", + "pattern": "^[a-z0-9_-]{1,64}$", + "description": "Names this reminder in webhooks and on the booking. Default: from its timing, like `start-1440`." + }, + "anchor": { + "type": "string", + "enum": [ + "start", + "end", + "confirmed", + "rescheduled", + "cancelled", + "declined", + "pending_approval", + "no_show" + ], + "description": "The moment this is timed from." + }, + "before_minutes": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 525600, + "description": "How long before the anchor. Only for `start` and `end`." + }, + "after_minutes": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 86400, + "description": "How long after the anchor, up to 60 days. Default for other anchors: 0." + }, + "send": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email_attendee", + "email_host", + "sms_attendee", + "sms_host", + "webhook" + ] + }, + "maxItems": 5, + "description": "Who hears about it. `webhook` fires `scheduling.booking.reminder`." + }, + "email_to": { + "type": "array", + "items": { + "type": "string", + "maxLength": 320, + "format": "email" + }, + "maxItems": 5, + "description": "Fixed addresses to email as well: verified members of your organization, or addresses on your verified domains (live environments)." + }, + "subject": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 200, + "description": "Your own subject, with `{{variables}}`. Needs `email_from`." + }, + "message": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 2000, + "description": "Your own words in place of the standard first line, with `{{variables}}` and no links. The booking's details stay below it. Needs `email_from`." + }, + "sms_message": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 320, + "description": "Your own text, with `{{variables}}` other than names, and no links. `Reply STOP to opt out.` is always added." + }, + "button": { + "type": [ + "object", + "null" + ], + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 40 }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "in_person" - ] - }, - "address": { - "type": [ - "string", - "null" - ], - "maxLength": 500, - "default": null, - "description": "The address to meet at. `null` on an event type means the booking supplies its own." - } - }, - "required": [ - "type" + "url": { + "type": "string", + "minLength": 1, + "maxLength": 500, + "description": "https, on one of your verified domains (live environments). May use `{{booking_id}}` in its path or query.", + "examples": [ + "https://example.com/feedback?booking={{booking_id}}" + ] + } + }, + "required": [ + "label", + "url" + ], + "additionalProperties": false, + "description": "One button in the email. Needs `email_from`." + }, + "include_invite": { + "type": "boolean", + "description": "Attach the calendar invitation to the emails." + }, + "send_if_late": { + "type": "boolean", + "description": "If it was booked after this reminder's moment, send it right away instead of skipping it, as long as the meeting is still ahead. Only for `before_minutes` on `start`." + }, + "skip_no_show": { + "type": "boolean", + "description": "Don't send it once the booking is marked a no-show. Default: true on `end`." + }, + "only_if": { + "type": [ + "object", + "null" + ], + "properties": { + "metadata": { + "type": [ + "object", + "null" ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "google_meet" - ] - } + "additionalProperties": { + "type": "string", + "maxLength": 500 }, - "required": [ - "type" - ], - "additionalProperties": false + "description": "The booking's metadata must have all of these." }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "microsoft_teams" - ] - } - }, - "required": [ - "type" + "location_types": { + "type": [ + "array", + "null" ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "zoom" - ] - }, - "url": { - "type": [ - "string", - "null" - ], - "maxLength": 2000, - "format": "uri", - "default": null, - "description": "The meeting's link, once it's created on the host's Zoom account." - } + "items": { + "type": "string", + "enum": [ + "link", + "phone", + "in_person", + "google_meet", + "microsoft_teams", + "zoom" + ] }, - "required": [ - "type" - ], - "additionalProperties": false + "minItems": 1, + "maxItems": 6, + "description": "The booking's location must be one of these." } - ], - "description": "One of the event type's `locations`, with your link filled in if it has none." - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string", - "maxLength": 500 }, - "description": "Your own key-value pairs. Don't put sensitive data here." + "additionalProperties": false, + "description": "Only for bookings matching all of this, checked when it's due." } }, "required": [ - "event_type", - "start", - "attendee" + "anchor" ], - "additionalProperties": false, - "examples": [ - { - "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", - "start": "2027-01-04T15:00:00.000Z", - "attendee": { - "uid": "patient_77", - "time_zone": "UTC" - } - } - ] + "additionalProperties": false }, - "ConfirmHoldRequest": { + "SchedulingEventTypeHostInput": { "type": "object", "properties": { - "if_still_free": { - "type": "boolean", - "default": false, - "description": "After the hold expired, book anyway if nobody else took the slot. Default: refuse with `hold_expired`." + "host": { + "type": "string", + "description": "The host's id or uid." + }, + "role": { + "type": "string", + "enum": [ + "required", + "member" + ], + "default": "member", + "description": "`required` hosts must all be free; one `member` is chosen for a booking." + }, + "weight": { + "type": "integer", + "minimum": 1, + "maximum": 10000, + "default": 100, + "description": "Share of a pool's bookings relative to other members." + }, + "schedule": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "A schedule of this host's. Default: their default schedule." } }, - "additionalProperties": false, - "examples": [ - { - "if_still_free": true - } - ] + "required": [ + "host" + ], + "additionalProperties": false }, - "SchedulingSeries": { + "SchedulingEventTypeList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "scheduling.series" + "list" ] }, - "id": { - "type": "string" - }, - "bookings": { + "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingBooking" - }, - "description": "The occurrences booked, each an ordinary booking." + "$ref": "#/components/schemas/SchedulingEventType" + } }, - "conflicts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "index": { - "type": "number", - "description": "The occurrence's place in the series, from 0." - }, - "start": { - "type": "string", - "description": "When it would have started." - } - }, - "required": [ - "index", - "start" - ] - }, - "description": "With `on_conflict: skip`, the occurrences that weren't free and weren't booked." + "has_more": { + "type": "boolean" } }, "required": [ "object", - "id", - "bookings", - "conflicts" + "data", + "has_more" ], "examples": [ { - "object": "scheduling.series", - "id": "series_01kd2m9a0b1c2d3e4f5g6h7j8m", - "bookings": [ + "object": "list", + "data": [ { - "object": "scheduling.booking", - "id": "book_01kd2m9a0b1c2d3e4f5g6h7j8m", - "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", - "status": "confirmed", + "object": "scheduling.event_type", + "id": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "uid": null, "title": "Initial consultation", - "start": "2027-01-04T15:00:00.000Z", - "end": "2027-01-04T15:30:00.000Z", + "description": null, + "kind": "one_on_one", + "status": "active", + "duration_minutes": 30, + "duration_options": [], + "step_minutes": 30, + "grid_time_zone": null, + "buffer_before_minutes": 0, + "buffer_after_minutes": 10, + "min_notice_minutes": 120, + "booking_window": null, + "limits": [], + "capacity": null, + "requires_approval": false, + "reassignment_enabled": true, + "approval_ttl_minutes": 1440, + "hold_ttl_seconds": 600, + "cancel_notice_minutes": 0, + "reschedule_notice_minutes": 0, + "max_guests": 0, + "max_active_bookings_per_attendee": null, + "host_calendar_details": "neutral", + "questions": [], + "locations": [], + "host_filter": null, + "notifications": null, + "reminders": null, "hosts": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" - ], - "attendees": [ { - "id": "attn_01kd2m9a0b1c2d3e4f5g6h7j8m", - "role": "attendee", - "uid": "patient_77", - "name": "Sam Lee", - "email": "sam@example.com", - "phone": null, - "time_zone": "UTC", - "locale": null + "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "role": "member", + "weight": 100, + "schedule": null } ], - "location": null, - "hold_expires_at": null, - "approval_expires_at": null, - "series": null, - "series_index": null, - "group_session": null, - "calendar_unverified": false, - "external_conflict_at": null, - "no_show_at": null, - "cancelled_at": null, - "cancelled_by": null, "metadata": {}, "created_at": "2027-01-01T10:00:00.000Z", "updated_at": "2027-01-01T10:00:00.000Z" } ], - "conflicts": [] + "has_more": false } ] }, - "CreateSeriesRequest": { + "UpdateEventTypeRequest": { "type": "object", "properties": { - "event_type": { - "type": "string", - "description": "The event type's id or uid." + "uid": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 255, + "description": "Your own id for this event type. `null` removes it." }, - "start": { - "type": "string", - "description": "An ISO 8601 instant with an offset.", - "examples": [ - "2027-01-04T15:00:00.000Z" - ] + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "description": "Plain text about the event type, shown on hosted booking pages and never in emails. Links, and anything shaped like a domain, must be on your organization's verified domains or their subdomains, and use `https`. `null` removes it." }, - "host": { + "title": { "type": "string", - "description": "A specific host (id or uid). Default: chosen fairly from those free." - }, - "attributes": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "string", - "maxLength": 200 - }, - { - "type": "array", - "items": { - "type": "string", - "maxLength": 200 - }, - "maxItems": 50 - } - ] - }, - "description": "Only choose among pool hosts matching all of these." + "minLength": 1, + "maxLength": 120, + "description": "Shown to attendees and in emails. Host calendars show it only with `host_calendar_details: full`." }, "duration_minutes": { "type": "integer", "minimum": 5, "maximum": 1440, - "description": "One of the event type's `duration_options`." + "description": "How long a booking lasts by default, in minutes." }, - "attendee": { - "$ref": "#/components/schemas/SchedulingAttendeeInput" + "duration_options": { + "type": "array", + "items": { + "type": "integer", + "minimum": 5, + "maximum": 1440 + }, + "maxItems": 10, + "description": "Other lengths an attendee may choose, in minutes. Empty: only `duration_minutes`." }, - "guests": { + "step_minutes": { + "type": "integer", + "minimum": 5, + "maximum": 1440, + "description": "How far apart slots start. Default: the duration." + }, + "grid_time_zone": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 64, + "description": "The zone slots align in. Default: each host's own.", + "examples": [ + "America/New_York" + ] + }, + "buffer_before_minutes": { + "type": "integer", + "minimum": 0, + "maximum": 480, + "description": "Minutes kept free before each booking. Default: 0." + }, + "buffer_after_minutes": { + "type": "integer", + "minimum": 0, + "maximum": 480, + "description": "Minutes kept free after each booking. Default: 0." + }, + "min_notice_minutes": { + "type": "integer", + "minimum": 0, + "maximum": 525600, + "description": "Slots starting sooner than this many minutes from now aren't offered. Default: 0." + }, + "booking_window": { + "$ref": "#/components/schemas/SchedulingBookingWindow" + }, + "limits": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingGuestInput" + "$ref": "#/components/schemas/SchedulingLimit" + }, + "maxItems": 4, + "description": "Caps on bookings per day or week. Default: none." + }, + "capacity": { + "type": [ + "integer", + "null" + ], + "minimum": 2, + "maximum": 10000, + "description": "Attendees per session. Required for, and only allowed on, `group`." + }, + "requires_approval": { + "type": "boolean", + "description": "Bookings start as `pending_approval`, holding the time until approved or declined. Default: `false`." + }, + "reassignment_enabled": { + "type": "boolean", + "description": "Whether bookings of this event type can go to another host. Default: true." + }, + "approval_ttl_minutes": { + "type": "integer", + "minimum": 5, + "maximum": 43200, + "description": "How long a booking can wait for approval before it's declined, in minutes. Default: 1440." + }, + "hold_ttl_seconds": { + "type": "integer", + "minimum": 60, + "maximum": 86400, + "description": "How long a hold keeps its slot, in seconds. Default: 600." + }, + "cancel_notice_minutes": { + "type": "integer", + "minimum": 0, + "maximum": 525600, + "description": "Attendees can't cancel closer to the start than this. API keys can." + }, + "reschedule_notice_minutes": { + "type": "integer", + "minimum": 0, + "maximum": 525600, + "description": "Attendees can't reschedule closer to the start than this. API keys can. Default: 0." + }, + "max_guests": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "How many people an attendee can add to a booking. Default: 0. Guests are emailed about it. Not on `group`." + }, + "max_active_bookings_per_attendee": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 1000, + "description": "How many upcoming bookings of this event type one person can have at a time, known by `uid` or email. Default: `null`, no limit. Past this, booking answers `attendee_limit_reached`. Guests don't count, and each occurrence of a series does." + }, + "host_calendar_details": { + "type": "string", + "enum": [ + "neutral", + "full" + ], + "description": "What hosts' calendars say about a booking. `neutral` (the default): a private event titled \"Appointment\". `full`: a private event with this title and the attendee's name, plus names, emails and phone numbers in its description, which go to the host's calendar provider. Never answers. Refused in HIPAA mode." + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingQuestion" + }, + "maxItems": 25, + "description": "What attendees are asked when booking. Keys must be unique." + }, + "locations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingLocation" }, "maxItems": 10, - "description": "Up to the event type's `max_guests`." + "description": "Where bookings can meet. A browser only chooses among these. A `group` event type can't use `zoom`." }, - "answers": { - "type": "object", + "host_filter": { + "type": [ + "object", + "null" + ], "additionalProperties": { "anyOf": [ { "type": "string", - "maxLength": 5000 + "maxLength": 200 }, { "type": "array", "items": { "type": "string", - "maxLength": 500 + "maxLength": 200 }, "maxItems": 50 } ] }, - "description": "Answers to the event type's questions. Stored encrypted, and returned only by retrieving the booking." + "description": "Pool members also include active hosts matching all of these attributes." }, - "location": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "link" - ] - }, - "url": { - "type": [ - "string", - "null" - ], - "maxLength": 2000, - "format": "uri", - "default": null, - "description": "The meeting link. `null` on an event type means the booking supplies its own." - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "phone" - ] - }, - "number": { - "type": [ - "string", - "null" - ], - "maxLength": 32, - "default": null, - "description": "The phone number to call. `null` on an event type means the booking supplies its own." - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "in_person" - ] - }, - "address": { - "type": [ - "string", - "null" - ], - "maxLength": 500, - "default": null, - "description": "The address to meet at. `null` on an event type means the booking supplies its own." - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "google_meet" - ] - } + "notifications": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "object", + "properties": { + "email_attendee": { + "type": "boolean" }, - "required": [ - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "microsoft_teams" - ] - } + "email_host": { + "type": "boolean" }, - "required": [ - "type" - ], - "additionalProperties": false + "sms_attendee": { + "type": "boolean" + } }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "zoom" - ] - }, - "url": { - "type": [ - "string", - "null" - ], - "maxLength": 2000, - "format": "uri", - "default": null, - "description": "The meeting's link, once it's created on the host's Zoom account." - } - }, - "required": [ - "type" - ], - "additionalProperties": false - } + "additionalProperties": false + }, + "description": "Per event, overriding your settings for this event type." + }, + "reminders": { + "type": [ + "array", + "null" ], - "description": "One of the event type's `locations`, with your link filled in if it has none." + "items": { + "$ref": "#/components/schemas/SchedulingReminderInput" + }, + "maxItems": 10, + "description": "Replaces your settings' `reminders` for this event type. `null` inherits them; an empty list means none." }, "metadata": { "type": "object", @@ -22395,74 +23185,33 @@ "type": "string", "maxLength": 500 }, - "description": "Your own key-value pairs. Don't put sensitive data here." - }, - "recurrence": { - "type": "object", - "properties": { - "frequency": { - "type": "string", - "enum": [ - "weekly", - "monthly" - ], - "description": "Repeat weekly or monthly." - }, - "interval": { - "type": "integer", - "minimum": 1, - "maximum": 12, - "default": 1, - "description": "Every this many weeks or months. Default: 1." - }, - "count": { - "type": "integer", - "minimum": 2, - "maximum": 52, - "description": "Occurrences in total, the first included." - } - }, - "required": [ - "frequency", - "count" - ], - "additionalProperties": false, - "description": "How often the booking repeats, at the attendee's local time." + "description": "Your own key-value pairs." }, - "on_conflict": { + "status": { "type": "string", "enum": [ - "reject", - "skip" + "active", + "archived" ], - "default": "reject", - "description": "If an occurrence isn't free: book nothing, or book the rest and report it." + "description": "`archived` stops new bookings, and keeps existing ones." + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingEventTypeHostInput" + }, + "maxItems": 500, + "description": "Replaces the assigned hosts. Removed hosts keep their existing bookings." } }, - "required": [ - "event_type", - "start", - "attendee", - "recurrence" - ], "additionalProperties": false, "examples": [ { - "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", - "start": "2027-01-04T15:00:00.000Z", - "attendee": { - "uid": "patient_77", - "time_zone": "America/New_York" - }, - "recurrence": { - "frequency": "weekly", - "interval": 1, - "count": 12 - } + "min_notice_minutes": 240 } ] }, - "SchedulingSeriesPreview": { + "SchedulingSlotList": { "type": "object", "properties": { "object": { @@ -22474,39 +23223,8 @@ "data": { "type": "array", "items": { - "type": "object", - "properties": { - "start": { - "type": "string", - "description": "When this occurrence starts." - }, - "available": { - "type": "boolean", - "description": "Whether it's free." - }, - "reason": { - "type": [ - "string", - "null" - ], - "description": "Why it isn't free, such as `host_busy`, `already_claimed`, `outside_availability` or `no_hosts`. `null` when it is." - }, - "hosts": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The ids of the hosts who could take it." - } - }, - "required": [ - "start", - "available", - "reason", - "hosts" - ] - }, - "description": "Each occurrence the request would book. Nothing is booked." + "$ref": "#/components/schemas/SchedulingSlot" + } }, "has_more": { "type": "boolean", @@ -22526,424 +23244,153 @@ "data": [ { "start": "2027-01-04T15:00:00.000Z", - "available": true, - "reason": null, + "end": "2027-01-04T15:30:00.000Z", "hosts": [ "host_01kd2m9a0b1c2d3e4f5g6h7j8m" - ] + ], + "seats_left": null } ], "has_more": false } ] }, - "SchedulingCancelledSeries": { + "SchedulingSlot": { "type": "object", "properties": { - "object": { + "start": { "type": "string", - "enum": [ - "list" - ] + "description": "When the slot starts." }, - "data": { + "end": { + "type": "string", + "description": "When the slot ends." + }, + "hosts": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingBooking" - } + "type": "string" + }, + "description": "Who could take it." }, - "has_more": { - "type": "boolean", - "enum": [ - false - ] + "seats_left": { + "type": [ + "number", + "null" + ], + "description": "Free seats at this start, for a `group` event type. `null` for the others." } }, "required": [ - "object", - "data", - "has_more" - ], - "examples": [ - { - "object": "list", - "data": [ - { - "object": "scheduling.booking", - "id": "book_01kd2m9a0b1c2d3e4f5g6h7j8m", - "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", - "status": "confirmed", - "title": "Initial consultation", - "start": "2027-01-04T15:00:00.000Z", - "end": "2027-01-04T15:30:00.000Z", - "hosts": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" - ], - "attendees": [ - { - "id": "attn_01kd2m9a0b1c2d3e4f5g6h7j8m", - "role": "attendee", - "uid": "patient_77", - "name": "Sam Lee", - "email": "sam@example.com", - "phone": null, - "time_zone": "UTC", - "locale": null - } - ], - "location": null, - "hold_expires_at": null, - "approval_expires_at": null, - "series": null, - "series_index": null, - "group_session": null, - "calendar_unverified": false, - "external_conflict_at": null, - "no_show_at": null, - "cancelled_at": null, - "cancelled_by": null, - "metadata": {}, - "created_at": "2027-01-01T10:00:00.000Z", - "updated_at": "2027-01-01T10:00:00.000Z" - } - ], - "has_more": false - } + "start", + "end", + "hosts", + "seats_left" ] }, - "CancelSeriesRequest": { - "type": "object", - "properties": { - "from_index": { - "type": "integer", - "minimum": 0, - "default": 0, - "description": "Cancel this occurrence and those after it. Default: all of them." - } - }, - "additionalProperties": false, - "examples": [ - { - "from_index": 4 - } - ] - }, - "SchedulingAttendeeErasure": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "scheduling.attendee_erasure" - ] - }, - "uid": { - "type": "string", - "description": "The attendee's uid." - }, - "erased": { - "type": "number", - "description": "How many attendee records were erased, one per booking." - } - }, - "required": [ - "object", - "uid", - "erased" - ], - "examples": [ - { - "object": "scheduling.attendee_erasure", - "uid": "patient_77", - "erased": 3 - } - ] - }, - "SchedulingAttendeeErasureByEmail": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "scheduling.attendee_erasure" - ] - }, - "erased": { - "type": "number", - "description": "How many attendee and guest records were erased." - } - }, - "required": [ - "object", - "erased" - ], - "examples": [ - { - "object": "scheduling.attendee_erasure", - "erased": 3 - } - ] - }, - "EraseSchedulingAttendeeByEmailRequest": { - "type": "object", - "properties": { - "email": { - "type": "string", - "maxLength": 320, - "format": "email", - "description": "The address to forget, an attendee's or a guest's." - } - }, - "required": [ - "email" - ], - "additionalProperties": false, - "examples": [ - { - "email": "sam@example.com" - } - ] - }, - "SchedulingCalendarAuthorization": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "scheduling.calendar_authorization" - ] - }, - "url": { - "type": "string", - "description": "Open it in a new window: providers refuse to be framed." - } - }, - "required": [ - "object", - "url" - ], - "examples": [ - { - "object": "scheduling.calendar_authorization", - "url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=abc" - } - ] - }, - "AuthorizeCalendarRequest": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "enum": [ - "google", - "microsoft", - "zoom" - ], - "description": "Whose consent page to open. CalDAV connects with a password instead." - }, - "return_url": { - "type": "string", - "maxLength": 2000, - "format": "uri", - "description": "Where to send the browser afterwards, on one of the environment's allowed origins. `coss_calendar` says what happened: `connected`, `denied` or `invalid`." - } - }, - "required": [ - "provider" - ], - "additionalProperties": false, - "examples": [ - { - "provider": "google" - } - ] - }, - "SchedulingCalendarConnection": { + "SchedulingSlotExplanation": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "scheduling.calendar_connection" + "scheduling.slot_explanation" ] }, - "id": { - "type": "string" - }, - "host": { - "type": "string", - "description": "The id of the host it belongs to." - }, - "provider": { + "start": { "type": "string", - "enum": [ - "google", - "microsoft", - "caldav", - "simulated", - "zoom" - ], - "description": "Where the calendar lives. `simulated` exists only in sandbox environments; `zoom` is only used for meetings, never read for busy times." + "description": "The start asked about." }, - "account": { + "end": { "type": "string", - "description": "The provider's account, such as the Google address." + "description": "When it would end." }, - "status": { - "type": "string", - "enum": [ - "active", - "needs_reauth" - ], - "description": "`needs_reauth`: access was lost, and this host isn't offered until it's back." + "available": { + "type": "boolean", + "description": "Whether it can be booked right now." }, - "delegation": { + "reason": { "type": [ "string", "null" ], - "description": "The calendar delegation that manages this connection. It can't be disconnected or reconnected by itself." + "description": "Why it can't: `not_on_grid`, `too_soon`, `outside_booking_window`, `out_of_office`, `outside_coverage`, `calendar_unverified`, `outside_availability`, `host_busy`, `already_claimed`, `limit_reached` or `session_full`, or `event_type_inactive` or `no_hosts`. `null` when it can. With several hosts refused, the first one's reason: see `hosts`." }, - "calendars": { + "hosts": { "type": "array", "items": { "type": "object", "properties": { - "id": { - "type": "string" + "host": { + "type": "string", + "description": "The host's id." }, - "external_id": { + "role": { "type": "string", - "description": "The calendar's id at the provider." + "enum": [ + "required", + "member" + ], + "description": "`required` hosts must all be free. Only one `member` needs to be." }, - "read_busy": { + "available": { "type": "boolean", - "description": "Whether its busy times block slots." + "description": "Whether this host could take it." }, - "write_target": { - "type": "boolean", - "description": "Whether bookings are written to it." + "reason": { + "type": [ + "string", + "null" + ], + "description": "Why not: `not_on_grid`, `too_soon`, `outside_booking_window`, `out_of_office`, `outside_coverage`, `calendar_unverified`, `outside_availability`, `host_busy`, `already_claimed`, `limit_reached` or `session_full`. `null` when they could." }, - "verified_at": { + "calendar_reason": { "type": [ "string", "null" ], - "description": "When it was last read successfully. `null`: not read yet." + "description": "With `calendar_unverified`: `calendar_needs_reauth`, `calendar_stale` or `calendar_not_covered`." } }, "required": [ - "id", - "external_id", - "read_busy", - "write_target", - "verified_at" + "host", + "role", + "available", + "reason", + "calendar_reason" ] }, - "description": "The account's calendars." - }, - "created_at": { - "type": "string" + "description": "Everyone who could take this event type, narrowed by the query." } }, "required": [ "object", - "id", - "host", - "provider", - "account", - "status", - "delegation", - "calendars", - "created_at" + "start", + "end", + "available", + "reason", + "hosts" ], "examples": [ { - "object": "scheduling.calendar_connection", - "id": "calconn_01kd2m9a0b1c2d3e4f5g6h7j8m", - "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", - "provider": "google", - "account": "ana@example.com", - "status": "active", - "delegation": null, - "calendars": [ + "object": "scheduling.slot_explanation", + "start": "2027-01-04T15:00:00.000Z", + "end": "2027-01-04T15:30:00.000Z", + "available": false, + "reason": "host_busy", + "hosts": [ { - "id": "cal_01kd2m9a0b1c2d3e4f5g6h7j8m", - "external_id": "ana@example.com", - "read_busy": true, - "write_target": true, - "verified_at": "2027-01-01T10:00:00.000Z" - } - ], - "created_at": "2027-01-01T10:00:00.000Z" - } - ] - }, - "CreateCalendarConnectionRequest": { - "oneOf": [ - { - "type": "object", - "properties": { - "provider": { - "type": "string", - "enum": [ - "caldav" - ] - }, - "url": { - "type": "string", - "maxLength": 2000, - "format": "uri", - "description": "The server's https URL." - }, - "username": { - "type": "string", - "minLength": 1, - "maxLength": 320, - "description": "The account's username on that server." - }, - "password": { - "type": "string", - "minLength": 1, - "maxLength": 500, - "description": "An app-specific password. Encrypted, and never returned." - } - }, - "required": [ - "provider", - "url", - "username", - "password" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "provider": { - "type": "string", - "enum": [ - "simulated" - ] + "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "role": "member", + "available": false, + "reason": "host_busy", + "calendar_reason": null } - }, - "required": [ - "provider" - ], - "additionalProperties": false - } - ], - "description": "`caldav` connects a server with an app-specific password; `simulated` is a sandbox calendar for tests.", - "examples": [ - { - "provider": "caldav", - "url": "https://caldav.icloud.com", - "username": "ana@example.com", - "password": "abcd-efgh-ijkl-mnop" + ] } ] }, - "SchedulingCalendarConnectionList": { + "SchedulingAvailabilitySearchResult": { "type": "object", "properties": { "object": { @@ -22955,11 +23402,52 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingCalendarConnection" - } + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "The host's id." + }, + "uid": { + "type": [ + "string", + "null" + ], + "description": "The host's own uid. `null` when none is set." + }, + "slots": { + "type": "array", + "items": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "When the slot starts." + }, + "end": { + "type": "string", + "description": "When the slot ends." + } + }, + "required": [ + "start", + "end" + ] + }, + "description": "The host's soonest slots." + } + }, + "required": [ + "host", + "uid", + "slots" + ] + }, + "description": "Each host with availability, soonest first." }, "has_more": { - "type": "boolean" + "type": "boolean", + "description": "Whether more hosts matched than `limit` returned." } }, "required": [ @@ -22972,308 +23460,408 @@ "object": "list", "data": [ { - "object": "scheduling.calendar_connection", - "id": "calconn_01kd2m9a0b1c2d3e4f5g6h7j8m", "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", - "provider": "google", - "account": "ana@example.com", - "status": "active", - "delegation": null, - "calendars": [ + "uid": "doctor_482", + "slots": [ { - "id": "cal_01kd2m9a0b1c2d3e4f5g6h7j8m", - "external_id": "ana@example.com", - "read_busy": true, - "write_target": true, - "verified_at": "2027-01-01T10:00:00.000Z" + "start": "2027-01-04T15:00:00.000Z", + "end": "2027-01-04T15:30:00.000Z" } - ], - "created_at": "2027-01-01T10:00:00.000Z" + ] } ], "has_more": false } ] }, - "DeletedSchedulingCalendarConnection": { + "AvailabilitySearchRequest": { "type": "object", "properties": { - "object": { + "event_type": { "type": "string", - "enum": [ - "scheduling.calendar_connection" - ] + "description": "The event type's id or uid." }, - "id": { - "type": "string" + "start": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] }, - "deleted": { - "type": "boolean", - "enum": [ - true + "end": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" ] + }, + "attributes": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "array", + "items": { + "type": "string", + "maxLength": 200 + }, + "maxItems": 50 + } + ] + }, + "description": "Only hosts matching all of these." + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 500, + "description": "Only these hosts (ids or uids). Default: everyone the event type could offer." + }, + "slots_per_host": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 3, + "description": "How many of each host's soonest slots to return." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50, + "description": "Hosts to return, soonest availability first." } }, "required": [ - "object", - "id", - "deleted" + "event_type", + "start", + "end" ], - "examples": [ - { - "object": "scheduling.calendar_connection", - "id": "calconn_01kd2m9a0b1c2d3e4f5g6h7j8m", - "deleted": true - } - ] - }, - "UpdateCalendarRequest": { - "type": "object", - "properties": { - "read_busy": { - "type": "boolean", - "description": "Whether this calendar's busy times block slots." - }, - "write_target": { - "type": "boolean", - "description": "Bookings are written to one calendar; setting this moves it here." - } - }, "additionalProperties": false, "examples": [ { - "read_busy": true + "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "start": "2027-01-04T00:00:00.000Z", + "end": "2027-01-18T00:00:00.000Z", + "attributes": { + "licensed_in": "CA" + } } ] }, - "SchedulingCalendarDelegation": { + "CreateBookingRequest": { "type": "object", "properties": { - "object": { + "event_type": { "type": "string", - "enum": [ - "scheduling.calendar_delegation" - ] + "description": "The event type's id or uid." }, - "id": { - "type": "string" + "start": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] }, - "provider": { + "host": { "type": "string", - "enum": [ - "google", - "microsoft" - ] - }, - "domain": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "pending", - "active", - "failing", - "deleting" - ], - "description": "`pending`: not verified yet. `failing`: the provider refused access, and its hosts aren't offered until it's back. `deleting`: its connections are being removed." + "description": "A specific host (id or uid). Default: chosen fairly from those free." }, - "match_host_email": { - "type": "boolean" + "attributes": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "array", + "items": { + "type": "string", + "maxLength": 200 + }, + "maxItems": 50 + } + ] + }, + "description": "Only choose among pool hosts matching all of these." }, - "client_id": { - "type": "string", - "description": "Google: the numeric client ID your Workspace admin authorizes." + "duration_minutes": { + "type": "integer", + "minimum": 5, + "maximum": 1440, + "description": "One of the event type's `duration_options`." }, - "tenant_id": { - "type": [ - "string", - "null" - ] + "attendee": { + "$ref": "#/components/schemas/SchedulingAttendeeInput" }, - "scopes": { + "guests": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/SchedulingGuestInput" }, - "description": "What to authorize: OAuth scopes in Google's Admin console, the application permission in Entra." - }, - "verified_account": { - "type": [ - "string", - "null" - ] - }, - "last_error_code": { - "type": [ - "string", - "null" - ] - }, - "failing_since": { - "type": [ - "string", - "null" - ] - }, - "secret_expires_at": { - "type": [ - "string", - "null" - ] - }, - "hosts_connected": { - "type": "integer" - }, - "hosts_failed": { - "type": "integer", - "description": "Hosts whose mailbox can't be reached, or who are out while this is `failing`." - }, - "created_at": { - "type": "string" + "maxItems": 10, + "description": "Up to the event type's `max_guests`." }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "object", - "id", - "provider", - "domain", - "status", - "match_host_email", - "client_id", - "tenant_id", - "scopes", - "verified_account", - "last_error_code", - "failing_since", - "secret_expires_at", - "hosts_connected", - "hosts_failed", - "created_at", - "updated_at" - ], - "examples": [ - { - "object": "scheduling.calendar_delegation", - "id": "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m", - "provider": "google", - "domain": "example.com", - "status": "active", - "match_host_email": true, - "client_id": "117063284751902345678", - "tenant_id": null, - "scopes": [ - "https://www.googleapis.com/auth/calendar.events.freebusy" - ], - "verified_account": "ana@example.com", - "last_error_code": null, - "failing_since": null, - "secret_expires_at": null, - "hosts_connected": 240, - "hosts_failed": 1, - "created_at": "2027-01-01T10:00:00.000Z", - "updated_at": "2027-01-01T10:00:00.000Z" - } - ] - }, - "CreateCalendarDelegationRequest": { - "oneOf": [ - { + "answers": { "type": "object", - "properties": { - "provider": { - "type": "string", - "enum": [ - "google" - ] - }, - "domain": { - "type": "string", - "maxLength": 253, - "pattern": "^[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)+$", - "description": "The Workspace or Microsoft 365 domain. Only addresses on exactly this domain connect, not its subdomains." - }, - "match_host_email": { - "type": "boolean", - "description": "Whether a host's `email` picks their mailbox when `calendar_account` isn't set. Turn it off if end users can type their own email into your app: otherwise one could name a colleague's address and reach that calendar." - }, - "service_account_key": { - "type": "string", - "maxLength": 10000, - "description": "The whole JSON key of a service account in your Google Cloud project. Never returned." - } + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "maxLength": 5000 + }, + { + "type": "array", + "items": { + "type": "string", + "maxLength": 500 + }, + "maxItems": 50 + } + ] }, - "required": [ - "provider", - "domain", - "service_account_key" - ], - "additionalProperties": false + "description": "Answers to the event type's questions. Stored encrypted, and returned only by retrieving the booking." }, - { - "type": "object", - "properties": { - "provider": { - "type": "string", - "enum": [ - "microsoft" - ] - }, - "domain": { - "type": "string", - "maxLength": 253, - "pattern": "^[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)+$", - "description": "The Workspace or Microsoft 365 domain. Only addresses on exactly this domain connect, not its subdomains." + "location": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "link" + ] + }, + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "format": "uri", + "default": null, + "description": "The meeting link. `null` on an event type means the booking supplies its own." + } + }, + "required": [ + "type" + ], + "additionalProperties": false }, - "match_host_email": { - "type": "boolean", - "description": "Whether a host's `email` picks their mailbox when `calendar_account` isn't set. Turn it off if end users can type their own email into your app: otherwise one could name a colleague's address and reach that calendar." + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone" + ] + }, + "number": { + "type": [ + "string", + "null" + ], + "maxLength": 32, + "default": null, + "description": "The phone number to call. `null` on an event type means the booking supplies its own." + } + }, + "required": [ + "type" + ], + "additionalProperties": false }, - "tenant_id": { - "type": "string", - "maxLength": 100, - "description": "The Directory (tenant) ID." + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "in_person" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "maxLength": 500, + "default": null, + "description": "The address to meet at. `null` on an event type means the booking supplies its own." + } + }, + "required": [ + "type" + ], + "additionalProperties": false }, - "client_id": { - "type": "string", - "maxLength": 100, - "description": "The Application (client) ID." + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "google_meet" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false }, - "client_secret": { - "type": "string", - "maxLength": 1000, - "description": "A client secret of that app. Never returned." + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "microsoft_teams" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false }, - "secret_expires_at": { - "type": "string", - "format": "date-time", - "description": "When the Entra client secret expires. Owners and admins are warned 14 days before." + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "zoom" + ] + }, + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "format": "uri", + "default": null, + "description": "The meeting's link, once it's created on the host's Zoom account." + } + }, + "required": [ + "type" + ], + "additionalProperties": false } - }, - "required": [ - "provider", - "domain", - "tenant_id", - "client_id", - "client_secret" ], - "additionalProperties": false + "description": "One of the event type's `locations`, with your link filled in if it has none." + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 500 + }, + "description": "Your own key-value pairs. Don't put sensitive data here." } + }, + "required": [ + "event_type", + "start", + "attendee" ], + "additionalProperties": false, "examples": [ { - "provider": "microsoft", - "domain": "example.com", - "tenant_id": "7d3f1c52-8e0b-4d6e-9a27-5c4b1e7d2f60", - "client_id": "3f1c7a52-8e0b-4d6e-9a27-5c4b1e7d2f60", - "client_secret": "abc~defghijklmnopqrstuvwxyz" + "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "start": "2027-01-04T15:00:00.000Z", + "attendee": { + "uid": "patient_77", + "name": "Sam Lee", + "email": "sam@example.com", + "time_zone": "UTC" + } } ] }, - "SchedulingCalendarDelegationList": { + "SchedulingAttendeeInput": { + "type": "object", + "properties": { + "uid": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Your own id for this person, used to erase their data later." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Needed for coss.com to notify them. Can't contain a link." + }, + "email": { + "type": "string", + "maxLength": 320, + "format": "email", + "description": "Where coss.com emails them, if it does." + }, + "phone": { + "type": "string", + "pattern": "^\\+[1-9]\\d{6,14}$", + "description": "An E.164 number, for SMS reminders." + }, + "time_zone": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Their IANA time zone. A series keeps their local time across clock changes.", + "examples": [ + "America/New_York" + ] + }, + "locale": { + "type": "string", + "minLength": 2, + "maxLength": 35, + "description": "Their locale, such as `en-US`. Emails and texts about the booking are written in it when coss.com has it, otherwise in English, and guests get the same." + } + }, + "required": [ + "time_zone" + ], + "additionalProperties": false, + "description": "Name and email are only needed if coss.com notifies this attendee." + }, + "SchedulingGuestInput": { + "type": "object", + "properties": { + "email": { + "type": "string", + "maxLength": 320, + "format": "email", + "description": "Where they're emailed about the booking." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Their name, if you have it." + } + }, + "required": [ + "email" + ], + "additionalProperties": false, + "description": "Someone else coming along. They're emailed about the booking, with no manage link." + }, + "SchedulingBookingList": { "type": "object", "properties": { "object": { @@ -23285,7 +23873,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SchedulingCalendarDelegation" + "$ref": "#/components/schemas/SchedulingBooking" } }, "has_more": { @@ -23302,23 +23890,40 @@ "object": "list", "data": [ { - "object": "scheduling.calendar_delegation", - "id": "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m", - "provider": "google", - "domain": "example.com", - "status": "active", - "match_host_email": true, - "client_id": "117063284751902345678", - "tenant_id": null, - "scopes": [ - "https://www.googleapis.com/auth/calendar.events.freebusy" + "object": "scheduling.booking", + "id": "book_01kd2m9a0b1c2d3e4f5g6h7j8m", + "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "status": "confirmed", + "title": "Initial consultation", + "start": "2027-01-04T15:00:00.000Z", + "end": "2027-01-04T15:30:00.000Z", + "hosts": [ + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ], - "verified_account": "ana@example.com", - "last_error_code": null, - "failing_since": null, - "secret_expires_at": null, - "hosts_connected": 240, - "hosts_failed": 1, + "attendees": [ + { + "id": "attn_01kd2m9a0b1c2d3e4f5g6h7j8m", + "role": "attendee", + "uid": "patient_77", + "name": "Sam Lee", + "email": "sam@example.com", + "phone": null, + "time_zone": "UTC", + "locale": null + } + ], + "location": null, + "hold_expires_at": null, + "approval_expires_at": null, + "series": null, + "series_index": null, + "group_session": null, + "calendar_unverified": false, + "external_conflict_at": null, + "no_show_at": null, + "cancelled_at": null, + "cancelled_by": null, + "metadata": {}, "created_at": "2027-01-01T10:00:00.000Z", "updated_at": "2027-01-01T10:00:00.000Z" } @@ -23327,214 +23932,324 @@ } ] }, - "UpdateCalendarDelegationRequest": { + "RescheduleBookingRequest": { "type": "object", "properties": { - "match_host_email": { - "type": "boolean", - "description": "Whether a host's `email` picks their mailbox when `calendar_account` isn't set. Turn it off if end users can type their own email into your app: otherwise one could name a colleague's address and reach that calendar." - }, - "service_account_key": { + "start": { "type": "string", - "maxLength": 10000, - "description": "A new key. It's tried on the verified mailbox before it replaces the old one." + "description": "The new start. The old time stays booked unless this one's free.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] }, - "client_secret": { + "host": { "type": "string", - "maxLength": 1000, - "description": "A new client secret. It's tried on the verified mailbox before it replaces the old one." - }, - "secret_expires_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When the Entra client secret expires. Owners and admins are warned 14 days before." + "description": "Move a pool booking to this host. Default: its current hosts." } }, + "required": [ + "start" + ], "additionalProperties": false, "examples": [ { - "match_host_email": false + "start": "2027-01-05T15:00:00.000Z" } ] }, - "VerifyCalendarDelegationRequest": { + "CancelSchedulingBookingRequest": { "type": "object", "properties": { - "account": { + "reason": { "type": "string", - "maxLength": 320, - "format": "email", - "description": "A real mailbox on the domain to try. In Microsoft 365, a user principal name." + "minLength": 1, + "maxLength": 500, + "description": "Why it's being cancelled. Only retrieving this booking returns it: it's never in webhooks, logs or emails." } }, - "required": [ - "account" - ], "additionalProperties": false, "examples": [ { - "account": "ana@example.com" + "reason": "Feeling better" } ] }, - "DeletedSchedulingCalendarDelegation": { + "SchedulingBookingEventList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "scheduling.calendar_delegation" + "list" ] }, - "id": { - "type": "string" + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingBookingEvent" + } }, - "deleted": { - "type": "boolean", - "enum": [ - true - ] + "has_more": { + "type": "boolean" } }, "required": [ "object", - "id", - "deleted" + "data", + "has_more" ], "examples": [ { - "object": "scheduling.calendar_delegation", - "id": "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m", - "deleted": true + "object": "list", + "data": [ + { + "object": "scheduling.booking_event", + "id": "bevt_01kd2m9a0b1c2d3e4f5g6h7j8m", + "sequence": 1, + "type": "scheduling.booking.confirmed", + "actor": { + "kind": "api_key", + "id": "key_01kd2m9a0b1c2d3e4f5g6h7j8m" + }, + "details": {}, + "occurred_at": "2027-01-02T09:00:00.000Z" + } + ], + "has_more": false } ] }, - "SetSimulatedBusyRequest": { + "SchedulingBookingEvent": { "type": "object", "properties": { - "intervals": { - "type": "array", - "items": { - "type": "object", - "properties": { - "start": { - "type": "string", - "description": "An ISO 8601 instant with an offset.", - "examples": [ - "2027-01-04T15:00:00.000Z" - ] - }, - "end": { - "type": "string", - "description": "An ISO 8601 instant with an offset.", - "examples": [ - "2027-01-04T15:00:00.000Z" - ] - } + "object": { + "type": "string", + "enum": [ + "scheduling.booking_event" + ] + }, + "id": { + "type": "string" + }, + "sequence": { + "type": "number", + "description": "This booking's events count up from 1." + }, + "type": { + "type": "string", + "description": "What happened, such as `scheduling.booking.confirmed`, `scheduling.booking.rescheduled` or `scheduling.booking.cancelled`." + }, + "actor": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "api_key", + "user", + "host_session", + "booker_session", + "manage_session", + "manager_session", + "system" + ] }, - "required": [ - "start", - "end" - ], - "additionalProperties": false + "id": { + "type": [ + "string", + "null" + ], + "description": "The API key, user or session's id. `null` for `system`." + } }, - "maxItems": 500, - "description": "The simulated calendar's busy times." + "required": [ + "kind", + "id" + ], + "description": "Who did it." + }, + "details": { + "type": "object", + "additionalProperties": {}, + "description": "More about it, such as `previous_start` on a reschedule. Never personal data." + }, + "occurred_at": { + "type": "string", + "description": "When it happened." } }, "required": [ - "intervals" + "object", + "id", + "sequence", + "type", + "actor", + "details", + "occurred_at" + ] + }, + "SchedulingBookingNotificationList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingBookingNotification" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" ], - "additionalProperties": false, "examples": [ { - "intervals": [ + "object": "list", + "data": [ { - "start": "2027-01-04T15:00:00.000Z", - "end": "2027-01-04T16:00:00.000Z" + "object": "scheduling.booking_notification", + "id": "sntf_01kd2m9a0b1c2d3e4f5g6h7j8m", + "kind": "reminder", + "reminder_key": "start-1440", + "channel": "email", + "recipient": "attendee", + "due_at": "2027-01-03T15:00:00.000Z", + "finished_at": null, + "outcome": null } - ] + ], + "has_more": false } ] }, - "SchedulingSession": { + "SchedulingBookingNotification": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "scheduling.session" + "scheduling.booking_notification" ] }, "id": { "type": "string" }, - "token": { + "kind": { "type": "string", - "description": "Shown only now. A bearer token for the client API." + "description": "`reminder` for an entry of `reminders`; otherwise a standard notification, or `started`/`ended`." }, - "url": { + "reminder_key": { "type": [ "string", "null" - ], - "description": "coss.com's hosted page, with the token in its fragment." + ] }, - "expires_at": { + "channel": { + "type": "string", + "enum": [ + "email", + "sms", + "webhook" + ] + }, + "recipient": { + "type": "string", + "enum": [ + "attendee", + "host", + "endpoint", + "address" + ] + }, + "due_at": { "type": "string" + }, + "finished_at": { + "type": [ + "string", + "null" + ] + }, + "outcome": { + "type": [ + "string", + "null" + ], + "description": "`null` while it's waiting. `sent`, or why not: `too_late`, `filtered`, `no_show`, `no_address`, `no_sender`, `stale`, `superseded`, `failed`." } }, "required": [ "object", "id", - "token", - "url", - "expires_at" + "kind", + "reminder_key", + "channel", + "recipient", + "due_at", + "finished_at", + "outcome" ], "examples": [ { - "object": "scheduling.session", - "id": "schs_01kd2m9a0b1c2d3e4f5g6h7j8m", - "token": "cossch_abc123", - "url": "https://example.test/h/schs_01kd2m9a0b1c2d3e4f5g6h7j8m#cossch_abc123", - "expires_at": "2027-01-01T11:00:00.000Z" + "object": "scheduling.booking_notification", + "id": "sntf_01kd2m9a0b1c2d3e4f5g6h7j8m", + "kind": "reminder", + "reminder_key": "start-1440", + "channel": "email", + "recipient": "attendee", + "due_at": "2027-01-03T15:00:00.000Z", + "finished_at": null, + "outcome": null } ] }, - "CreateSchedulingHostSessionRequest": { + "UpdateBookingLocationRequest": { "type": "object", "properties": { - "expires_in": { - "type": "integer", - "minimum": 60, - "maximum": 86400, - "description": "Seconds until it stops working. At most a day." - }, - "read_only": { - "type": "boolean", - "description": "The host can look but not change anything." + "location": { + "$ref": "#/components/schemas/SchedulingLocation" } }, + "required": [ + "location" + ], "additionalProperties": false, "examples": [ { - "expires_in": 3600 + "location": { + "type": "link", + "url": "https://meet.example.com/abc" + } } ] }, - "CreateSchedulingBookerSessionRequest": { + "CreateHoldRequest": { "type": "object", "properties": { "event_type": { "type": "string", "description": "The event type's id or uid." }, + "start": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + }, "host": { "type": "string", - "description": "Only this host (id or uid)." + "description": "A specific host (id or uid). Default: chosen fairly from those free." }, "attributes": { "type": "object", @@ -23554,974 +24269,1078 @@ } ] }, - "description": "Only offer times, and book, among pool hosts matching all of these. The browser can't see or change them." - }, - "attendee": { - "type": "object", - "properties": { - "uid": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "description": "Your own id for this person, used to erase their data later." - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100, - "description": "Needed for coss.com to notify them. Can't contain a link." - }, - "email": { - "type": "string", - "maxLength": 320, - "format": "email", - "description": "Where coss.com emails them, if it does." - }, - "phone": { - "type": "string", - "pattern": "^\\+[1-9]\\d{6,14}$", - "description": "An E.164 number, for SMS reminders." - }, - "time_zone": { - "type": "string", - "minLength": 1, - "maxLength": 64, - "description": "Their IANA time zone. A series keeps their local time across clock changes.", - "examples": [ - "America/New_York" - ] - }, - "locale": { - "type": "string", - "minLength": 2, - "maxLength": 35, - "description": "Their locale, such as `en-US`. Emails and texts about the booking are written in it when coss.com has it, otherwise in English, and guests get the same." - } - }, - "required": [ - "time_zone" - ], - "additionalProperties": false, - "description": "Who's booking, if you already know: the browser then can't change it." + "description": "Only choose among pool hosts matching all of these." }, - "expires_in": { + "duration_minutes": { "type": "integer", - "minimum": 60, - "maximum": 86400, - "description": "Seconds until it stops working. At most a day." - } - }, - "required": [ - "event_type" - ], - "additionalProperties": false, - "examples": [ - { - "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", - "attendee": { - "uid": "patient_77", - "name": "Sam Lee", - "time_zone": "UTC" - } - } - ] - }, - "SchedulingSettings": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "scheduling.settings" - ] + "minimum": 5, + "maximum": 1440, + "description": "One of the event type's `duration_options`." }, - "allowed_origins": { + "attendee": { + "$ref": "#/components/schemas/SchedulingAttendeeInput" + }, + "guests": { "type": "array", "items": { - "type": "string" - }, - "description": "Sites allowed to embed coss.com's hosted pages and call the client API." - }, - "brand_name": { - "type": [ - "string", - "null" - ], - "description": "Your name on hosted pages and on emails sent from `email_from`. `null`: none set." - }, - "accent_color": { - "type": [ - "string", - "null" - ], - "description": "The accent color of hosted pages and emails sent from `email_from`, as `#RRGGBB`. `null`: the default." - }, - "logo": { - "type": [ - "object", - "null" - ], - "properties": { - "content_type": { - "type": "string", - "enum": [ - "image/png", - "image/jpeg" - ], - "description": "The image's type, checked from its bytes." - }, - "width": { - "type": "integer", - "description": "Width in pixels." - }, - "height": { - "type": "integer", - "description": "Height in pixels." - }, - "data_url": { - "type": "string", - "description": "The image, as a `data:` URL." - } + "$ref": "#/components/schemas/SchedulingGuestInput" }, - "required": [ - "content_type", - "width", - "height", - "data_url" - ], - "description": "At the top of the hosted pages, and of emails sent from `email_from`." - }, - "email_from": { - "type": [ - "string", - "null" - ], - "description": "Sender of booking emails, on a verified domain." - }, - "sms_from": { - "type": [ - "string", - "null" - ], - "description": "Your approved number for SMS reminders. Without one, no SMS is sent." - }, - "calendar_failure_mode": { - "type": "string", - "enum": [ - "hide", - "last_known" - ], - "description": "When a host's calendar can't be read: `hide` stops offering them, `last_known` keeps offering their last known times." + "maxItems": 10, + "description": "Up to the event type's `max_guests`." }, - "notifications": { - "type": [ - "object", - "null" - ], + "answers": { + "type": "object", "additionalProperties": { - "type": "object", - "properties": { - "email_attendee": { - "type": "boolean", - "description": "Email the attendee about this event." - }, - "email_host": { - "type": "boolean", - "description": "Email the host about this event." + "anyOf": [ + { + "type": "string", + "maxLength": 5000 }, - "sms_attendee": { - "type": "boolean", - "description": "Text the attendee about this event, if `sms_from` is set." + { + "type": "array", + "items": { + "type": "string", + "maxLength": 500 + }, + "maxItems": 50 } - } + ] }, - "description": "Per event, overriding the defaults." + "description": "Answers to the event type's questions. Stored encrypted, and returned only by retrieving the booking." }, - "email_reply_to": { - "type": [ - "string", - "null" - ] - }, - "reminders": { - "type": [ - "array", - "null" + "location": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "link" + ] + }, + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "format": "uri", + "default": null, + "description": "The meeting link. `null` on an event type means the booking supplies its own." + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone" + ] + }, + "number": { + "type": [ + "string", + "null" + ], + "maxLength": 32, + "default": null, + "description": "The phone number to call. `null` on an event type means the booking supplies its own." + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "in_person" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "maxLength": 500, + "default": null, + "description": "The address to meet at. `null` on an event type means the booking supplies its own." + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "google_meet" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "microsoft_teams" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "zoom" + ] + }, + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "format": "uri", + "default": null, + "description": "The meeting's link, once it's created on the host's Zoom account." + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } ], - "items": { - "$ref": "#/components/schemas/SchedulingReminder" + "description": "One of the event type's `locations`, with your link filled in if it has none." + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 500 }, - "description": "What goes out around a booking, unless an event type has its own. `null`: a reminder a day before, on the channels `notifications.reminder` chooses." + "description": "Your own key-value pairs. Don't put sensitive data here." } }, "required": [ - "object", - "allowed_origins", - "brand_name", - "accent_color", - "logo", - "email_from", - "sms_from", - "calendar_failure_mode", - "notifications", - "email_reply_to", - "reminders" + "event_type", + "start", + "attendee" ], + "additionalProperties": false, "examples": [ { - "object": "scheduling.settings", - "allowed_origins": [ - "https://app.example.com" - ], - "brand_name": "Acme Health", - "accent_color": null, - "logo": null, - "email_from": "appointments@example.com", - "sms_from": null, - "calendar_failure_mode": "hide", - "notifications": { - "reminder": { - "sms_attendee": true - } - }, - "email_reply_to": null, - "reminders": null + "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "start": "2027-01-04T15:00:00.000Z", + "attendee": { + "uid": "patient_77", + "time_zone": "UTC" + } } ] }, - "UpdateSchedulingSettingsRequest": { + "ConfirmHoldRequest": { "type": "object", "properties": { - "allowed_origins": { - "type": "array", - "items": { - "type": "string", - "maxLength": 200 - }, - "maxItems": 10, - "description": "Sites allowed to embed coss.com's hosted pages and call the client API, as `https://host` origins." - }, - "brand_name": { - "type": [ - "string", - "null" - ], - "minLength": 1, - "maxLength": 100, - "description": "Your name on hosted pages and on emails sent from `email_from`. `null` removes it." - }, - "accent_color": { - "type": [ - "string", - "null" - ], - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "The accent color as `#RRGGBB`. `null` restores the default." - }, - "logo": { - "type": [ - "object", - "null" - ], - "properties": { - "content": { - "type": "string", - "maxLength": 266668, - "description": "A PNG or JPEG's bytes, base64-encoded. 200 KB at most." - } - }, - "required": [ - "content" - ], - "additionalProperties": false, - "description": "Your logo, a PNG or JPEG. `null` removes it." - }, - "email_from": { - "type": [ - "string", - "null" - ], - "maxLength": 320, - "format": "email", - "description": "Sender of booking emails, on a verified domain. `null`: coss.com's own address." - }, - "sms_from": { - "type": [ - "string", - "null" - ], - "pattern": "^\\+[1-9]\\d{6,14}$", - "description": "Your approved number for SMS reminders, in E.164. `null`: no SMS is sent." - }, - "calendar_failure_mode": { - "type": "string", - "enum": [ - "hide", - "last_known" - ], - "description": "When a host's calendar can't be read: `hide` stops offering them, `last_known` keeps offering their last known times." - }, - "notifications": { - "type": [ - "object", - "null" - ], - "additionalProperties": { - "type": "object", - "properties": { - "email_attendee": { - "type": "boolean", - "description": "Email the attendee about this event." - }, - "email_host": { - "type": "boolean", - "description": "Email the host about this event." - }, - "sms_attendee": { - "type": "boolean", - "description": "Text the attendee about this event, if `sms_from` is set." - } - }, - "additionalProperties": false - }, - "description": "Per event (`confirmed`, `pending_approval`, `rescheduled`, `cancelled`, `declined`, `reminder`), overriding the defaults. `null` restores them." - }, - "email_reply_to": { - "type": [ - "string", - "null" - ], - "maxLength": 320, - "format": "email", - "description": "Where replies go, for emails from `email_from`." - }, - "reminders": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/SchedulingReminderInput" - }, - "maxItems": 10, - "description": "What goes out around a booking, unless an event type has its own. `null` goes back to a reminder a day before." + "if_still_free": { + "type": "boolean", + "default": false, + "description": "After the hold expired, book anyway if nobody else took the slot. Default: refuse with `hold_expired`." } }, "additionalProperties": false, "examples": [ { - "brand_name": "Acme Health" + "if_still_free": true } ] }, - "TrackingSettings": { + "SchedulingSeries": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "tracking_settings" + "scheduling.series" ] }, - "default": { - "anyOf": [ - { - "$ref": "#/components/schemas/TrackingRule" - }, - { - "type": "null" - } - ], - "description": "The environment's tracking default. `null`: none is set." + "id": { + "type": "string" }, - "categories": { + "bookings": { "type": "array", "items": { - "$ref": "#/components/schemas/TrackingRule" + "$ref": "#/components/schemas/SchedulingBooking" }, - "description": "Category rules, by category name." + "description": "The occurrences booked, each an ordinary booking." + }, + "conflicts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "index": { + "type": "number", + "description": "The occurrence's place in the series, from 0." + }, + "start": { + "type": "string", + "description": "When it would have started." + } + }, + "required": [ + "index", + "start" + ] + }, + "description": "With `on_conflict: skip`, the occurrences that weren't free and weren't booked." } }, "required": [ "object", - "default", - "categories" + "id", + "bookings", + "conflicts" ], - "description": "Open and click tracking and one-click unsubscribe settings of the API key's environment. For each of `opens`, `clicks` and `unsubscribe`, a send's own `tracking` wins, then its category's rule, then `default` (the environment default; `null` when none is set), then off.", "examples": [ { - "object": "tracking_settings", - "default": { - "object": "tracking_rule", - "id": "etr_01k7c8k1d9w4x2b7n3m5q6r8st", - "category": null, - "opens": true, - "clicks": true, - "unsubscribe": false, - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z" - }, - "categories": [ + "object": "scheduling.series", + "id": "series_01kd2m9a0b1c2d3e4f5g6h7j8m", + "bookings": [ { - "object": "tracking_rule", - "id": "etr_01k7c8m4q2f3e9v6t5r1n0p8yz", - "category": "password-reset", - "opens": false, - "clicks": false, - "unsubscribe": false, - "created_at": "2026-01-16T10:00:00.000Z", - "updated_at": "2026-01-16T10:00:00.000Z" + "object": "scheduling.booking", + "id": "book_01kd2m9a0b1c2d3e4f5g6h7j8m", + "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "status": "confirmed", + "title": "Initial consultation", + "start": "2027-01-04T15:00:00.000Z", + "end": "2027-01-04T15:30:00.000Z", + "hosts": [ + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" + ], + "attendees": [ + { + "id": "attn_01kd2m9a0b1c2d3e4f5g6h7j8m", + "role": "attendee", + "uid": "patient_77", + "name": "Sam Lee", + "email": "sam@example.com", + "phone": null, + "time_zone": "UTC", + "locale": null + } + ], + "location": null, + "hold_expires_at": null, + "approval_expires_at": null, + "series": null, + "series_index": null, + "group_session": null, + "calendar_unverified": false, + "external_conflict_at": null, + "no_show_at": null, + "cancelled_at": null, + "cancelled_by": null, + "metadata": {}, + "created_at": "2027-01-01T10:00:00.000Z", + "updated_at": "2027-01-01T10:00:00.000Z" } - ] + ], + "conflicts": [] } ] }, - "TrackingRule": { + "CreateSeriesRequest": { "type": "object", "properties": { - "object": { + "event_type": { "type": "string", - "enum": [ - "tracking_rule" - ] + "description": "The event type's id or uid." }, - "id": { + "start": { "type": "string", + "description": "An ISO 8601 instant with an offset.", "examples": [ - "etr_01k7c8m4q2f3e9v6t5r1n0p8yz" + "2027-01-04T15:00:00.000Z" ] }, - "category": { - "type": [ - "string", - "null" - ], - "description": "The category the rule applies to; `null` for the environment default." + "host": { + "type": "string", + "description": "A specific host (id or uid). Default: chosen fairly from those free." }, - "opens": { - "type": "boolean", - "description": "Whether opens are tracked." + "attributes": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "array", + "items": { + "type": "string", + "maxLength": 200 + }, + "maxItems": 50 + } + ] + }, + "description": "Only choose among pool hosts matching all of these." }, - "clicks": { - "type": "boolean", - "description": "Whether clicks are tracked." + "duration_minutes": { + "type": "integer", + "minimum": 5, + "maximum": 1440, + "description": "One of the event type's `duration_options`." }, - "unsubscribe": { - "type": "boolean", - "description": "Whether emails get one-click unsubscribe headers (only emails with a category and a single recipient)." + "attendee": { + "$ref": "#/components/schemas/SchedulingAttendeeInput" }, - "created_at": { - "type": "string", - "format": "date-time" + "guests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingGuestInput" + }, + "maxItems": 10, + "description": "Up to the event type's `max_guests`." }, - "updated_at": { - "type": "string", - "format": "date-time" - } - }, - "required": [ - "object", - "id", - "category", - "opens", - "clicks", - "unsubscribe", - "created_at", - "updated_at" - ], - "description": "An environment's tracking default (`category: null`) or the rule for one category. Emails are fixed to the settings that applied when they were sent.", - "examples": [ - { - "object": "tracking_rule", - "id": "etr_01k7c8m4q2f3e9v6t5r1n0p8yz", - "category": "password-reset", - "opens": false, - "clicks": false, - "unsubscribe": false, - "created_at": "2026-01-16T10:00:00.000Z", - "updated_at": "2026-01-16T10:00:00.000Z" + "answers": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "maxLength": 5000 + }, + { + "type": "array", + "items": { + "type": "string", + "maxLength": 500 + }, + "maxItems": 50 + } + ] + }, + "description": "Answers to the event type's questions. Stored encrypted, and returned only by retrieving the booking." + }, + "location": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "link" + ] + }, + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "format": "uri", + "default": null, + "description": "The meeting link. `null` on an event type means the booking supplies its own." + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone" + ] + }, + "number": { + "type": [ + "string", + "null" + ], + "maxLength": 32, + "default": null, + "description": "The phone number to call. `null` on an event type means the booking supplies its own." + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "in_person" + ] + }, + "address": { + "type": [ + "string", + "null" + ], + "maxLength": 500, + "default": null, + "description": "The address to meet at. `null` on an event type means the booking supplies its own." + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "google_meet" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "microsoft_teams" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "zoom" + ] + }, + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "format": "uri", + "default": null, + "description": "The meeting's link, once it's created on the host's Zoom account." + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + ], + "description": "One of the event type's `locations`, with your link filled in if it has none." + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 500 + }, + "description": "Your own key-value pairs. Don't put sensitive data here." + }, + "recurrence": { + "type": "object", + "properties": { + "frequency": { + "type": "string", + "enum": [ + "weekly", + "monthly" + ], + "description": "Repeat weekly or monthly." + }, + "interval": { + "type": "integer", + "minimum": 1, + "maximum": 12, + "default": 1, + "description": "Every this many weeks or months. Default: 1." + }, + "count": { + "type": "integer", + "minimum": 2, + "maximum": 52, + "description": "Occurrences in total, the first included." + } + }, + "required": [ + "frequency", + "count" + ], + "additionalProperties": false, + "description": "How often the booking repeats, at the attendee's local time." + }, + "on_conflict": { + "type": "string", + "enum": [ + "reject", + "skip" + ], + "default": "reject", + "description": "If an occurrence isn't free: book nothing, or book the rest and report it." + } + }, + "required": [ + "event_type", + "start", + "attendee", + "recurrence" + ], + "additionalProperties": false, + "examples": [ + { + "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "start": "2027-01-04T15:00:00.000Z", + "attendee": { + "uid": "patient_77", + "time_zone": "America/New_York" + }, + "recurrence": { + "frequency": "weekly", + "interval": 1, + "count": 12 + } } ] }, - "UpdateTrackingRuleRequest": { + "SchedulingSeriesPreview": { "type": "object", "properties": { - "opens": { - "type": "boolean", - "description": "Track opens." + "object": { + "type": "string", + "enum": [ + "list" + ] }, - "clicks": { - "type": "boolean", - "description": "Track clicks." + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "When this occurrence starts." + }, + "available": { + "type": "boolean", + "description": "Whether it's free." + }, + "reason": { + "type": [ + "string", + "null" + ], + "description": "Why it isn't free, such as `host_busy`, `already_claimed`, `outside_availability` or `no_hosts`. `null` when it is." + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The ids of the hosts who could take it." + } + }, + "required": [ + "start", + "available", + "reason", + "hosts" + ] + }, + "description": "Each occurrence the request would book. Nothing is booked." }, - "unsubscribe": { + "has_more": { "type": "boolean", - "description": "Add one-click unsubscribe headers to emails with a category and a single recipient." + "enum": [ + false + ] } }, - "additionalProperties": false, - "description": "Fields left out keep their current value; a new rule starts with all off. A send's own `tracking` option still wins over the rule.", + "required": [ + "object", + "data", + "has_more" + ], "examples": [ { - "opens": false, - "clicks": false + "object": "list", + "data": [ + { + "start": "2027-01-04T15:00:00.000Z", + "available": true, + "reason": null, + "hosts": [ + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] + } + ], + "has_more": false } ] }, - "DeletedTrackingRule": { + "SchedulingCancelledSeries": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "tracking_rule" + "list" ] }, - "id": { - "type": "string" - }, - "category": { - "type": [ - "string", - "null" - ] + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingBooking" + } }, - "deleted": { + "has_more": { "type": "boolean", "enum": [ - true + false ] } }, "required": [ "object", - "id", - "category", - "deleted" + "data", + "has_more" ], "examples": [ { - "object": "tracking_rule", - "id": "etr_01k7c8m4q2f3e9v6t5r1n0p8yz", - "category": "password-reset", - "deleted": true + "object": "list", + "data": [ + { + "object": "scheduling.booking", + "id": "book_01kd2m9a0b1c2d3e4f5g6h7j8m", + "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "status": "confirmed", + "title": "Initial consultation", + "start": "2027-01-04T15:00:00.000Z", + "end": "2027-01-04T15:30:00.000Z", + "hosts": [ + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" + ], + "attendees": [ + { + "id": "attn_01kd2m9a0b1c2d3e4f5g6h7j8m", + "role": "attendee", + "uid": "patient_77", + "name": "Sam Lee", + "email": "sam@example.com", + "phone": null, + "time_zone": "UTC", + "locale": null + } + ], + "location": null, + "hold_expires_at": null, + "approval_expires_at": null, + "series": null, + "series_index": null, + "group_session": null, + "calendar_unverified": false, + "external_conflict_at": null, + "no_show_at": null, + "cancelled_at": null, + "cancelled_by": null, + "metadata": {}, + "created_at": "2027-01-01T10:00:00.000Z", + "updated_at": "2027-01-01T10:00:00.000Z" + } + ], + "has_more": false } ] }, - "StorageSettings": { + "CancelSeriesRequest": { + "type": "object", + "properties": { + "from_index": { + "type": "integer", + "minimum": 0, + "default": 0, + "description": "Cancel this occurrence and those after it. Default: all of them." + } + }, + "additionalProperties": false, + "examples": [ + { + "from_index": 4 + } + ] + }, + "SchedulingAttendeeErasure": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "storage_settings" + "scheduling.attendee_erasure" ] }, - "content_storage": { + "uid": { "type": "string", - "enum": [ - "store", - "discard" - ], - "description": "`store` (the default) keeps each email's content (subject, body, headers, template variables, attachment filenames) for the email retention period. `discard` keeps it only until the email is sent, fails or is canceled; its metadata (addresses, tags, category, status, recipients, events) is kept as usual." + "description": "The attendee's uid." }, - "updated_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When the setting last changed; `null` if it never did (content is stored)." + "erased": { + "type": "number", + "description": "How many attendee records were erased, one per booking." } }, "required": [ "object", - "content_storage", - "updated_at" + "uid", + "erased" ], - "description": "What coss.com keeps of the API key's environment's emails. Emails keep the setting that applied when they were sent.", "examples": [ { - "object": "storage_settings", - "content_storage": "discard", - "updated_at": "2026-10-16T10:00:00.000Z" + "object": "scheduling.attendee_erasure", + "uid": "patient_77", + "erased": 3 } ] }, - "UpdateStorageSettingsRequest": { + "SchedulingAttendeeErasureByEmail": { "type": "object", "properties": { - "content_storage": { + "object": { "type": "string", "enum": [ - "store", - "discard" - ], - "description": "`store` keeps emails' content; `discard` clears it once they are sent." + "scheduling.attendee_erasure" + ] + }, + "erased": { + "type": "number", + "description": "How many attendee and guest records were erased." } }, "required": [ - "content_storage" + "object", + "erased" + ], + "examples": [ + { + "object": "scheduling.attendee_erasure", + "erased": 3 + } + ] + }, + "EraseSchedulingAttendeeByEmailRequest": { + "type": "object", + "properties": { + "email": { + "type": "string", + "maxLength": 320, + "format": "email", + "description": "The address to forget, an attendee's or a guest's." + } + }, + "required": [ + "email" ], "additionalProperties": false, - "description": "Applies to emails sent from now on; emails already sent keep their setting.", "examples": [ { - "content_storage": "discard" + "email": "sam@example.com" } ] }, - "Contact": { + "SchedulingCalendarAuthorization": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "contact" + "scheduling.calendar_authorization" ] }, - "id": { - "type": "string" - }, - "email": { + "url": { "type": "string", - "description": "The contact's address. Unique in the environment, case-insensitively." - }, - "first_name": { - "type": [ - "string", - "null" - ], - "description": "Their first name. `null`: not set." - }, - "last_name": { - "type": [ - "string", - "null" - ], - "description": "Their last name. `null`: not set." - }, - "status": { + "description": "Open it in a new window: providers refuse to be framed." + } + }, + "required": [ + "object", + "url" + ], + "examples": [ + { + "object": "scheduling.calendar_authorization", + "url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=abc" + } + ] + }, + "AuthorizeCalendarRequest": { + "type": "object", + "properties": { + "provider": { "type": "string", "enum": [ - "subscribed", - "unsubscribed", - "pending" + "google", + "microsoft", + "zoom" ], - "description": "Consent to marketing email: `subscribed`, `unsubscribed` (never receives marketing email; transactional email is unaffected) or `pending` (waiting for a double opt-in confirmation)." - }, - "properties": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - } - ] - }, - "description": "Custom properties by key." - }, - "source": { - "type": "string", - "enum": [ - "api", - "dashboard", - "import", - "preference_center", - "form" - ], - "description": "How the contact was created." - }, - "subscribed_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When it last became `subscribed`. `null`: it hasn't." - }, - "unsubscribed_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When it became `unsubscribed`. `null` unless it is." - }, - "confirmed_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When the contact confirmed a double opt-in." - }, - "created_at": { - "type": "string", - "format": "date-time" + "description": "Whose consent page to open. CalDAV connects with a password instead." }, - "updated_at": { + "return_url": { "type": "string", - "format": "date-time" + "maxLength": 2000, + "format": "uri", + "description": "Where to send the browser afterwards, on one of the environment's allowed origins. `coss_calendar` says what happened: `connected`, `denied` or `invalid`." } }, "required": [ - "object", - "id", - "email", - "first_name", - "last_name", - "status", - "properties", - "source", - "subscribed_at", - "unsubscribed_at", - "confirmed_at", - "created_at", - "updated_at" + "provider" ], + "additionalProperties": false, "examples": [ { - "object": "contact", - "id": "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", - "email": "ada@example.com", - "first_name": "Ada", - "last_name": "Lovelace", - "status": "subscribed", - "properties": { - "plan": "pro", - "seats": 5 - }, - "source": "api", - "subscribed_at": "2026-10-10T12:00:00.000Z", - "unsubscribed_at": null, - "confirmed_at": null, - "created_at": "2026-10-10T12:00:00.000Z", - "updated_at": "2026-10-10T12:00:00.000Z" + "provider": "google" } ] }, - "UpsertContactRequest": { + "SchedulingCalendarConnection": { "type": "object", "properties": { - "email": { + "object": { "type": "string", - "description": "The contact's address. Display names are ignored; matching is case-insensitive.", - "examples": [ - "ada@example.com" + "enum": [ + "scheduling.calendar_connection" ] }, - "first_name": { - "type": [ - "string", - "null" - ], - "minLength": 1, - "maxLength": 256, - "description": "Their first name. `null` removes it." + "id": { + "type": "string" }, - "last_name": { - "type": [ - "string", - "null" + "host": { + "type": "string", + "description": "The id of the host it belongs to." + }, + "provider": { + "type": "string", + "enum": [ + "google", + "microsoft", + "caldav", + "simulated", + "zoom" ], - "minLength": 1, - "maxLength": 256, - "description": "Their last name. `null` removes it." + "description": "Where the calendar lives. `simulated` exists only in sandbox environments; `zoom` is only used for meetings, never read for busy times." }, - "properties": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "string", - "maxLength": 2000 - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "null" - } - ] - }, - "description": "Custom properties by key (define them first, see contact properties). Merged into the contact's existing properties; `null` removes one.", - "examples": [ - { - "plan": "pro", - "seats": 5 - } - ] + "account": { + "type": "string", + "description": "The provider's account, such as the Google address." }, "status": { "type": "string", "enum": [ - "subscribed", - "unsubscribed" + "active", + "needs_reauth" ], - "description": "Sets the contact's consent. Omitted, a new contact is `subscribed` and an existing one keeps its status." + "description": "`needs_reauth`: access was lost, and this host isn't offered until it's back." }, - "audience_ids": { - "type": "array", - "items": { - "type": "string" - }, - "maxItems": 20, - "description": "Audiences to add the contact to (it stays in the others)." + "delegation": { + "type": [ + "string", + "null" + ], + "description": "The calendar delegation that manages this connection. It can't be disconnected or reconnected by itself." }, - "topics": { + "calendars": { "type": "array", "items": { "type": "object", "properties": { - "topic": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The topic's key." + "id": { + "type": "string" }, - "status": { + "external_id": { "type": "string", - "enum": [ - "opted_in", - "opted_out" + "description": "The calendar's id at the provider." + }, + "read_busy": { + "type": "boolean", + "description": "Whether its busy times block slots." + }, + "write_target": { + "type": "boolean", + "description": "Whether bookings are written to it." + }, + "verified_at": { + "type": [ + "string", + "null" ], - "description": "`opted_in` or `opted_out` of the topic." + "description": "When it was last read successfully. `null`: not read yet." } }, "required": [ - "topic", - "status" - ], - "additionalProperties": false + "id", + "external_id", + "read_busy", + "write_target", + "verified_at" + ] }, - "maxItems": 100, - "description": "Choices per topic; topics left out keep the contact's choice." + "description": "The account's calendars." }, - "double_opt_in": { - "type": "boolean", - "description": "Ask the contact to confirm first (default: the marketing settings' `double_opt_in_default`): a new or unsubscribed contact becomes `pending` and gets a confirmation email; it is `subscribed` once they click its link. Can't be combined with `status: \"subscribed\"`, nor can the default (pass `false` to subscribe directly)." + "created_at": { + "type": "string" } }, "required": [ - "email" + "object", + "id", + "host", + "provider", + "account", + "status", + "delegation", + "calendars", + "created_at" ], - "additionalProperties": false, "examples": [ { - "email": "ada@example.com", - "first_name": "Ada", - "properties": { - "plan": "pro" - }, - "topics": [ + "object": "scheduling.calendar_connection", + "id": "calconn_01kd2m9a0b1c2d3e4f5g6h7j8m", + "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "provider": "google", + "account": "ana@example.com", + "status": "active", + "delegation": null, + "calendars": [ { - "topic": "newsletter", - "status": "opted_in" + "id": "cal_01kd2m9a0b1c2d3e4f5g6h7j8m", + "external_id": "ana@example.com", + "read_busy": true, + "write_target": true, + "verified_at": "2027-01-01T10:00:00.000Z" } - ] - } - ] - }, - "ContactConfirmation": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "contact_confirmation" - ] - }, - "contact_id": { - "type": "string", - "description": "The contact's id." - }, - "status": { - "type": "string", - "enum": [ - "sent", - "cooldown", - "limit_reached", - "not_pending" ], - "description": "`sent`; `cooldown` (one confirmation per contact per day); `limit_reached` (three were sent and none confirmed); `not_pending` (the contact isn't waiting to confirm)." - } - }, - "required": [ - "object", - "contact_id", - "status" - ], - "examples": [ - { - "object": "contact_confirmation", - "contact_id": "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", - "status": "sent" + "created_at": "2027-01-01T10:00:00.000Z" } ] }, - "PreferenceLinks": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "preference_links" - ] - }, - "contact_id": { - "type": "string", - "description": "The contact's id." - }, - "preferences_url": { - "type": "string", - "description": "The contact's preference center." + "CreateCalendarConnectionRequest": { + "oneOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "caldav" + ] + }, + "url": { + "type": "string", + "maxLength": 2000, + "format": "uri", + "description": "The server's https URL." + }, + "username": { + "type": "string", + "minLength": 1, + "maxLength": 320, + "description": "The account's username on that server." + }, + "password": { + "type": "string", + "minLength": 1, + "maxLength": 500, + "description": "An app-specific password. Encrypted, and never returned." + } + }, + "required": [ + "provider", + "url", + "username", + "password" + ], + "additionalProperties": false }, - "unsubscribe_url": { - "type": "string", - "description": "A one-button unsubscribe page." - } - }, - "required": [ - "object", - "contact_id", - "preferences_url", - "unsubscribe_url" - ], - "examples": [ { - "object": "preference_links", - "contact_id": "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", - "preferences_url": "https://track.cosslinks.com/t/u/aXyZ…", - "unsubscribe_url": "https://track.cosslinks.com/t/u/aXyZ…/unsubscribe" - } - ] - }, - "CreatePreferenceLinksRequest": { - "type": "object", - "properties": { - "topic": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The topic your email is about: its unsubscribe link leaves just that topic." + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "simulated" + ] + } + }, + "required": [ + "provider" + ], + "additionalProperties": false } - }, - "additionalProperties": false, + ], + "description": "`caldav` connects a server with an app-specific password; `simulated` is a sandbox calendar for tests.", "examples": [ { - "topic": "newsletter" + "provider": "caldav", + "url": "https://caldav.icloud.com", + "username": "ana@example.com", + "password": "abcd-efgh-ijkl-mnop" } ] }, - "ContactList": { + "SchedulingCalendarConnectionList": { "type": "object", "properties": { "object": { @@ -24533,7 +25352,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Contact" + "$ref": "#/components/schemas/SchedulingCalendarConnection" } }, "has_more": { @@ -24550,440 +25369,308 @@ "object": "list", "data": [ { - "object": "contact", - "id": "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", - "email": "ada@example.com", - "first_name": "Ada", - "last_name": "Lovelace", - "status": "subscribed", - "properties": { - "plan": "pro", - "seats": 5 - }, - "source": "api", - "subscribed_at": "2026-10-10T12:00:00.000Z", - "unsubscribed_at": null, - "confirmed_at": null, - "created_at": "2026-10-10T12:00:00.000Z", - "updated_at": "2026-10-10T12:00:00.000Z" + "object": "scheduling.calendar_connection", + "id": "calconn_01kd2m9a0b1c2d3e4f5g6h7j8m", + "host": "host_01kd2m9a0b1c2d3e4f5g6h7j8m", + "provider": "google", + "account": "ana@example.com", + "status": "active", + "delegation": null, + "calendars": [ + { + "id": "cal_01kd2m9a0b1c2d3e4f5g6h7j8m", + "external_id": "ana@example.com", + "read_busy": true, + "write_target": true, + "verified_at": "2027-01-01T10:00:00.000Z" + } + ], + "created_at": "2027-01-01T10:00:00.000Z" } ], "has_more": false } ] }, - "ContactImport": { + "DeletedSchedulingCalendarConnection": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "contact_import" + "scheduling.calendar_connection" ] }, - "created": { - "type": "integer", - "description": "Contacts created." - }, - "updated": { - "type": "integer", - "description": "Existing contacts updated." - }, - "kept_unsubscribed": { - "type": "integer", - "description": "Existing contacts the import marked `subscribed` that stay `unsubscribed`: an import never re-subscribes anyone." + "id": { + "type": "string" }, - "invalid": { - "type": "array", - "items": { - "type": "object", - "properties": { - "index": { - "type": "integer", - "description": "The entry's position in `contacts`, from 0." - }, - "email": { - "type": "string", - "description": "The address it gave." - }, - "message": { - "type": "string", - "description": "Why it was refused." - } - }, - "required": [ - "index", - "email", - "message" - ] - }, - "description": "Entries that were refused. The rest of the import still went through." + "deleted": { + "type": "boolean", + "enum": [ + true + ] } }, "required": [ "object", - "created", - "updated", - "kept_unsubscribed", - "invalid" + "id", + "deleted" ], "examples": [ { - "object": "contact_import", - "created": 1, - "updated": 1, - "kept_unsubscribed": 0, - "invalid": [ - { - "index": 2, - "email": "not-an-address", - "message": "Must be an email address." - } - ] + "object": "scheduling.calendar_connection", + "id": "calconn_01kd2m9a0b1c2d3e4f5g6h7j8m", + "deleted": true } ] }, - "ImportContactsRequest": { + "UpdateCalendarRequest": { "type": "object", "properties": { - "contacts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "email": { - "type": "string", - "description": "The contact's address (validated on its own)." - }, - "first_name": { - "type": [ - "string", - "null" - ], - "description": "Their first name." - }, - "last_name": { - "type": [ - "string", - "null" - ], - "description": "Their last name." - }, - "properties": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "null" - } - ] - }, - "description": "Custom properties by key, checked against their definitions." - }, - "status": { - "type": "string", - "description": "`subscribed` or `unsubscribed` (anything else, like an export's `pending`, is reported in `invalid`). Omitted, a new contact is `subscribed` and an existing one keeps its status." - } - }, - "required": [ - "email" - ], - "additionalProperties": false - }, - "minItems": 1, - "maxItems": 1000, - "description": "The contacts to create or update, at most 1,000." + "read_busy": { + "type": "boolean", + "description": "Whether this calendar's busy times block slots." }, - "audience_id": { - "type": "string", - "description": "Adds every imported contact to this audience." + "write_target": { + "type": "boolean", + "description": "Bookings are written to one calendar; setting this moves it here." } }, - "required": [ - "contacts" - ], "additionalProperties": false, "examples": [ { - "contacts": [ - { - "email": "ada@example.com", - "first_name": "Ada", - "properties": { - "plan": "pro" - } - }, - { - "email": "grace@example.com", - "status": "unsubscribed" - } - ] + "read_busy": true } ] }, - "UpdateContactRequest": { + "SchedulingCalendarDelegation": { "type": "object", "properties": { - "first_name": { - "type": [ - "string", - "null" - ], - "minLength": 1, - "maxLength": 256, - "description": "Their first name. `null` removes it." + "object": { + "type": "string", + "enum": [ + "scheduling.calendar_delegation" + ] }, - "last_name": { - "type": [ - "string", - "null" - ], - "minLength": 1, - "maxLength": 256, - "description": "Their last name. `null` removes it." + "id": { + "type": "string" }, - "properties": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "type": "string", - "maxLength": 2000 - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "null" - } - ] - }, - "description": "Custom properties by key (define them first, see contact properties). Merged into the contact's existing properties; `null` removes one.", - "examples": [ - { - "plan": "pro", - "seats": 5 - } + "provider": { + "type": "string", + "enum": [ + "google", + "microsoft" ] }, + "domain": { + "type": "string" + }, "status": { "type": "string", "enum": [ - "subscribed", - "unsubscribed" + "pending", + "active", + "failing", + "deleting" ], - "description": "Sets the contact's consent. Omitted, a new contact is `subscribed` and an existing one keeps its status." + "description": "`pending`: not verified yet. `failing`: the provider refused access, and its hosts aren't offered until it's back. `deleting`: its connections are being removed." }, - "topics": { + "match_host_email": { + "type": "boolean" + }, + "client_id": { + "type": "string", + "description": "Google: the numeric client ID your Workspace admin authorizes." + }, + "tenant_id": { + "type": [ + "string", + "null" + ] + }, + "scopes": { "type": "array", "items": { - "type": "object", - "properties": { - "topic": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The topic's key." - }, - "status": { - "type": "string", - "enum": [ - "opted_in", - "opted_out" - ], - "description": "`opted_in` or `opted_out` of the topic." - } - }, - "required": [ - "topic", - "status" - ], - "additionalProperties": false + "type": "string" }, - "maxItems": 100 - } - }, - "additionalProperties": false, - "examples": [ - { - "last_name": "Byron", - "properties": { - "seats": null - } - } - ] - }, - "DeletedContact": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "contact" + "description": "What to authorize: OAuth scopes in Google's Admin console, the application permission in Entra." + }, + "verified_account": { + "type": [ + "string", + "null" ] }, - "id": { - "type": "string" + "last_error_code": { + "type": [ + "string", + "null" + ] }, - "deleted": { - "type": "boolean", - "enum": [ - true + "failing_since": { + "type": [ + "string", + "null" + ] + }, + "secret_expires_at": { + "type": [ + "string", + "null" ] + }, + "hosts_connected": { + "type": "integer" + }, + "hosts_failed": { + "type": "integer", + "description": "Hosts whose mailbox can't be reached, or who are out while this is `failing`." + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" } }, "required": [ "object", "id", - "deleted" + "provider", + "domain", + "status", + "match_host_email", + "client_id", + "tenant_id", + "scopes", + "verified_account", + "last_error_code", + "failing_since", + "secret_expires_at", + "hosts_connected", + "hosts_failed", + "created_at", + "updated_at" ], "examples": [ { - "object": "contact", - "id": "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", - "deleted": true + "object": "scheduling.calendar_delegation", + "id": "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m", + "provider": "google", + "domain": "example.com", + "status": "active", + "match_host_email": true, + "client_id": "117063284751902345678", + "tenant_id": null, + "scopes": [ + "https://www.googleapis.com/auth/calendar.events.freebusy" + ], + "verified_account": "ana@example.com", + "last_error_code": null, + "failing_since": null, + "secret_expires_at": null, + "hosts_connected": 240, + "hosts_failed": 1, + "created_at": "2027-01-01T10:00:00.000Z", + "updated_at": "2027-01-01T10:00:00.000Z" } ] }, - "ContactTopics": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "contact_topics" - ] - }, - "contact_id": { - "type": "string", - "description": "The contact's id." - }, - "topics": { - "type": "array", - "items": { - "type": "object", - "properties": { - "topic": { - "type": "string", - "description": "The topic's key." - }, - "name": { - "type": "string", - "description": "The topic's name." - }, - "subscribed": { - "type": "boolean", - "description": "Whether emails about the topic reach the contact (its choice, else the topic's default; an unsubscribed contact gets no marketing email at all)." - }, - "choice": { - "type": [ - "string", - "null" - ], - "enum": [ - "opted_in", - "opted_out", - null - ], - "description": "The contact's explicit choice; `null`: the topic's default applies." - } + "CreateCalendarDelegationRequest": { + "oneOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google" + ] }, - "required": [ - "topic", - "name", - "subscribed", - "choice" - ] + "domain": { + "type": "string", + "maxLength": 253, + "pattern": "^[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)+$", + "description": "The Workspace or Microsoft 365 domain. Only addresses on exactly this domain connect, not its subdomains." + }, + "match_host_email": { + "type": "boolean", + "description": "Whether a host's `email` picks their mailbox when `calendar_account` isn't set. Turn it off if end users can type their own email into your app: otherwise one could name a colleague's address and reach that calendar." + }, + "service_account_key": { + "type": "string", + "maxLength": 10000, + "description": "The whole JSON key of a service account in your Google Cloud project. Never returned." + } }, - "description": "Every topic of the environment, with this contact's standing." - } - }, - "required": [ - "object", - "contact_id", - "topics" - ], - "examples": [ + "required": [ + "provider", + "domain", + "service_account_key" + ], + "additionalProperties": false + }, { - "object": "contact_topics", - "contact_id": "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", - "topics": [ - { - "topic": "newsletter", - "name": "Newsletter", - "subscribed": true, - "choice": null + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "microsoft" + ] }, - { - "topic": "product", - "name": "Product updates", - "subscribed": false, - "choice": "opted_out" - } - ] - } - ] - }, - "UpdateContactTopicsRequest": { - "type": "object", - "properties": { - "topics": { - "type": "array", - "items": { - "type": "object", - "properties": { - "topic": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The topic's key." - }, - "status": { - "type": "string", - "enum": [ - "opted_in", - "opted_out" - ], - "description": "`opted_in` or `opted_out` of the topic." - } + "domain": { + "type": "string", + "maxLength": 253, + "pattern": "^[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)+$", + "description": "The Workspace or Microsoft 365 domain. Only addresses on exactly this domain connect, not its subdomains." }, - "required": [ - "topic", - "status" - ], - "additionalProperties": false + "match_host_email": { + "type": "boolean", + "description": "Whether a host's `email` picks their mailbox when `calendar_account` isn't set. Turn it off if end users can type their own email into your app: otherwise one could name a colleague's address and reach that calendar." + }, + "tenant_id": { + "type": "string", + "maxLength": 100, + "description": "The Directory (tenant) ID." + }, + "client_id": { + "type": "string", + "maxLength": 100, + "description": "The Application (client) ID." + }, + "client_secret": { + "type": "string", + "maxLength": 1000, + "description": "A client secret of that app. Never returned." + }, + "secret_expires_at": { + "type": "string", + "format": "date-time", + "description": "When the Entra client secret expires. Owners and admins are warned 14 days before." + } }, - "minItems": 1, - "maxItems": 100, - "description": "The contact's choices per topic. Topics not listed keep theirs." + "required": [ + "provider", + "domain", + "tenant_id", + "client_id", + "client_secret" + ], + "additionalProperties": false } - }, - "required": [ - "topics" ], - "additionalProperties": false, "examples": [ { - "topics": [ - { - "topic": "product", - "status": "opted_out" - } - ] + "provider": "microsoft", + "domain": "example.com", + "tenant_id": "7d3f1c52-8e0b-4d6e-9a27-5c4b1e7d2f60", + "client_id": "3f1c7a52-8e0b-4d6e-9a27-5c4b1e7d2f60", + "client_secret": "abc~defghijklmnopqrstuvwxyz" } ] }, - "ContactPropertyList": { + "SchedulingCalendarDelegationList": { "type": "object", "properties": { "object": { @@ -24995,7 +25682,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ContactProperty" + "$ref": "#/components/schemas/SchedulingCalendarDelegation" } }, "has_more": { @@ -25012,181 +25699,91 @@ "object": "list", "data": [ { - "object": "contact_property", - "id": "cprop_01k7c8m4q2f3e9v6t5r1n0p8yz", - "key": "plan", - "type": "string", - "fallback": "free", - "created_at": "2026-10-10T12:00:00.000Z", - "updated_at": "2026-10-10T12:00:00.000Z" + "object": "scheduling.calendar_delegation", + "id": "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m", + "provider": "google", + "domain": "example.com", + "status": "active", + "match_host_email": true, + "client_id": "117063284751902345678", + "tenant_id": null, + "scopes": [ + "https://www.googleapis.com/auth/calendar.events.freebusy" + ], + "verified_account": "ana@example.com", + "last_error_code": null, + "failing_since": null, + "secret_expires_at": null, + "hosts_connected": 240, + "hosts_failed": 1, + "created_at": "2027-01-01T10:00:00.000Z", + "updated_at": "2027-01-01T10:00:00.000Z" } ], "has_more": false } ] }, - "ContactProperty": { + "UpdateCalendarDelegationRequest": { "type": "object", "properties": { - "object": { + "match_host_email": { + "type": "boolean", + "description": "Whether a host's `email` picks their mailbox when `calendar_account` isn't set. Turn it off if end users can type their own email into your app: otherwise one could name a colleague's address and reach that calendar." + }, + "service_account_key": { "type": "string", - "enum": [ - "contact_property" - ] - }, - "id": { - "type": "string" - }, - "key": { - "type": "string", - "description": "How contacts and templates name it (`{{{contact.}}}`). Can't change." - }, - "type": { - "type": "string", - "enum": [ - "string", - "number", - "boolean", - "date" - ], - "description": "`date` values are ISO 8601 strings." - }, - "fallback": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Used in templates for contacts without a value." - }, - "created_at": { - "type": "string", - "format": "date-time" + "maxLength": 10000, + "description": "A new key. It's tried on the verified mailbox before it replaces the old one." }, - "updated_at": { - "type": "string", - "format": "date-time" - } - }, - "required": [ - "object", - "id", - "key", - "type", - "fallback", - "created_at", - "updated_at" - ], - "examples": [ - { - "object": "contact_property", - "id": "cprop_01k7c8m4q2f3e9v6t5r1n0p8yz", - "key": "plan", - "type": "string", - "fallback": "free", - "created_at": "2026-10-10T12:00:00.000Z", - "updated_at": "2026-10-10T12:00:00.000Z" - } - ] - }, - "CreateContactPropertyRequest": { - "type": "object", - "properties": { - "key": { + "client_secret": { "type": "string", - "pattern": "^[a-z][a-z0-9_]{0,49}$", - "description": "How contacts and templates name it. Lower-case letters, digits and `_`, starting with a letter. Can't change." + "maxLength": 1000, + "description": "A new client secret. It's tried on the verified mailbox before it replaces the old one." }, - "type": { - "type": "string", - "enum": [ + "secret_expires_at": { + "type": [ "string", - "number", - "boolean", - "date" - ], - "description": "What values it takes. `date` values are ISO 8601 strings. Can't change." - }, - "fallback": { - "anyOf": [ - { - "type": "string", - "maxLength": 2000 - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "null" - } + "null" ], - "description": "Used in templates for contacts without a value." + "format": "date-time", + "description": "When the Entra client secret expires. Owners and admins are warned 14 days before." } }, - "required": [ - "key", - "type" - ], "additionalProperties": false, "examples": [ { - "key": "plan", - "type": "string", - "fallback": "free" + "match_host_email": false } ] }, - "UpdateContactPropertyRequest": { + "VerifyCalendarDelegationRequest": { "type": "object", "properties": { - "fallback": { - "anyOf": [ - { - "type": "string", - "maxLength": 2000 - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Used in templates for contacts without a value. `null` removes it." + "account": { + "type": "string", + "maxLength": 320, + "format": "email", + "description": "A real mailbox on the domain to try. In Microsoft 365, a user principal name." } }, "required": [ - "fallback" + "account" ], "additionalProperties": false, "examples": [ { - "fallback": "starter" + "account": "ana@example.com" } ] }, - "DeletedContactProperty": { + "DeletedSchedulingCalendarDelegation": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "contact_property" + "scheduling.calendar_delegation" ] }, "id": { @@ -25206,750 +25803,738 @@ ], "examples": [ { - "object": "contact_property", - "id": "cprop_01k7c8m4q2f3e9v6t5r1n0p8yz", + "object": "scheduling.calendar_delegation", + "id": "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m", "deleted": true } ] }, - "BroadcastList": { + "SetSimulatedBusyRequest": { "type": "object", "properties": { - "object": { - "type": "string", - "enum": [ - "list" - ] - }, - "data": { + "intervals": { "type": "array", "items": { - "$ref": "#/components/schemas/Broadcast" - } - }, - "has_more": { - "type": "boolean" + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + }, + "end": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + } + }, + "required": [ + "start", + "end" + ], + "additionalProperties": false + }, + "maxItems": 500, + "description": "The simulated calendar's busy times." } }, "required": [ - "object", - "data", - "has_more" + "intervals" ], + "additionalProperties": false, "examples": [ { - "object": "list", - "data": [ + "intervals": [ { - "object": "broadcast", - "id": "bcast_01k7c8m4q2f3e9v6t5r1n0p8yz", - "name": "October newsletter", - "status": "sent", - "audience_id": null, - "segment_id": "seg_01k7c8m4q2f3e9v6t5r1n0p8yz", - "topic": "newsletter", - "from": "Acme ", - "reply_to": [], - "subject": "What's new, {{{contact.first_name|there}}}", - "preview_text": null, - "html": "

Hi {{{contact.first_name|there}}},

", - "text": null, - "tracking": { - "opens": true, - "clicks": true - }, - "rate_per_second": null, - "scheduled_at": "2026-10-20T09:00:00.000Z", - "started_at": "2026-10-20T09:00:01.000Z", - "completed_at": "2026-10-20T09:12:40.000Z", - "canceled_at": null, - "recipients_estimated": 1200, - "pause_reason": null, - "error": null, - "stats": { - "recipients": 1200, - "sent": 1200, - "delivered": 1180, - "bounced": 6, - "complained": 1, - "failed": 0, - "opened": 540, - "clicked": 120, - "unsubscribed": 4 - }, - "created_at": "2026-10-15T12:00:00.000Z", - "updated_at": "2026-10-20T09:12:40.000Z" + "start": "2027-01-04T15:00:00.000Z", + "end": "2027-01-04T16:00:00.000Z" } - ], - "has_more": false + ] } ] }, - "Broadcast": { + "SchedulingSession": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "broadcast" + "scheduling.session" ] }, "id": { "type": "string" }, - "name": { - "type": "string", - "description": "Shown only to you." - }, - "status": { + "token": { "type": "string", - "enum": [ - "draft", - "scheduled", - "sending", - "paused", - "sent", - "canceled", - "failed" - ], - "description": "`draft` (editable), `scheduled`, `sending`, `paused` (see `pause_reason`), `sent`, `canceled`, or `failed` (see `error`)." - }, - "audience_id": { - "type": [ - "string", - "null" - ], - "description": "The audience it goes to. `null`: a segment." - }, - "segment_id": { - "type": [ - "string", - "null" - ], - "description": "The segment it goes to. `null`: an audience." + "description": "Shown only now. A bearer token for the client API." }, - "topic": { + "url": { "type": [ "string", "null" ], - "description": "The topic it's about (its key), and its emails' `category`. `null`: none." - }, - "from": { - "type": "string", - "description": "Who it's sent from." + "description": "coss.com's hosted page, with the token in its fragment." }, - "reply_to": { + "expires_at": { + "type": "string" + } + }, + "required": [ + "object", + "id", + "token", + "url", + "expires_at" + ], + "examples": [ + { + "object": "scheduling.session", + "id": "schs_01kd2m9a0b1c2d3e4f5g6h7j8m", + "token": "cossch_abc123", + "url": "https://example.test/h/schs_01kd2m9a0b1c2d3e4f5g6h7j8m#cossch_abc123", + "expires_at": "2027-01-01T11:00:00.000Z" + } + ] + }, + "CreateSchedulingManagerSessionRequest": { + "type": "object", + "properties": { + "hosts": { "type": "array", "items": { "type": "string" }, - "description": "Addresses replies go to." + "minItems": 1, + "maxItems": 1000, + "description": "The hosts this page can reach, by id or uid." }, - "subject": { - "type": "string", - "description": "The subject, before contact fields are filled in." + "host_filter": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "description": "Everyone matching these attributes, asked again on every request." }, - "preview_text": { - "type": [ - "string", - "null" - ], - "description": "The inbox preview line. `null`: none." + "expires_in": { + "type": "integer", + "minimum": 60, + "maximum": 86400, + "description": "Seconds until it stops working. At most a day." }, - "html": { - "type": [ - "string", - "null" - ], - "description": "The HTML part, before contact fields are filled in. `null`: text only." + "read_only": { + "type": "boolean" + } + }, + "additionalProperties": false, + "examples": [ + { + "host_filter": { + "team": "cardiology" + }, + "expires_in": 3600 + } + ] + }, + "CreateSchedulingHostSessionRequest": { + "type": "object", + "properties": { + "expires_in": { + "type": "integer", + "minimum": 60, + "maximum": 86400, + "description": "Seconds until it stops working. At most a day." }, - "text": { - "type": [ - "string", - "null" - ], - "description": "The plain text part. `null`: HTML only." + "read_only": { + "type": "boolean", + "description": "The host can look but not change anything." + } + }, + "additionalProperties": false, + "examples": [ + { + "expires_in": 3600 + } + ] + }, + "CreateSchedulingBookerSessionRequest": { + "type": "object", + "properties": { + "event_type": { + "type": "string", + "description": "The event type's id or uid." }, - "tracking": { + "host": { + "type": "string", + "description": "Only this host (id or uid)." + }, + "attributes": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "array", + "items": { + "type": "string", + "maxLength": 200 + }, + "maxItems": 50 + } + ] + }, + "description": "Only offer times, and book, among pool hosts matching all of these. The browser can't see or change them." + }, + "attendee": { "type": "object", "properties": { - "opens": { - "type": "boolean", - "description": "Whether opens are tracked." + "uid": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Your own id for this person, used to erase their data later." }, - "clicks": { - "type": "boolean", - "description": "Whether clicks are tracked." + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Needed for coss.com to notify them. Can't contain a link." + }, + "email": { + "type": "string", + "maxLength": 320, + "format": "email", + "description": "Where coss.com emails them, if it does." + }, + "phone": { + "type": "string", + "pattern": "^\\+[1-9]\\d{6,14}$", + "description": "An E.164 number, for SMS reminders." + }, + "time_zone": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Their IANA time zone. A series keeps their local time across clock changes.", + "examples": [ + "America/New_York" + ] + }, + "locale": { + "type": "string", + "minLength": 2, + "maxLength": 35, + "description": "Their locale, such as `en-US`. Emails and texts about the booking are written in it when coss.com has it, otherwise in English, and guests get the same." } }, "required": [ - "opens", - "clicks" + "time_zone" ], - "description": "Which opens and clicks are tracked." + "additionalProperties": false, + "description": "Who's booking, if you already know: the browser then can't change it." }, - "rate_per_second": { + "expires_in": { + "type": "integer", + "minimum": 60, + "maximum": 86400, + "description": "Seconds until it stops working. At most a day." + } + }, + "required": [ + "event_type" + ], + "additionalProperties": false, + "examples": [ + { + "event_type": "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m", + "attendee": { + "uid": "patient_77", + "name": "Sam Lee", + "time_zone": "UTC" + } + } + ] + }, + "SchedulingSettings": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "scheduling.settings" + ] + }, + "allowed_origins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Sites allowed to embed coss.com's hosted pages and call the client API." + }, + "brand_name": { "type": [ - "integer", + "string", "null" ], - "description": "Recipients per second. `null`: the plan's marketing rate." + "description": "Your name on hosted pages and on emails sent from `email_from`. `null`: none set." }, - "scheduled_at": { + "accent_color": { "type": [ "string", "null" ], - "format": "date-time", - "description": "When it's set to start. `null`: not scheduled." + "description": "The accent color of hosted pages and emails sent from `email_from`, as `#RRGGBB`. `null`: the default." }, - "started_at": { + "logo": { "type": [ - "string", + "object", "null" ], - "format": "date-time", - "description": "When sending started. `null`: it hasn't." + "properties": { + "content_type": { + "type": "string", + "enum": [ + "image/png", + "image/jpeg" + ], + "description": "The image's type, checked from its bytes." + }, + "width": { + "type": "integer", + "description": "Width in pixels." + }, + "height": { + "type": "integer", + "description": "Height in pixels." + }, + "data_url": { + "type": "string", + "description": "The image, as a `data:` URL." + } + }, + "required": [ + "content_type", + "width", + "height", + "data_url" + ], + "description": "At the top of the hosted pages, and of emails sent from `email_from`." }, - "completed_at": { + "email_from": { "type": [ "string", "null" ], - "format": "date-time", - "description": "When it finished. `null`: it hasn't." + "description": "Sender of booking emails, on a verified domain." }, - "canceled_at": { + "sms_from": { "type": [ "string", "null" ], - "format": "date-time", - "description": "When it was canceled. `null`: it wasn't." + "description": "Your approved number for SMS reminders. Without one, no SMS is sent." }, - "recipients_estimated": { - "type": [ - "integer", - "null" + "name_host_in_attendee_emails": { + "type": "boolean", + "description": "Name the new host when telling an attendee their booking has one." + }, + "host_reassignment": { + "type": "string", + "enum": [ + "off", + "eligible" ], - "description": "How many contacts it would reach, counted when it started." + "description": "Whether hosts can give their own bookings to someone else from their page. `eligible` lets them, to anyone free who takes that event type." }, - "pause_reason": { - "type": [ - "string", - "null" + "out_of_office_reasons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "key", + "label" + ] + }, + "description": "Reasons of your own for time away, on top of the preset ones." + }, + "calendar_failure_mode": { + "type": "string", + "enum": [ + "hide", + "last_known" ], - "description": "Why a paused broadcast waits: `paused` (by you), `billing_hold`, `budget_exceeded`, `usage_limit`, `marketing_paused`, `account_suspended`, `sending_paused`." + "description": "When a host's calendar can't be read: `hide` stops offering them, `last_known` keeps offering their last known times." }, - "error": { + "notifications": { "type": [ "object", "null" ], - "properties": { - "code": { - "type": "string", - "description": "Machine-readable reason." - }, - "message": { - "type": "string", - "description": "Human-readable explanation." + "additionalProperties": { + "type": "object", + "properties": { + "email_attendee": { + "type": "boolean", + "description": "Email the attendee about this event." + }, + "email_host": { + "type": "boolean", + "description": "Email the host about this event." + }, + "sms_attendee": { + "type": "boolean", + "description": "Text the attendee about this event, if `sms_from` is set." + } } }, - "required": [ - "code", - "message" - ], - "description": "Why it `failed`, such as `target_unavailable` when its audience, segment or topic was deleted while sending. `null`: it didn't." - }, - "stats": { - "$ref": "#/components/schemas/BroadcastStats" + "description": "Per event, overriding the defaults." }, - "created_at": { - "type": "string", - "format": "date-time" + "email_reply_to": { + "type": [ + "string", + "null" + ] }, - "updated_at": { - "type": "string", - "format": "date-time" + "reminders": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SchedulingReminder" + }, + "description": "What goes out around a booking, unless an event type has its own. `null`: a reminder a day before, on the channels `notifications.reminder` chooses." } }, "required": [ "object", - "id", - "name", - "status", - "audience_id", - "segment_id", - "topic", - "from", - "reply_to", - "subject", - "preview_text", - "html", - "text", - "tracking", - "rate_per_second", - "scheduled_at", - "started_at", - "completed_at", - "canceled_at", - "recipients_estimated", - "pause_reason", - "error", - "stats", - "created_at", - "updated_at" + "allowed_origins", + "brand_name", + "accent_color", + "logo", + "email_from", + "sms_from", + "name_host_in_attendee_emails", + "host_reassignment", + "out_of_office_reasons", + "calendar_failure_mode", + "notifications", + "email_reply_to", + "reminders" ], "examples": [ { - "object": "broadcast", - "id": "bcast_01k7c8m4q2f3e9v6t5r1n0p8yz", - "name": "October newsletter", - "status": "sent", - "audience_id": null, - "segment_id": "seg_01k7c8m4q2f3e9v6t5r1n0p8yz", - "topic": "newsletter", - "from": "Acme ", - "reply_to": [], - "subject": "What's new, {{{contact.first_name|there}}}", - "preview_text": null, - "html": "

Hi {{{contact.first_name|there}}},

", - "text": null, - "tracking": { - "opens": true, - "clicks": true - }, - "rate_per_second": null, - "scheduled_at": "2026-10-20T09:00:00.000Z", - "started_at": "2026-10-20T09:00:01.000Z", - "completed_at": "2026-10-20T09:12:40.000Z", - "canceled_at": null, - "recipients_estimated": 1200, - "pause_reason": null, - "error": null, - "stats": { - "recipients": 1200, - "sent": 1200, - "delivered": 1180, - "bounced": 6, - "complained": 1, - "failed": 0, - "opened": 540, - "clicked": 120, - "unsubscribed": 4 + "object": "scheduling.settings", + "allowed_origins": [ + "https://app.example.com" + ], + "brand_name": "Acme Health", + "accent_color": null, + "logo": null, + "email_from": "appointments@example.com", + "sms_from": null, + "name_host_in_attendee_emails": false, + "host_reassignment": "off", + "out_of_office_reasons": [], + "calendar_failure_mode": "hide", + "notifications": { + "reminder": { + "sms_attendee": true + } }, - "created_at": "2026-10-15T12:00:00.000Z", - "updated_at": "2026-10-20T09:12:40.000Z" + "email_reply_to": null, + "reminders": null } ] }, - "CreateBroadcastRequest": { + "UpdateSchedulingSettingsRequest": { "type": "object", "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "Shown only to you." - }, - "audience_id": { - "type": "string", - "description": "Send to this audience…" - }, - "segment_id": { - "type": "string", - "description": "…or to this segment (one of the two)." - }, - "topic": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The topic the broadcast is about (its key): only contacts it reaches get it, and its unsubscribe link leaves just that topic. It is the emails' `category`." - }, - "from": { - "type": "string", - "examples": [ - "Acme " - ] - }, - "reply_to": { + "allowed_origins": { "type": "array", "items": { - "type": "string" + "type": "string", + "maxLength": 200 }, "maxItems": 10, - "description": "Addresses replies go to." + "description": "Sites allowed to embed coss.com's hosted pages and call the client API, as `https://host` origins." }, - "subject": { - "type": "string", + "brand_name": { + "type": [ + "string", + "null" + ], "minLength": 1, - "maxLength": 998, - "description": "May use `{{{contact.|fallback}}}` (e.g. `{{{contact.first_name|there}}}`)." - }, - "preview_text": { - "type": "string", - "maxLength": 500, - "description": "The inbox preview line (a hidden preheader at the start of the HTML)." - }, - "html": { - "type": "string", - "maxLength": 1000000, - "description": "`{{{contact.|fallback}}}` for contact fields and properties (escaped), and `{{{unsubscribe_url}}}`, `{{{preferences_url}}}`, `{{{mailing_address}}}`; a footer with the postal address and an unsubscribe link is added unless they are there." + "maxLength": 100, + "description": "Your name on hosted pages and on emails sent from `email_from`. `null` removes it." }, - "text": { - "type": "string", - "maxLength": 1000000, - "description": "The plain text part. Takes the same placeholders as `html`." + "accent_color": { + "type": [ + "string", + "null" + ], + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "The accent color as `#RRGGBB`. `null` restores the default." }, - "tracking": { - "type": "object", + "logo": { + "type": [ + "object", + "null" + ], "properties": { - "opens": { - "type": "boolean", - "description": "Track opens with a pixel." - }, - "clicks": { - "type": "boolean", - "description": "Track clicks by rewriting links." + "content": { + "type": "string", + "maxLength": 266668, + "description": "A PNG or JPEG's bytes, base64-encoded. 200 KB at most." } }, - "additionalProperties": false, - "description": "Which opens and clicks are tracked." - }, - "rate_per_second": { - "type": "integer", - "minimum": 1, - "maximum": 1000, - "description": "Recipients per second, at most the plan's marketing rate (the default)." - }, - "send": { - "type": "boolean", - "description": "Start sending right away." - }, - "scheduled_at": { - "type": "string", - "format": "date-time", - "description": "Start sending at this time (ISO 8601 with an offset, at most 30 days ahead)." - } - }, - "required": [ - "name", - "from", - "subject" - ], - "additionalProperties": false, - "examples": [ - { - "name": "October newsletter", - "segment_id": "seg_01k7c8m4q2f3e9v6t5r1n0p8yz", - "topic": "newsletter", - "from": "Acme ", - "subject": "What's new, {{{contact.first_name|there}}}", - "html": "

Hi {{{contact.first_name|there}}},

…

Unsubscribe

", - "scheduled_at": "2026-10-20T09:00:00Z" - } - ] - }, - "UpdateBroadcastRequest": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "Shown only to you." - }, - "audience_id": { - "type": [ - "string", - "null" + "required": [ + "content" ], - "description": "Send to this audience. `null` clears it." + "additionalProperties": false, + "description": "Your logo, a PNG or JPEG. `null` removes it." }, - "segment_id": { + "email_from": { "type": [ "string", "null" ], - "description": "Send to this segment. `null` clears it." + "maxLength": 320, + "format": "email", + "description": "Sender of booking emails, on a verified domain. `null`: coss.com's own address." }, - "topic": { + "sms_from": { "type": [ "string", "null" ], - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The topic it's about (its key). `null` clears it." + "pattern": "^\\+[1-9]\\d{6,14}$", + "description": "Your approved number for SMS reminders, in E.164. `null`: no SMS is sent." }, - "from": { + "calendar_failure_mode": { "type": "string", - "examples": [ - "Acme " - ] + "enum": [ + "hide", + "last_known" + ], + "description": "When a host's calendar can't be read: `hide` stops offering them, `last_known` keeps offering their last known times." }, - "reply_to": { - "type": "array", - "items": { - "type": "string" - }, - "maxItems": 10, - "description": "Addresses replies go to." + "name_host_in_attendee_emails": { + "type": "boolean", + "description": "Name the new host when telling an attendee their booking has one." }, - "subject": { + "host_reassignment": { "type": "string", - "minLength": 1, - "maxLength": 998, - "description": "May use `{{{contact.|fallback}}}` (e.g. `{{{contact.first_name|there}}}`)." + "enum": [ + "off", + "eligible" + ], + "description": "Whether hosts can give their own bookings to someone else from their page. `eligible` lets them, to anyone free who takes that event type." }, - "preview_text": { + "out_of_office_reasons": { "type": [ - "string", + "array", "null" ], - "maxLength": 500, - "description": "The inbox preview line. `null` removes it." + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,63}$", + "description": "One of `unspecified`, `vacation`, `travel`, `sick`, `public_holiday`, `parental_leave`, `training` and `other`, or a key from the settings' `out_of_office_reasons`. Only people who manage this host see it: never bookers, webhooks or calendars.", + "examples": [ + "vacation" + ] + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 60 + } + }, + "required": [ + "key", + "label" + ], + "additionalProperties": false + }, + "maxItems": 50, + "description": "Reasons of your own, on top of the preset ones. Hosts and managers see them." }, - "html": { + "notifications": { "type": [ - "string", + "object", "null" ], - "maxLength": 1000000, - "description": "The HTML part. `null` removes it, if `text` remains." + "additionalProperties": { + "type": "object", + "properties": { + "email_attendee": { + "type": "boolean", + "description": "Email the attendee about this event." + }, + "email_host": { + "type": "boolean", + "description": "Email the host about this event." + }, + "sms_attendee": { + "type": "boolean", + "description": "Text the attendee about this event, if `sms_from` is set." + } + }, + "additionalProperties": false + }, + "description": "Per event (`confirmed`, `pending_approval`, `rescheduled`, `cancelled`, `declined`, `reminder`), overriding the defaults. `null` restores them." }, - "text": { + "email_reply_to": { "type": [ "string", "null" ], - "maxLength": 1000000, - "description": "The plain text part. `null` removes it, if `html` remains." - }, - "tracking": { - "type": "object", - "properties": { - "opens": { - "type": "boolean", - "description": "Track opens with a pixel." - }, - "clicks": { - "type": "boolean", - "description": "Track clicks by rewriting links." - } - }, - "additionalProperties": false, - "description": "Which opens and clicks are tracked." + "maxLength": 320, + "format": "email", + "description": "Where replies go, for emails from `email_from`." }, - "rate_per_second": { + "reminders": { "type": [ - "integer", + "array", "null" ], - "minimum": 1, - "maximum": 1000, - "description": "Recipients per second. `null`: the plan's marketing rate." + "items": { + "$ref": "#/components/schemas/SchedulingReminderInput" + }, + "maxItems": 10, + "description": "What goes out around a booking, unless an event type has its own. `null` goes back to a reminder a day before." } }, "additionalProperties": false, "examples": [ { - "subject": "Our October news" + "brand_name": "Acme Health" } ] }, - "DeletedBroadcast": { + "TrackingSettings": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "broadcast" + "tracking_settings" ] }, - "id": { - "type": "string" + "default": { + "anyOf": [ + { + "$ref": "#/components/schemas/TrackingRule" + }, + { + "type": "null" + } + ], + "description": "The environment's tracking default. `null`: none is set." }, - "deleted": { - "type": "boolean", - "enum": [ - true - ] + "categories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TrackingRule" + }, + "description": "Category rules, by category name." } }, "required": [ "object", - "id", - "deleted" - ], - "examples": [ - { - "object": "broadcast", - "id": "bcast_01k7c8m4q2f3e9v6t5r1n0p8yz", - "deleted": true - } - ] - }, - "SendBroadcastRequest": { - "type": "object", - "properties": { - "scheduled_at": { - "type": "string", - "format": "date-time", - "description": "Omit to start right away." - } - }, - "additionalProperties": false, - "examples": [ - { - "scheduled_at": "2026-10-20T09:00:00Z" - } - ] - }, - "BroadcastTest": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "broadcast_test" - ] - }, - "email_ids": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The ids of the test emails." - } - }, - "required": [ - "object", - "email_ids" + "default", + "categories" ], + "description": "Open and click tracking and one-click unsubscribe settings of the API key's environment. For each of `opens`, `clicks` and `unsubscribe`, a send's own `tracking` wins, then its category's rule, then `default` (the environment default; `null` when none is set), then off.", "examples": [ { - "object": "broadcast_test", - "email_ids": [ - "email_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - ] - }, - "TestBroadcastRequest": { - "type": "object", - "properties": { - "to": { - "type": "array", - "items": { - "type": "string" + "object": "tracking_settings", + "default": { + "object": "tracking_rule", + "id": "etr_01k7c8k1d9w4x2b7n3m5q6r8st", + "category": null, + "opens": true, + "clicks": true, + "unsubscribe": false, + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z" }, - "minItems": 1, - "maxItems": 5, - "description": "Who gets the test, at most 5 addresses." - }, - "contact": { - "type": "string", - "description": "A contact (id or address) whose values fill the template; default: fallbacks." - } - }, - "required": [ - "to" - ], - "additionalProperties": false, - "examples": [ - { - "to": [ - "me@acme.com" + "categories": [ + { + "object": "tracking_rule", + "id": "etr_01k7c8m4q2f3e9v6t5r1n0p8yz", + "category": "password-reset", + "opens": false, + "clicks": false, + "unsubscribe": false, + "created_at": "2026-01-16T10:00:00.000Z", + "updated_at": "2026-01-16T10:00:00.000Z" + } ] } ] }, - "BroadcastEmailList": { + "TrackingRule": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "list" + "tracking_rule" ] }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Email" - } - }, - "has_more": { - "type": "boolean" - } - }, - "required": [ - "object", - "data", - "has_more" - ] - }, - "AudienceList": { - "type": "object", - "properties": { - "object": { + "id": { "type": "string", - "enum": [ - "list" + "examples": [ + "etr_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Audience" - } - }, - "has_more": { - "type": "boolean" - } - }, - "required": [ - "object", - "data", - "has_more" - ], - "examples": [ - { - "object": "list", - "data": [ - { - "object": "audience", - "id": "aud_01k7c8m4q2f3e9v6t5r1n0p8yz", - "name": "Beta testers", - "contact_count": 42, - "created_at": "2026-10-10T12:00:00.000Z", - "updated_at": "2026-10-10T12:00:00.000Z" - } + "category": { + "type": [ + "string", + "null" ], - "has_more": false - } - ] - }, - "Audience": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "audience" - ] + "description": "The category the rule applies to; `null` for the environment default." }, - "id": { - "type": "string" + "opens": { + "type": "boolean", + "description": "Whether opens are tracked." }, - "name": { - "type": "string", - "description": "Your name for the list, unique in the environment." + "clicks": { + "type": "boolean", + "description": "Whether clicks are tracked." }, - "contact_count": { - "type": "integer", - "description": "Members, whatever their status." + "unsubscribe": { + "type": "boolean", + "description": "Whether emails get one-click unsubscribe headers (only emails with a category and a single recipient)." }, "created_at": { "type": "string", @@ -25963,74 +26548,70 @@ "required": [ "object", "id", - "name", - "contact_count", + "category", + "opens", + "clicks", + "unsubscribe", "created_at", "updated_at" ], + "description": "An environment's tracking default (`category: null`) or the rule for one category. Emails are fixed to the settings that applied when they were sent.", "examples": [ { - "object": "audience", - "id": "aud_01k7c8m4q2f3e9v6t5r1n0p8yz", - "name": "Beta testers", - "contact_count": 42, - "created_at": "2026-10-10T12:00:00.000Z", - "updated_at": "2026-10-10T12:00:00.000Z" - } - ] - }, - "CreateAudienceRequest": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "Your name for the list, unique in the environment." - } - }, - "required": [ - "name" - ], - "additionalProperties": false, - "examples": [ - { - "name": "Beta testers" + "object": "tracking_rule", + "id": "etr_01k7c8m4q2f3e9v6t5r1n0p8yz", + "category": "password-reset", + "opens": false, + "clicks": false, + "unsubscribe": false, + "created_at": "2026-01-16T10:00:00.000Z", + "updated_at": "2026-01-16T10:00:00.000Z" } ] }, - "UpdateAudienceRequest": { + "UpdateTrackingRuleRequest": { "type": "object", "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "Your name for the list, unique in the environment." + "opens": { + "type": "boolean", + "description": "Track opens." + }, + "clicks": { + "type": "boolean", + "description": "Track clicks." + }, + "unsubscribe": { + "type": "boolean", + "description": "Add one-click unsubscribe headers to emails with a category and a single recipient." } }, - "required": [ - "name" - ], "additionalProperties": false, + "description": "Fields left out keep their current value; a new rule starts with all off. A send's own `tracking` option still wins over the rule.", "examples": [ { - "name": "Early access" + "opens": false, + "clicks": false } ] }, - "DeletedAudience": { + "DeletedTrackingRule": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "audience" + "tracking_rule" ] }, "id": { "type": "string" }, + "category": { + "type": [ + "string", + "null" + ] + }, "deleted": { "type": "boolean", "enum": [ @@ -26041,167 +26622,171 @@ "required": [ "object", "id", + "category", "deleted" ], "examples": [ { - "object": "audience", - "id": "aud_01k7c8m4q2f3e9v6t5r1n0p8yz", + "object": "tracking_rule", + "id": "etr_01k7c8m4q2f3e9v6t5r1n0p8yz", + "category": "password-reset", "deleted": true } ] }, - "AudienceContactsAdded": { + "StorageSettings": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "audience_contacts" + "storage_settings" ] }, - "added": { - "type": "integer", - "description": "Contacts newly added." - }, - "existing": { - "type": "integer", - "description": "Contacts that were already members." + "content_storage": { + "type": "string", + "enum": [ + "store", + "discard" + ], + "description": "`store` (the default) keeps each email's content (subject, body, headers, template variables, attachment filenames) for the email retention period. `discard` keeps it only until the email is sent, fails or is canceled; its metadata (addresses, tags, category, status, recipients, events) is kept as usual." }, - "missing": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Ids or addresses of no contact." + "updated_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When the setting last changed; `null` if it never did (content is stored)." } }, "required": [ "object", - "added", - "existing", - "missing" + "content_storage", + "updated_at" ], + "description": "What coss.com keeps of the API key's environment's emails. Emails keep the setting that applied when they were sent.", "examples": [ { - "object": "audience_contacts", - "added": 1, - "existing": 0, - "missing": [] + "object": "storage_settings", + "content_storage": "discard", + "updated_at": "2026-10-16T10:00:00.000Z" } ] }, - "AddAudienceContactsRequest": { + "UpdateStorageSettingsRequest": { "type": "object", "properties": { - "contacts": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1, - "maxItems": 1000, - "description": "Contact ids or addresses of existing contacts." + "content_storage": { + "type": "string", + "enum": [ + "store", + "discard" + ], + "description": "`store` keeps emails' content; `discard` clears it once they are sent." } }, "required": [ - "contacts" + "content_storage" ], "additionalProperties": false, + "description": "Applies to emails sent from now on; emails already sent keep their setting.", "examples": [ { - "contacts": [ - "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", - "grace@example.com" - ] + "content_storage": "discard" } ] }, - "SegmentList": { + "Contact": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "list" + "contact" ] }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Segment" - } + "id": { + "type": "string" }, - "has_more": { - "type": "boolean" - } - }, - "required": [ - "object", - "data", - "has_more" - ], - "examples": [ - { - "object": "list", - "data": [ - { - "object": "segment", - "id": "seg_01k7c8m4q2f3e9v6t5r1n0p8yz", - "name": "Pro customers", - "filter": { - "version": 1, - "match": "all", - "conditions": [ - { - "field": "property", - "key": "plan", - "op": "eq", - "value": "pro" - }, - { - "match": "any", - "conditions": [ - { - "field": "email", - "op": "ends_with", - "value": "@acme.com" - }, - { - "field": "audience", - "op": "in", - "audience_id": "aud_01k7c8m4q2f3e9v6t5r1n0p8yz" - } - ] - } - ] - }, - "created_at": "2026-10-10T12:00:00.000Z", - "updated_at": "2026-10-10T12:00:00.000Z" - } + "email": { + "type": "string", + "description": "The contact's address. Unique in the environment, case-insensitively." + }, + "first_name": { + "type": [ + "string", + "null" ], - "has_more": false - } - ] - }, - "Segment": { - "type": "object", - "properties": { - "object": { + "description": "Their first name. `null`: not set." + }, + "last_name": { + "type": [ + "string", + "null" + ], + "description": "Their last name. `null`: not set." + }, + "status": { "type": "string", "enum": [ - "segment" - ] + "subscribed", + "unsubscribed", + "pending" + ], + "description": "Consent to marketing email: `subscribed`, `unsubscribed` (never receives marketing email; transactional email is unaffected) or `pending` (waiting for a double opt-in confirmation)." }, - "id": { - "type": "string" + "properties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "description": "Custom properties by key." }, - "name": { + "source": { "type": "string", - "description": "Your name for the segment, unique in the environment." + "enum": [ + "api", + "dashboard", + "import", + "preference_center", + "form" + ], + "description": "How the contact was created." }, - "filter": { - "$ref": "#/components/schemas/SegmentFilter" + "subscribed_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When it last became `subscribed`. `null`: it hasn't." + }, + "unsubscribed_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When it became `unsubscribed`. `null` unless it is." + }, + "confirmed_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When the contact confirmed a double opt-in." }, "created_at": { "type": "string", @@ -26215,623 +26800,254 @@ "required": [ "object", "id", - "name", - "filter", + "email", + "first_name", + "last_name", + "status", + "properties", + "source", + "subscribed_at", + "unsubscribed_at", + "confirmed_at", "created_at", "updated_at" ], "examples": [ { - "object": "segment", - "id": "seg_01k7c8m4q2f3e9v6t5r1n0p8yz", - "name": "Pro customers", - "filter": { - "version": 1, - "match": "all", - "conditions": [ - { - "field": "property", - "key": "plan", - "op": "eq", - "value": "pro" - }, - { - "match": "any", - "conditions": [ - { - "field": "email", - "op": "ends_with", - "value": "@acme.com" - }, - { - "field": "audience", - "op": "in", - "audience_id": "aud_01k7c8m4q2f3e9v6t5r1n0p8yz" - } - ] - } - ] + "object": "contact", + "id": "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", + "email": "ada@example.com", + "first_name": "Ada", + "last_name": "Lovelace", + "status": "subscribed", + "properties": { + "plan": "pro", + "seats": 5 }, + "source": "api", + "subscribed_at": "2026-10-10T12:00:00.000Z", + "unsubscribed_at": null, + "confirmed_at": null, "created_at": "2026-10-10T12:00:00.000Z", "updated_at": "2026-10-10T12:00:00.000Z" } ] }, - "SegmentFilter": { + "UpsertContactRequest": { "type": "object", "properties": { - "version": { - "type": "number", - "enum": [ - 1 + "email": { + "type": "string", + "description": "The contact's address. Display names are ignored; matching is case-insensitive.", + "examples": [ + "ada@example.com" + ] + }, + "first_name": { + "type": [ + "string", + "null" ], - "description": "The filter's format. Always 1." + "minLength": 1, + "maxLength": 256, + "description": "Their first name. `null` removes it." }, - "match": { - "type": "string", - "enum": [ - "all", - "any" + "last_name": { + "type": [ + "string", + "null" ], - "description": "`all`: every condition must hold; `any`: one is enough." + "minLength": 1, + "maxLength": 256, + "description": "Their last name. `null` removes it." }, - "conditions": { - "type": "array", - "items": { + "properties": { + "type": "object", + "additionalProperties": { "anyOf": [ { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "property" - ] - }, - "key": { - "type": "string", - "description": "The custom property to test." - }, - "op": { - "type": "string", - "enum": [ - "eq", - "neq", - "in", - "not_in", - "gt", - "gte", - "lt", - "lte", - "contains", - "starts_with", - "exists", - "not_exists" - ], - "description": "How to compare: `eq`, `neq`, `in`, `not_in`, `gt`, `gte`, `lt`, `lte`, `contains`, `starts_with`, or `exists` and `not_exists`, which take no value." - }, - "value": { - "anyOf": [ - { - "type": "string", - "maxLength": 2000 - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "array", - "items": { - "anyOf": [ - { - "type": "string", - "maxLength": 2000 - }, - { - "type": "number" - }, - { - "type": "boolean" - } - ] - }, - "minItems": 1, - "maxItems": 100 - } - ], - "description": "A list for `in`/`not_in`; none for `exists`/`not_exists`." - } - }, - "required": [ - "field", - "key", - "op" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "email" - ] - }, - "op": { - "type": "string", - "enum": [ - "eq", - "ends_with", - "contains" - ], - "description": "How to compare the contact's address." - }, - "value": { - "type": "string", - "minLength": 1, - "maxLength": 254, - "description": "What to compare it with." - } - }, - "required": [ - "field", - "op", - "value" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "audience" - ] - }, - "op": { - "type": "string", - "enum": [ - "in", - "not_in" - ], - "description": "Whether the contact is in the audience." - }, - "audience_id": { - "type": "string", - "description": "The audience's id." - } - }, - "required": [ - "field", - "op", - "audience_id" - ], - "additionalProperties": false + "type": "string", + "maxLength": 2000 }, { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "topic" - ] - }, - "op": { - "type": "string", - "enum": [ - "subscribed", - "not_subscribed" - ], - "description": "Whether emails about the topic reach the contact." - }, - "topic": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The topic's key." - } - }, - "required": [ - "field", - "op", - "topic" - ], - "additionalProperties": false + "type": "number" }, { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "created_at" - ] - }, - "op": { - "type": "string", - "enum": [ - "before", - "after", - "within_days" - ], - "description": "`before` or `after` a time, or created `within_days` the last days." - }, - "value": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "integer", - "minimum": 1, - "maximum": 3650 - } - ], - "description": "A timestamp for `before`/`after`, a number of days for `within_days`." - } - }, - "required": [ - "field", - "op", - "value" - ], - "additionalProperties": false + "type": "boolean" }, { - "type": "object", - "properties": { - "match": { - "type": "string", - "enum": [ - "all", - "any" - ], - "description": "`all`: every condition must hold; `any`: one is enough." - }, - "conditions": { - "type": "array", - "items": { - "oneOf": [ - { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "property" - ] - }, - "key": { - "type": "string", - "description": "The custom property to test." - }, - "op": { - "type": "string", - "enum": [ - "eq", - "neq", - "in", - "not_in", - "gt", - "gte", - "lt", - "lte", - "contains", - "starts_with", - "exists", - "not_exists" - ], - "description": "How to compare: `eq`, `neq`, `in`, `not_in`, `gt`, `gte`, `lt`, `lte`, `contains`, `starts_with`, or `exists` and `not_exists`, which take no value." - }, - "value": { - "anyOf": [ - { - "type": "string", - "maxLength": 2000 - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "array", - "items": { - "anyOf": [ - { - "type": "string", - "maxLength": 2000 - }, - { - "type": "number" - }, - { - "type": "boolean" - } - ] - }, - "minItems": 1, - "maxItems": 100 - } - ], - "description": "A list for `in`/`not_in`; none for `exists`/`not_exists`." - } - }, - "required": [ - "field", - "key", - "op" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "email" - ] - }, - "op": { - "type": "string", - "enum": [ - "eq", - "ends_with", - "contains" - ], - "description": "How to compare the contact's address." - }, - "value": { - "type": "string", - "minLength": 1, - "maxLength": 254, - "description": "What to compare it with." - } - }, - "required": [ - "field", - "op", - "value" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "audience" - ] - }, - "op": { - "type": "string", - "enum": [ - "in", - "not_in" - ], - "description": "Whether the contact is in the audience." - }, - "audience_id": { - "type": "string", - "description": "The audience's id." - } - }, - "required": [ - "field", - "op", - "audience_id" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "topic" - ] - }, - "op": { - "type": "string", - "enum": [ - "subscribed", - "not_subscribed" - ], - "description": "Whether emails about the topic reach the contact." - }, - "topic": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The topic's key." - } - }, - "required": [ - "field", - "op", - "topic" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "created_at" - ] - }, - "op": { - "type": "string", - "enum": [ - "before", - "after", - "within_days" - ], - "description": "`before` or `after` a time, or created `within_days` the last days." - }, - "value": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "integer", - "minimum": 1, - "maximum": 3650 - } - ], - "description": "A timestamp for `before`/`after`, a number of days for `within_days`." - } - }, - "required": [ - "field", - "op", - "value" - ], - "additionalProperties": false - } - ] - }, - "minItems": 1, - "description": "The group's conditions." - } - }, - "required": [ - "match", - "conditions" - ], - "additionalProperties": false + "type": "null" } ] }, - "minItems": 1, - "description": "Conditions, or `{ match, conditions }` groups of them. At most 20 in all." - } - }, - "required": [ - "version", - "match", - "conditions" - ], - "additionalProperties": false, - "description": "Which contacts belong to the segment. Conditions on `property` (custom properties), `email`, `audience` membership, `topic` subscription and `created_at`; one level of nested `{ match, conditions }` groups." - }, - "CreateSegmentRequest": { - "type": "object", - "properties": { - "name": { + "description": "Custom properties by key (define them first, see contact properties). Merged into the contact's existing properties; `null` removes one.", + "examples": [ + { + "plan": "pro", + "seats": 5 + } + ] + }, + "status": { "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "Your name for the segment, unique in the environment." + "enum": [ + "subscribed", + "unsubscribed" + ], + "description": "Sets the contact's consent. Omitted, a new contact is `subscribed` and an existing one keeps its status." }, - "filter": { - "$ref": "#/components/schemas/SegmentFilter" + "audience_ids": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 20, + "description": "Audiences to add the contact to (it stays in the others)." + }, + "topics": { + "type": "array", + "items": { + "type": "object", + "properties": { + "topic": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The topic's key." + }, + "status": { + "type": "string", + "enum": [ + "opted_in", + "opted_out" + ], + "description": "`opted_in` or `opted_out` of the topic." + } + }, + "required": [ + "topic", + "status" + ], + "additionalProperties": false + }, + "maxItems": 100, + "description": "Choices per topic; topics left out keep the contact's choice." + }, + "double_opt_in": { + "type": "boolean", + "description": "Ask the contact to confirm first (default: the marketing settings' `double_opt_in_default`): a new or unsubscribed contact becomes `pending` and gets a confirmation email; it is `subscribed` once they click its link. Can't be combined with `status: \"subscribed\"`, nor can the default (pass `false` to subscribe directly)." } }, "required": [ - "name", - "filter" + "email" ], "additionalProperties": false, "examples": [ { - "name": "Pro customers", - "filter": { - "version": 1, - "match": "all", - "conditions": [ - { - "field": "property", - "key": "plan", - "op": "eq", - "value": "pro" - }, - { - "match": "any", - "conditions": [ - { - "field": "email", - "op": "ends_with", - "value": "@acme.com" - }, - { - "field": "audience", - "op": "in", - "audience_id": "aud_01k7c8m4q2f3e9v6t5r1n0p8yz" - } - ] - } - ] - } + "email": "ada@example.com", + "first_name": "Ada", + "properties": { + "plan": "pro" + }, + "topics": [ + { + "topic": "newsletter", + "status": "opted_in" + } + ] } ] }, - "UpdateSegmentRequest": { + "ContactConfirmation": { "type": "object", "properties": { - "name": { + "object": { "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "Your name for the segment, unique in the environment." + "enum": [ + "contact_confirmation" + ] }, - "filter": { - "$ref": "#/components/schemas/SegmentFilter" + "contact_id": { + "type": "string", + "description": "The contact's id." + }, + "status": { + "type": "string", + "enum": [ + "sent", + "cooldown", + "limit_reached", + "not_pending" + ], + "description": "`sent`; `cooldown` (one confirmation per contact per day); `limit_reached` (three were sent and none confirmed); `not_pending` (the contact isn't waiting to confirm)." } }, - "additionalProperties": false, + "required": [ + "object", + "contact_id", + "status" + ], "examples": [ { - "name": "Pro and Scale customers" + "object": "contact_confirmation", + "contact_id": "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", + "status": "sent" } ] }, - "DeletedSegment": { + "PreferenceLinks": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "segment" + "preference_links" ] }, - "id": { - "type": "string" + "contact_id": { + "type": "string", + "description": "The contact's id." }, - "deleted": { - "type": "boolean", - "enum": [ - true - ] + "preferences_url": { + "type": "string", + "description": "The contact's preference center." + }, + "unsubscribe_url": { + "type": "string", + "description": "A one-button unsubscribe page." } }, "required": [ "object", - "id", - "deleted" + "contact_id", + "preferences_url", + "unsubscribe_url" ], "examples": [ { - "object": "segment", - "id": "seg_01k7c8m4q2f3e9v6t5r1n0p8yz", - "deleted": true + "object": "preference_links", + "contact_id": "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", + "preferences_url": "https://track.cosslinks.com/t/u/aXyZ…", + "unsubscribe_url": "https://track.cosslinks.com/t/u/aXyZ…/unsubscribe" } ] }, - "TopicList": { + "CreatePreferenceLinksRequest": { + "type": "object", + "properties": { + "topic": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The topic your email is about: its unsubscribe link leaves just that topic." + } + }, + "additionalProperties": false, + "examples": [ + { + "topic": "newsletter" + } + ] + }, + "ContactList": { "type": "object", "properties": { "object": { @@ -26843,7 +27059,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Topic" + "$ref": "#/components/schemas/Contact" } }, "has_more": { @@ -26860,14 +27076,20 @@ "object": "list", "data": [ { - "object": "topic", - "id": "topic_01k7c8m4q2f3e9v6t5r1n0p8yz", - "key": "newsletter", - "name": "Newsletter", - "description": "Monthly product news.", - "default_subscription": "opt_in", - "visibility": "public", - "position": 0, + "object": "contact", + "id": "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", + "email": "ada@example.com", + "first_name": "Ada", + "last_name": "Lovelace", + "status": "subscribed", + "properties": { + "plan": "pro", + "seats": 5 + }, + "source": "api", + "subscribed_at": "2026-10-10T12:00:00.000Z", + "unsubscribed_at": null, + "confirmed_at": null, "created_at": "2026-10-10T12:00:00.000Z", "updated_at": "2026-10-10T12:00:00.000Z" } @@ -26876,202 +27098,265 @@ } ] }, - "Topic": { + "ContactImport": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "topic" + "contact_import" ] }, - "id": { - "type": "string" - }, - "key": { - "type": "string", - "description": "The `category` of the emails about this topic. Can't change." - }, - "name": { - "type": "string", - "description": "Shown in the preference center." - }, - "description": { - "type": [ - "string", - "null" - ], - "description": "Shown in the preference center under its name. `null`: none." - }, - "default_subscription": { - "type": "string", - "enum": [ - "opt_in", - "opt_out" - ], - "description": "`opt_in`: every subscribed contact gets the topic's emails unless they opt out. `opt_out`: only contacts who opted in do." - }, - "visibility": { - "type": "string", - "enum": [ - "public", - "private" - ], - "description": "`private` topics never show in the preference center." + "created": { + "type": "integer", + "description": "Contacts created." }, - "position": { + "updated": { "type": "integer", - "description": "Order in the preference center (ascending)." + "description": "Existing contacts updated." }, - "created_at": { - "type": "string", - "format": "date-time" + "kept_unsubscribed": { + "type": "integer", + "description": "Existing contacts the import marked `subscribed` that stay `unsubscribed`: an import never re-subscribes anyone." }, - "updated_at": { - "type": "string", - "format": "date-time" + "invalid": { + "type": "array", + "items": { + "type": "object", + "properties": { + "index": { + "type": "integer", + "description": "The entry's position in `contacts`, from 0." + }, + "email": { + "type": "string", + "description": "The address it gave." + }, + "message": { + "type": "string", + "description": "Why it was refused." + } + }, + "required": [ + "index", + "email", + "message" + ] + }, + "description": "Entries that were refused. The rest of the import still went through." } }, "required": [ "object", - "id", - "key", - "name", - "description", - "default_subscription", - "visibility", - "position", - "created_at", - "updated_at" + "created", + "updated", + "kept_unsubscribed", + "invalid" ], "examples": [ { - "object": "topic", - "id": "topic_01k7c8m4q2f3e9v6t5r1n0p8yz", - "key": "newsletter", - "name": "Newsletter", - "description": "Monthly product news.", - "default_subscription": "opt_in", - "visibility": "public", - "position": 0, - "created_at": "2026-10-10T12:00:00.000Z", - "updated_at": "2026-10-10T12:00:00.000Z" + "object": "contact_import", + "created": 1, + "updated": 1, + "kept_unsubscribed": 0, + "invalid": [ + { + "index": 2, + "email": "not-an-address", + "message": "Must be an email address." + } + ] } ] }, - "CreateTopicRequest": { + "ImportContactsRequest": { "type": "object", "properties": { - "key": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The `category` of the emails about this topic. Can't change." - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 200, - "description": "Shown in the preference center." - }, - "description": { - "type": [ - "string", - "null" - ], - "maxLength": 500, - "description": "Shown in the preference center under its name. `null`: none." - }, - "default_subscription": { - "type": "string", - "enum": [ - "opt_in", - "opt_out" - ], - "description": "`opt_in`: every subscribed contact gets the topic's emails unless they opt out. `opt_out`: only contacts who opted in do." + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "The contact's address (validated on its own)." + }, + "first_name": { + "type": [ + "string", + "null" + ], + "description": "Their first name." + }, + "last_name": { + "type": [ + "string", + "null" + ], + "description": "Their last name." + }, + "properties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "description": "Custom properties by key, checked against their definitions." + }, + "status": { + "type": "string", + "description": "`subscribed` or `unsubscribed` (anything else, like an export's `pending`, is reported in `invalid`). Omitted, a new contact is `subscribed` and an existing one keeps its status." + } + }, + "required": [ + "email" + ], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 1000, + "description": "The contacts to create or update, at most 1,000." }, - "visibility": { + "audience_id": { "type": "string", - "enum": [ - "public", - "private" - ], - "description": "`private` topics never show in the preference center (default `public`)." - }, - "position": { - "type": "integer", - "minimum": 0, - "maximum": 1000, - "description": "Order in the preference center (ascending)." + "description": "Adds every imported contact to this audience." } }, "required": [ - "key", - "name", - "default_subscription" + "contacts" ], "additionalProperties": false, "examples": [ { - "key": "newsletter", - "name": "Newsletter", - "default_subscription": "opt_in" + "contacts": [ + { + "email": "ada@example.com", + "first_name": "Ada", + "properties": { + "plan": "pro" + } + }, + { + "email": "grace@example.com", + "status": "unsubscribed" + } + ] } ] }, - "UpdateTopicRequest": { + "UpdateContactRequest": { "type": "object", "properties": { - "name": { - "type": "string", + "first_name": { + "type": [ + "string", + "null" + ], "minLength": 1, - "maxLength": 200, - "description": "Shown in the preference center." + "maxLength": 256, + "description": "Their first name. `null` removes it." }, - "description": { + "last_name": { "type": [ "string", "null" ], - "maxLength": 500, - "description": "Shown in the preference center under its name. `null`: none." + "minLength": 1, + "maxLength": 256, + "description": "Their last name. `null` removes it." }, - "default_subscription": { - "type": "string", - "enum": [ - "opt_in", - "opt_out" - ], - "description": "`opt_in`: every subscribed contact gets the topic's emails unless they opt out. `opt_out`: only contacts who opted in do." + "properties": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "description": "Custom properties by key (define them first, see contact properties). Merged into the contact's existing properties; `null` removes one.", + "examples": [ + { + "plan": "pro", + "seats": 5 + } + ] }, - "visibility": { + "status": { "type": "string", "enum": [ - "public", - "private" + "subscribed", + "unsubscribed" ], - "description": "`private` topics never show in the preference center (default `public`)." + "description": "Sets the contact's consent. Omitted, a new contact is `subscribed` and an existing one keeps its status." }, - "position": { - "type": "integer", - "minimum": 0, - "maximum": 1000, - "description": "Order in the preference center (ascending)." - } + "topics": { + "type": "array", + "items": { + "type": "object", + "properties": { + "topic": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The topic's key." + }, + "status": { + "type": "string", + "enum": [ + "opted_in", + "opted_out" + ], + "description": "`opted_in` or `opted_out` of the topic." + } + }, + "required": [ + "topic", + "status" + ], + "additionalProperties": false + }, + "maxItems": 100 + } }, "additionalProperties": false, "examples": [ { - "visibility": "private" + "last_name": "Byron", + "properties": { + "seats": null + } } ] }, - "DeletedTopic": { + "DeletedContact": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "topic" + "contact" ] }, "id": { @@ -27091,262 +27376,140 @@ ], "examples": [ { - "object": "topic", - "id": "topic_01k7c8m4q2f3e9v6t5r1n0p8yz", + "object": "contact", + "id": "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", "deleted": true } ] }, - "MarketingSettings": { + "ContactTopics": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "marketing_settings" + "contact_topics" ] }, - "display_name": { - "type": [ - "string", - "null" - ], - "description": "The sender's name in the preference center (default: the organization's)." - }, - "mailing_address": { - "type": [ - "string", - "null" - ], - "description": "The postal address every marketing email carries (CAN-SPAM). Marketing emails can't be sent without one." - }, - "double_opt_in_default": { - "type": "boolean", - "description": "Whether contacts upserted without `double_opt_in` must confirm first." - }, - "confirmation_from": { - "type": [ - "string", - "null" - ], - "description": "The sender of confirmation emails (a verified domain's address for live keys)." - }, - "confirmation_subject": { - "type": [ - "string", - "null" - ], - "description": "`null`: coss.com's default." - }, - "confirmation_html": { - "type": [ - "string", - "null" - ], - "description": "`null`: coss.com's default. Must contain `{{{confirmation_url}}}`." - }, - "confirmation_text": { - "type": [ - "string", - "null" - ], - "description": "`null`: coss.com's default. Must contain `{{{confirmation_url}}}`." + "contact_id": { + "type": "string", + "description": "The contact's id." }, - "confirmed_redirect_url": { - "type": [ - "string", - "null" - ], - "description": "Where confirming sends the contact (https); `null`: a coss.com confirmation page." + "topics": { + "type": "array", + "items": { + "type": "object", + "properties": { + "topic": { + "type": "string", + "description": "The topic's key." + }, + "name": { + "type": "string", + "description": "The topic's name." + }, + "subscribed": { + "type": "boolean", + "description": "Whether emails about the topic reach the contact (its choice, else the topic's default; an unsubscribed contact gets no marketing email at all)." + }, + "choice": { + "type": [ + "string", + "null" + ], + "enum": [ + "opted_in", + "opted_out", + null + ], + "description": "The contact's explicit choice; `null`: the topic's default applies." + } + }, + "required": [ + "topic", + "name", + "subscribed", + "choice" + ] + }, + "description": "Every topic of the environment, with this contact's standing." } }, "required": [ "object", - "display_name", - "mailing_address", - "double_opt_in_default", - "confirmation_from", - "confirmation_subject", - "confirmation_html", - "confirmation_text", - "confirmed_redirect_url" + "contact_id", + "topics" ], "examples": [ { - "object": "marketing_settings", - "display_name": "Acme", - "mailing_address": "Acme Inc., 548 Market St, San Francisco, CA 94104, USA", - "double_opt_in_default": true, - "confirmation_from": "Acme ", - "confirmation_subject": null, - "confirmation_html": null, - "confirmation_text": null, - "confirmed_redirect_url": "https://acme.com/welcome" + "object": "contact_topics", + "contact_id": "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", + "topics": [ + { + "topic": "newsletter", + "name": "Newsletter", + "subscribed": true, + "choice": null + }, + { + "topic": "product", + "name": "Product updates", + "subscribed": false, + "choice": "opted_out" + } + ] } ] }, - "UpdateMarketingSettingsRequest": { + "UpdateContactTopicsRequest": { "type": "object", "properties": { - "display_name": { - "type": [ - "string", - "null" - ], - "minLength": 1, - "maxLength": 200, - "description": "The sender's name in the preference center. `null`: the organization's." - }, - "mailing_address": { - "type": [ - "string", - "null" - ], - "minLength": 1, - "maxLength": 500, - "description": "The postal address every marketing email carries (CAN-SPAM). Marketing emails can't be sent without one." - }, - "double_opt_in_default": { - "type": "boolean", - "description": "Whether contacts upserted without `double_opt_in` must confirm first." - }, - "confirmation_from": { - "type": [ - "string", - "null" - ], - "description": "The sender of confirmation emails (a verified domain's address for live keys)." - }, - "confirmation_subject": { - "type": [ - "string", - "null" - ], - "minLength": 1, - "maxLength": 998, - "description": "`null`: coss.com's default." - }, - "confirmation_html": { - "type": [ - "string", - "null" - ], - "maxLength": 200000, - "description": "`null`: coss.com's default. Must contain `{{{confirmation_url}}}`." - }, - "confirmation_text": { - "type": [ - "string", - "null" - ], - "maxLength": 200000, - "description": "`null`: coss.com's default. Must contain `{{{confirmation_url}}}`." - }, - "confirmed_redirect_url": { - "type": [ - "string", - "null" - ], - "maxLength": 2000, - "format": "uri", - "description": "Where a confirmed contact is sent, an https URL. `null`: coss.com's own page." + "topics": { + "type": "array", + "items": { + "type": "object", + "properties": { + "topic": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The topic's key." + }, + "status": { + "type": "string", + "enum": [ + "opted_in", + "opted_out" + ], + "description": "`opted_in` or `opted_out` of the topic." + } + }, + "required": [ + "topic", + "status" + ], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 100, + "description": "The contact's choices per topic. Topics not listed keep theirs." } }, + "required": [ + "topics" + ], "additionalProperties": false, "examples": [ { - "mailing_address": "Acme Inc., 548 Market St, San Francisco, CA 94104, USA" - } - ] - }, - "Reputation": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "reputation" - ] - }, - "status": { - "type": "string", - "enum": [ - "good", - "warning", - "suspended" - ], - "description": "`good`, `warning` (a rate is approaching the suspension threshold) or `suspended` (live sending is refused until coss.com support reinstates the organization)." - }, - "reason": { - "type": [ - "string", - "null" - ], - "description": "The trigger behind a `warning` or `suspended` status.", - "example": "complaint_rate" - }, - "status_changed_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When `status` last changed. `null`: it never has." - }, - "evaluated_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When the checks below were computed; `null` before the first evaluation." - }, - "window": { - "type": [ - "object", - "null" - ], - "properties": { - "start": { - "type": "string", - "format": "date-time", - "description": "When the period starts." - }, - "end": { - "type": "string", - "format": "date-time", - "description": "When the period ends." + "topics": [ + { + "topic": "product", + "status": "opted_out" } - }, - "required": [ - "start", - "end" - ], - "description": "The period the checks cover (live environments only)." - }, - "checks": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ReputationCheck" - }, - "description": "The measurements behind `status`." - }, - "marketing": { - "$ref": "#/components/schemas/MarketingStanding" + ] } - }, - "required": [ - "object", - "status", - "reason", - "status_changed_at", - "evaluated_at", - "window", - "checks", - "marketing" ] }, - "LogList": { + "ContactPropertyList": { "type": "object", "properties": { "object": { @@ -27358,7 +27521,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Log" + "$ref": "#/components/schemas/ContactProperty" } }, "has_more": { @@ -27375,271 +27538,207 @@ "object": "list", "data": [ { - "object": "log", - "id": "log_01k7c8m4q2f3e9v6t5r1n0p8yz", - "occurred_at": "2026-01-15T09:30:00.000Z", - "source": "api", - "level": "info", - "type": "api.request", - "message": "POST /v1/emails 202", - "request_id": "req_01k7c8m4q2f3e9v6t5r1n0p8yz", - "api_key_id": "key_01k7c8m4q2f3e9v6t5r1n0p8yz", - "method": "POST", - "path": "/v1/emails", - "route": "/v1/emails", - "status_code": 202, - "duration_ms": 48, - "client_ip": "203.0.113.7", - "user_agent": "coss-node/0.1.0", - "error_code": null, - "resource_type": "email", - "resource_id": "email_01k7c8m4q2f3e9v6t5r1n0p8yz", - "recipient": null, - "attributes": { - "request": { - "body": { - "from": "hello@example.com", - "to": [ - "user@example.com" - ], - "subject": "Hi" - } - }, - "response": { - "body": { - "object": "email", - "id": "email_01k7c8m4q2f3e9v6t5r1n0p8yz" - } - } - } + "object": "contact_property", + "id": "cprop_01k7c8m4q2f3e9v6t5r1n0p8yz", + "key": "plan", + "type": "string", + "fallback": "free", + "created_at": "2026-10-10T12:00:00.000Z", + "updated_at": "2026-10-10T12:00:00.000Z" } ], "has_more": false } ] }, - "Log": { + "ContactProperty": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "log" + "contact_property" ] }, "id": { - "type": "string", - "examples": [ - "log_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "type": "string" }, - "occurred_at": { + "key": { "type": "string", - "format": "date-time", - "description": "When it happened." - }, - "source": { - "$ref": "#/components/schemas/LogSource" - }, - "level": { - "$ref": "#/components/schemas/LogLevel" + "description": "How contacts and templates name it (`{{{contact.}}}`). Can't change." }, "type": { "type": "string", - "description": "`api.request`, `webhook.attempt`, `webhook.forward` (an event `coss listen` forwarded), or an email event type such as `email.delivered`." - }, - "message": { - "type": "string", - "description": "A one-line summary." - }, - "request_id": { - "type": [ + "enum": [ "string", - "null" + "number", + "boolean", + "date" ], - "description": "The `X-Request-Id` of the API request this line belongs to." + "description": "`date` values are ISO 8601 strings." }, - "api_key_id": { - "type": [ - "string", - "null" + "fallback": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } ], - "description": "The API key that made the request. `null`: not an API request." + "description": "Used in templates for contacts without a value." }, - "method": { - "type": [ + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "object", + "id", + "key", + "type", + "fallback", + "created_at", + "updated_at" + ], + "examples": [ + { + "object": "contact_property", + "id": "cprop_01k7c8m4q2f3e9v6t5r1n0p8yz", + "key": "plan", + "type": "string", + "fallback": "free", + "created_at": "2026-10-10T12:00:00.000Z", + "updated_at": "2026-10-10T12:00:00.000Z" + } + ] + }, + "CreateContactPropertyRequest": { + "type": "object", + "properties": { + "key": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{0,49}$", + "description": "How contacts and templates name it. Lower-case letters, digits and `_`, starting with a letter. Can't change." + }, + "type": { + "type": "string", + "enum": [ "string", - "null" + "number", + "boolean", + "date" ], - "description": "The HTTP method. `null`: not an HTTP line." + "description": "What values it takes. `date` values are ISO 8601 strings. Can't change." }, - "path": { - "type": [ - "string", - "null" - ], - "description": "The request's path. `null`: not an API request." - }, - "route": { - "type": [ - "string", - "null" - ], - "description": "The matched route, e.g. `/v1/emails/:id`." - }, - "status_code": { - "type": [ - "integer", - "null" - ], - "description": "The API response status, or the webhook endpoint's response status." - }, - "duration_ms": { - "type": [ - "integer", - "null" - ], - "description": "How long it took, in milliseconds. `null`: not timed." - }, - "client_ip": { - "type": [ - "string", - "null" - ], - "description": "The caller's address. `null`: not an API request." - }, - "user_agent": { - "type": [ - "string", - "null" - ], - "description": "The caller's `User-Agent`. `null`: not sent." - }, - "error_code": { - "type": [ - "string", - "null" - ], - "description": "The error's `code`. `null`: no error." - }, - "resource_type": { - "type": [ - "string", - "null" + "fallback": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } ], - "description": "What the line is about: `email`, `domain`, `webhook_endpoint`…" - }, - "resource_id": { - "type": [ - "string", - "null" + "description": "Used in templates for contacts without a value." + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false, + "examples": [ + { + "key": "plan", + "type": "string", + "fallback": "free" + } + ] + }, + "UpdateContactPropertyRequest": { + "type": "object", + "properties": { + "fallback": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } ], - "description": "The id of what it's about. `null`: nothing in particular." + "description": "Used in templates for contacts without a value. `null` removes it." + } + }, + "required": [ + "fallback" + ], + "additionalProperties": false, + "examples": [ + { + "fallback": "starter" + } + ] + }, + "DeletedContactProperty": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "contact_property" + ] }, - "recipient": { - "type": [ - "string", - "null" - ], - "description": "The recipient an email event is about. `null`: none." + "id": { + "type": "string" }, - "attributes": { - "type": "object", - "additionalProperties": {}, - "description": "Details: redacted request and response bodies for API requests (secrets, attachment contents and long strings are never stored), event data for emails and webhooks." + "deleted": { + "type": "boolean", + "enum": [ + true + ] } }, "required": [ "object", "id", - "occurred_at", - "source", - "level", - "type", - "message", - "request_id", - "api_key_id", - "method", - "path", - "route", - "status_code", - "duration_ms", - "client_ip", - "user_agent", - "error_code", - "resource_type", - "resource_id", - "recipient", - "attributes" + "deleted" ], "examples": [ { - "object": "log", - "id": "log_01k7c8m4q2f3e9v6t5r1n0p8yz", - "occurred_at": "2026-01-15T09:30:00.000Z", - "source": "api", - "level": "info", - "type": "api.request", - "message": "POST /v1/emails 202", - "request_id": "req_01k7c8m4q2f3e9v6t5r1n0p8yz", - "api_key_id": "key_01k7c8m4q2f3e9v6t5r1n0p8yz", - "method": "POST", - "path": "/v1/emails", - "route": "/v1/emails", - "status_code": 202, - "duration_ms": 48, - "client_ip": "203.0.113.7", - "user_agent": "coss-node/0.1.0", - "error_code": null, - "resource_type": "email", - "resource_id": "email_01k7c8m4q2f3e9v6t5r1n0p8yz", - "recipient": null, - "attributes": { - "request": { - "body": { - "from": "hello@example.com", - "to": [ - "user@example.com" - ], - "subject": "Hi" - } - }, - "response": { - "body": { - "object": "email", - "id": "email_01k7c8m4q2f3e9v6t5r1n0p8yz" - } - } - } + "object": "contact_property", + "id": "cprop_01k7c8m4q2f3e9v6t5r1n0p8yz", + "deleted": true } ] }, - "LogSource": { - "type": "string", - "enum": [ - "api", - "email", - "webhook", - "risk", - "smtp", - "sms", - "sign", - "verify", - "forms", - "scheduling" - ], - "description": "`api`: a request to the API. `email`: an email event. `webhook`: a delivery attempt. `risk`: a risk assessment or its feedback. `smtp`: a message submitted to the SMTP relay. `sms`: an SMS event." - }, - "LogLevel": { - "type": "string", - "enum": [ - "info", - "warning", - "error" - ], - "description": "`error`: 5xx responses, bounces, complaints, failures. `warning`: 4xx responses, delays, suppressions, failed webhook attempts that will be retried. `info`: everything else." - }, - "AuditEventList": { + "BroadcastList": { "type": "object", "properties": { "object": { @@ -27651,7 +27750,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/AuditEvent" + "$ref": "#/components/schemas/Broadcast" } }, "has_more": { @@ -27668,871 +27767,631 @@ "object": "list", "data": [ { - "object": "audit_event", - "id": "oaudit_01k7c8m4q2f3e9v6t5r1n0p8yz", - "created_at": "2026-01-15T09:30:00.000Z", - "actor_type": "api_key", - "actor_id": "key_01k7c8m4q2f3e9v6t5r1n0p8yz", - "actor_label": "key_01k7c8m4q2f3e9v6t5r1n0p8yz", - "action": "suppression.created", - "target_type": "suppression", - "target_id": "sup_01k7c8m4q2f3e9v6t5r1n0p8yz", - "target_label": "user@example.com", - "project_id": "proj_01k7c8m4q2f3e9v6t5r1n0p8yz", - "environment_id": "env_01k7c8m4q2f3e9v6t5r1n0p8yz", - "metadata": { - "scope": "*", - "request_id": "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + "object": "broadcast", + "id": "bcast_01k7c8m4q2f3e9v6t5r1n0p8yz", + "name": "October newsletter", + "status": "sent", + "audience_id": null, + "segment_id": "seg_01k7c8m4q2f3e9v6t5r1n0p8yz", + "topic": "newsletter", + "from": "Acme ", + "reply_to": [], + "subject": "What's new, {{{contact.first_name|there}}}", + "preview_text": null, + "html": "

Hi {{{contact.first_name|there}}},

", + "text": null, + "tracking": { + "opens": true, + "clicks": true }, - "client_ip": "203.0.113.7", - "user_agent": "coss-node/0.1.0" + "rate_per_second": null, + "scheduled_at": "2026-10-20T09:00:00.000Z", + "started_at": "2026-10-20T09:00:01.000Z", + "completed_at": "2026-10-20T09:12:40.000Z", + "canceled_at": null, + "recipients_estimated": 1200, + "pause_reason": null, + "error": null, + "stats": { + "recipients": 1200, + "sent": 1200, + "delivered": 1180, + "bounced": 6, + "complained": 1, + "failed": 0, + "opened": 540, + "clicked": 120, + "unsubscribed": 4 + }, + "created_at": "2026-10-15T12:00:00.000Z", + "updated_at": "2026-10-20T09:12:40.000Z" } ], "has_more": false } ] }, - "AuditEvent": { + "Broadcast": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "audit_event" + "broadcast" ] }, "id": { + "type": "string" + }, + "name": { "type": "string", - "examples": [ - "oaudit_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "description": "Shown only to you." }, - "created_at": { + "status": { "type": "string", - "format": "date-time" + "enum": [ + "draft", + "scheduled", + "sending", + "paused", + "sent", + "canceled", + "failed" + ], + "description": "`draft` (editable), `scheduled`, `sending`, `paused` (see `pause_reason`), `sent`, `canceled`, or `failed` (see `error`)." }, - "actor_type": { - "$ref": "#/components/schemas/AuditActorType" + "audience_id": { + "type": [ + "string", + "null" + ], + "description": "The audience it goes to. `null`: a segment." }, - "actor_id": { + "segment_id": { "type": [ "string", "null" ], - "description": "The user's or API key's id; `null` for the system." + "description": "The segment it goes to. `null`: an audience." }, - "actor_label": { + "topic": { + "type": [ + "string", + "null" + ], + "description": "The topic it's about (its key), and its emails' `category`. `null`: none." + }, + "from": { "type": "string", - "description": "Who made the change, copied when it was recorded so it outlives them: the user's email, the API key's id, or `coss.com support`." + "description": "Who it's sent from." }, - "action": { + "reply_to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Addresses replies go to." + }, + "subject": { "type": "string", - "description": "`.`, e.g. `api_key.created`, `domain.deleted`, `member.role_changed`." + "description": "The subject, before contact fields are filled in." }, - "target_type": { + "preview_text": { "type": [ "string", "null" ], - "description": "What was changed: `domain`, `api_key`, `member`…" + "description": "The inbox preview line. `null`: none." }, - "target_id": { + "html": { "type": [ "string", "null" ], - "description": "The id of what it acted on. `null`: nothing in particular." + "description": "The HTML part, before contact fields are filled in. `null`: text only." }, - "target_label": { + "text": { "type": [ "string", "null" ], - "description": "A readable name for the target when it was changed." + "description": "The plain text part. `null`: HTML only." }, - "project_id": { + "tracking": { + "type": "object", + "properties": { + "opens": { + "type": "boolean", + "description": "Whether opens are tracked." + }, + "clicks": { + "type": "boolean", + "description": "Whether clicks are tracked." + } + }, + "required": [ + "opens", + "clicks" + ], + "description": "Which opens and clicks are tracked." + }, + "rate_per_second": { "type": [ - "string", + "integer", "null" ], - "description": "`null` for organization-wide changes (domains, members…)." + "description": "Recipients per second. `null`: the plan's marketing rate." }, - "environment_id": { + "scheduled_at": { "type": [ "string", "null" ], - "description": "`null` for organization-wide changes (domains, members…)." + "format": "date-time", + "description": "When it's set to start. `null`: not scheduled." }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "description": "Details of the change; API changes include their `request_id`." + "started_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When sending started. `null`: it hasn't." }, - "client_ip": { + "completed_at": { "type": [ "string", "null" ], - "description": "The actor's address. `null`: unknown." + "format": "date-time", + "description": "When it finished. `null`: it hasn't." }, - "user_agent": { + "canceled_at": { "type": [ "string", "null" ], - "description": "The actor's `User-Agent`. `null`: unknown." - } - }, - "required": [ - "object", - "id", - "created_at", - "actor_type", - "actor_id", - "actor_label", - "action", - "target_type", - "target_id", - "target_label", - "project_id", - "environment_id", - "metadata", - "client_ip", - "user_agent" - ], - "examples": [ - { - "object": "audit_event", - "id": "oaudit_01k7c8m4q2f3e9v6t5r1n0p8yz", - "created_at": "2026-01-15T09:30:00.000Z", - "actor_type": "api_key", - "actor_id": "key_01k7c8m4q2f3e9v6t5r1n0p8yz", - "actor_label": "key_01k7c8m4q2f3e9v6t5r1n0p8yz", - "action": "suppression.created", - "target_type": "suppression", - "target_id": "sup_01k7c8m4q2f3e9v6t5r1n0p8yz", - "target_label": "user@example.com", - "project_id": "proj_01k7c8m4q2f3e9v6t5r1n0p8yz", - "environment_id": "env_01k7c8m4q2f3e9v6t5r1n0p8yz", - "metadata": { - "scope": "*", - "request_id": "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - }, - "client_ip": "203.0.113.7", - "user_agent": "coss-node/0.1.0" - } - ] - }, - "AuditActorType": { - "type": "string", - "enum": [ - "user", - "api_key", - "system" - ], - "description": "`user`: a member, in the dashboard (`actor_id` is the user's id, `actor_label` their email). `api_key`: a request made with an API key (`actor_id` is the key's id). `system`: coss.com itself, including coss.com support." - }, - "Billing": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "billing" - ] + "format": "date-time", + "description": "When it was canceled. `null`: it wasn't." }, - "organization_id": { - "type": "string", - "description": "The organization's id." + "recipients_estimated": { + "type": [ + "integer", + "null" + ], + "description": "How many contacts it would reach, counted when it started." }, - "plan": { - "type": "string", - "enum": [ - "free", - "pro", - "scale", - "enterprise" + "pause_reason": { + "type": [ + "string", + "null" ], - "description": "The plan in effect now (`free` without a contract)." + "description": "Why a paused broadcast waits: `paused` (by you), `billing_hold`, `budget_exceeded`, `usage_limit`, `marketing_paused`, `account_suspended`, `sending_paused`." }, - "contract": { + "error": { "type": [ "object", "null" ], "properties": { - "id": { - "type": "string", - "description": "The contract's id." - }, - "kind": { + "code": { "type": "string", - "enum": [ - "self_serve", - "sales", - "internal" - ], - "description": "`self_serve`: chosen in the dashboard. `sales`: agreed with coss.com. `internal`: coss.com's own." + "description": "Machine-readable reason." }, - "effective_from": { + "message": { "type": "string", - "format": "date-time", - "description": "When it took effect." - }, - "effective_until": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When it ends. `null`: no end is set." - }, - "term_end": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "End of a committed term." - }, - "auto_renew": { - "type": "boolean", - "description": "Whether a committed term renews when it ends." + "description": "Human-readable explanation." } }, "required": [ - "id", - "kind", - "effective_from", - "effective_until", - "term_end", - "auto_renew" + "code", + "message" ], - "description": "The contract in effect; `null` on the Free plan." + "description": "Why it `failed`, such as `target_unavailable` when its audience, segment or topic was deleted while sending. `null`: it didn't." }, - "scheduled_change": { - "type": [ - "object", - "null" - ], - "properties": { - "effective_at": { - "type": "string", - "format": "date-time", - "description": "When it takes effect." - }, - "plan": { - "type": "string", - "enum": [ - "free", - "pro", - "scale", - "enterprise" - ], - "description": "The plan it changes to." - } - }, - "required": [ - "effective_at", - "plan" - ], - "description": "A plan change that takes effect later (a downgrade, a cancellation)." + "stats": { + "$ref": "#/components/schemas/BroadcastStats" }, - "status": { + "created_at": { "type": "string", - "enum": [ - "active", - "past_due", - "delinquent" - ], - "description": "`past_due`: an invoice is overdue; `delinquent`: past its grace period, and live sending is held." + "format": "date-time" }, - "holds": { - "type": "array", - "items": { - "type": "object", - "properties": { - "reason": { - "type": "string", - "enum": [ - "delinquent", - "spend_limit", - "credit_limit", - "admin" - ], - "description": "`delinquent`: an invoice is past its grace period. `spend_limit`: the spend limit was reached. `credit_limit`: the credit limit was reached, or a usage charge failed. `admin`: placed by coss.com." - }, - "since": { - "type": "string", - "format": "date-time", - "description": "When the hold began." - } - }, - "required": [ - "reason", - "since" - ] + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "object", + "id", + "name", + "status", + "audience_id", + "segment_id", + "topic", + "from", + "reply_to", + "subject", + "preview_text", + "html", + "text", + "tracking", + "rate_per_second", + "scheduled_at", + "started_at", + "completed_at", + "canceled_at", + "recipients_estimated", + "pause_reason", + "error", + "stats", + "created_at", + "updated_at" + ], + "examples": [ + { + "object": "broadcast", + "id": "bcast_01k7c8m4q2f3e9v6t5r1n0p8yz", + "name": "October newsletter", + "status": "sent", + "audience_id": null, + "segment_id": "seg_01k7c8m4q2f3e9v6t5r1n0p8yz", + "topic": "newsletter", + "from": "Acme ", + "reply_to": [], + "subject": "What's new, {{{contact.first_name|there}}}", + "preview_text": null, + "html": "

Hi {{{contact.first_name|there}}},

", + "text": null, + "tracking": { + "opens": true, + "clicks": true }, - "description": "Billing holds on live sending (sandbox keys keep working)." + "rate_per_second": null, + "scheduled_at": "2026-10-20T09:00:00.000Z", + "started_at": "2026-10-20T09:00:01.000Z", + "completed_at": "2026-10-20T09:12:40.000Z", + "canceled_at": null, + "recipients_estimated": 1200, + "pause_reason": null, + "error": null, + "stats": { + "recipients": 1200, + "sent": 1200, + "delivered": 1180, + "bounced": 6, + "complained": 1, + "failed": 0, + "opened": 540, + "clicked": 120, + "unsubscribed": 4 + }, + "created_at": "2026-10-15T12:00:00.000Z", + "updated_at": "2026-10-20T09:12:40.000Z" + } + ] + }, + "CreateBroadcastRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Shown only to you." }, - "currency": { + "audience_id": { "type": "string", - "enum": [ - "usd" - ], - "description": "Always `usd`." + "description": "Send to this audience…" }, - "period": { - "type": "object", - "properties": { - "start": { - "type": "string", - "format": "date-time", - "description": "When the period starts (inclusive)." - }, - "end": { - "type": "string", - "format": "date-time", - "description": "When the period ends (exclusive)." - } - }, - "required": [ - "start", - "end" - ], - "description": "The current billing month (UTC)." + "segment_id": { + "type": "string", + "description": "…or to this segment (one of the two)." }, - "usage": { + "topic": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The topic the broadcast is about (its key): only contacts it reaches get it, and its unsubscribe link leaves just that topic. It is the emails' `category`." + }, + "from": { + "type": "string", + "examples": [ + "Acme " + ] + }, + "reply_to": { "type": "array", "items": { - "type": "object", - "properties": { - "meter": { - "type": "string", - "enum": [ - "email.recipients", - "email.dedicated_ip", - "email.attachment_data", - "email.received", - "email.received_data", - "email.inboxes", - "sms.segments", - "sms.received_segments", - "sms.numbers", - "sms.registrations", - "sign.completed_requests", - "risk.phone_checks", - "risk.assessments", - "risk.content_checks", - "verify.verifications", - "verify.sms", - "forms.submissions", - "forms.file_uploads", - "scheduling.bookings", - "scheduling.hosts", - "hooks.messages" - ], - "description": "What is counted, e.g. `email.recipients`." - }, - "quantity": { - "type": "integer", - "minimum": 0, - "description": "Units used so far this month." - }, - "included": { - "type": [ - "integer", - "null" - ], - "minimum": 0, - "description": "The plan's monthly allowance where usage stops (Free), else `null`." - } - }, - "required": [ - "meter", - "quantity", - "included" - ] + "type": "string" }, - "description": "This month's usage per meter." + "maxItems": 10, + "description": "Addresses replies go to." }, - "estimate": { + "subject": { + "type": "string", + "minLength": 1, + "maxLength": 998, + "description": "May use `{{{contact.|fallback}}}` (e.g. `{{{contact.first_name|there}}}`)." + }, + "preview_text": { + "type": "string", + "maxLength": 500, + "description": "The inbox preview line (a hidden preheader at the start of the HTML)." + }, + "html": { + "type": "string", + "maxLength": 1000000, + "description": "`{{{contact.|fallback}}}` for contact fields and properties (escaped), and `{{{unsubscribe_url}}}`, `{{{preferences_url}}}`, `{{{mailing_address}}}`; a footer with the postal address and an unsubscribe link is added unless they are there." + }, + "text": { + "type": "string", + "maxLength": 1000000, + "description": "The plain text part. Takes the same placeholders as `html`." + }, + "tracking": { "type": "object", "properties": { - "usage": { - "type": "integer", - "description": "Usage this month at your rates." - }, - "discount": { - "type": "integer", - "description": "An amount in cents (USD)." - }, - "credit": { - "type": "integer", - "description": "Credit applied to this estimate (your plan's fee is credit)." + "opens": { + "type": "boolean", + "description": "Track opens with a pixel." }, - "total": { - "type": "integer", - "description": "Estimated total including usage and fees billed in arrears, after discounts and credits." + "clicks": { + "type": "boolean", + "description": "Track clicks by rewriting links." } }, - "required": [ - "usage", - "discount", - "credit", - "total" - ], - "description": "This month so far, as it would be invoiced if it ended now." + "additionalProperties": false, + "description": "Which opens and clicks are tracked." }, - "credit_remaining": { + "rate_per_second": { "type": "integer", - "description": "Unused credit usable this month." + "minimum": 1, + "maximum": 1000, + "description": "Recipients per second, at most the plan's marketing rate (the default)." }, - "spend_limit": { + "send": { + "type": "boolean", + "description": "Start sending right away." + }, + "scheduled_at": { + "type": "string", + "format": "date-time", + "description": "Start sending at this time (ISO 8601 with an offset, at most 30 days ahead)." + } + }, + "required": [ + "name", + "from", + "subject" + ], + "additionalProperties": false, + "examples": [ + { + "name": "October newsletter", + "segment_id": "seg_01k7c8m4q2f3e9v6t5r1n0p8yz", + "topic": "newsletter", + "from": "Acme ", + "subject": "What's new, {{{contact.first_name|there}}}", + "html": "

Hi {{{contact.first_name|there}}},

…

Unsubscribe

", + "scheduled_at": "2026-10-20T09:00:00Z" + } + ] + }, + "UpdateBroadcastRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Shown only to you." + }, + "audience_id": { "type": [ - "integer", + "string", "null" ], - "description": "Usage beyond the credit past which live sending is held; `null`: none." + "description": "Send to this audience. `null` clears it." }, - "credit_limit": { + "segment_id": { "type": [ - "object", + "string", "null" ], - "properties": { - "limit": { - "type": "integer", - "description": "What coss.com carries before live sending is held. It rises as payments settle." - }, - "used": { - "type": "integer", - "description": "Usage not invoiced yet, unpaid invoices and recent usage charges." - }, - "charge_threshold": { - "type": [ - "integer", - "null" - ], - "description": "Usage not invoiced yet is charged early at this amount; `null`: it isn't charged automatically." - } - }, - "required": [ - "limit", - "used", - "charge_threshold" - ], - "description": "Where the organization stands against its credit limit; `null`: it has none." + "description": "Send to this segment. `null` clears it." }, - "payment_method": { + "topic": { "type": [ - "object", + "string", "null" ], - "properties": { - "type": { - "type": "string", - "description": "Its type, such as `card`." - }, - "brand": { - "type": [ - "string", - "null" - ], - "description": "The card's brand. `null` for other types." - }, - "last4": { - "type": [ - "string", - "null" - ], - "description": "The card's last four digits. `null` for other types." - } - }, - "required": [ - "type", - "brand", - "last4" - ], - "description": "The payment method on file. `null`: none." + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The topic it's about (its key). `null` clears it." }, - "add_ons": { + "from": { + "type": "string", + "examples": [ + "Acme " + ] + }, + "reply_to": { "type": "array", "items": { - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "e.g. `dedicated_ips`." - }, - "name": { - "type": "string", - "description": "The add-on's name." - }, - "quantity": { - "type": "integer", - "description": "How many units are on the contract." - }, - "unit_amount": { - "type": "integer", - "description": "The price per unit and interval, in cents." - }, - "interval": { - "type": "string", - "enum": [ - "month", - "quarter", - "year" - ], - "description": "How often it's charged." - } - }, - "required": [ - "key", - "name", - "quantity", - "unit_amount", - "interval" - ] + "type": "string" }, - "description": "Add-ons of the contract in effect, charged on top of the plan." + "maxItems": 10, + "description": "Addresses replies go to." }, - "features": { + "subject": { + "type": "string", + "minLength": 1, + "maxLength": 998, + "description": "May use `{{{contact.|fallback}}}` (e.g. `{{{contact.first_name|there}}}`)." + }, + "preview_text": { + "type": [ + "string", + "null" + ], + "maxLength": 500, + "description": "The inbox preview line. `null` removes it." + }, + "html": { + "type": [ + "string", + "null" + ], + "maxLength": 1000000, + "description": "The HTML part. `null` removes it, if `text` remains." + }, + "text": { + "type": [ + "string", + "null" + ], + "maxLength": 1000000, + "description": "The plain text part. `null` removes it, if `html` remains." + }, + "tracking": { "type": "object", "properties": { - "tracking_domains": { + "opens": { "type": "boolean", - "description": "Custom tracking domains." + "description": "Track opens with a pixel." }, - "audit_log": { - "type": "boolean", - "description": "The organization's audit log." - }, - "log_drains": { - "type": "boolean", - "description": "Log drains." - }, - "sso": { - "type": "boolean", - "description": "Single sign-on." - }, - "scim": { - "type": "boolean", - "description": "Directory sync (SCIM)." - }, - "dedicated_ips": { + "clicks": { "type": "boolean", - "description": "Dedicated IP pools." + "description": "Track clicks by rewriting links." } }, - "required": [ - "tracking_domains", - "audit_log", - "log_drains", - "sso", - "scim", - "dedicated_ips" - ], - "description": "Which features the plan includes." + "additionalProperties": false, + "description": "Which opens and clicks are tracked." }, - "limits": { - "type": "object", - "properties": { - "domains": { - "type": [ - "integer", - "null" - ], - "description": "Sending domains. `null`: unlimited." - }, - "members": { - "type": [ - "integer", - "null" - ], - "description": "Members of the organization. `null`: unlimited." - }, - "webhook_endpoints_per_environment": { - "type": [ - "integer", - "null" - ], - "description": "Webhook endpoints per environment. `null`: unlimited." - }, - "contacts_per_environment": { - "type": [ - "integer", - "null" - ], - "description": "Contacts per environment. `null`: unlimited." - }, - "inboxes": { - "type": [ - "integer", - "null" - ], - "description": "Inboxes. `null`: unlimited." - } - }, - "required": [ - "domains", - "members", - "webhook_endpoints_per_environment", - "contacts_per_environment", - "inboxes" + "rate_per_second": { + "type": [ + "integer", + "null" ], - "description": "Resource limits (`null`: unlimited)." + "minimum": 1, + "maximum": 1000, + "description": "Recipients per second. `null`: the plan's marketing rate." } }, - "required": [ - "object", - "organization_id", - "plan", - "contract", - "scheduled_change", - "status", - "holds", - "currency", - "period", - "usage", - "estimate", - "credit_remaining", - "spend_limit", - "credit_limit", - "payment_method", - "add_ons", - "features", - "limits" - ], + "additionalProperties": false, "examples": [ { - "object": "billing", - "organization_id": "org_01k7c8m4q2f3e9v6t5r1n0p8yz", - "plan": "pro", - "contract": { - "id": "bcon_01k7c8m4q2f3e9v6t5r1n0p8yz", - "kind": "self_serve", - "effective_from": "2026-10-01T00:00:00.000Z", - "effective_until": null, - "term_end": null, - "auto_renew": false - }, - "scheduled_change": null, - "status": "active", - "holds": [], - "currency": "usd", - "period": { - "start": "2026-10-01T00:00:00.000Z", - "end": "2026-11-01T00:00:00.000Z" - }, - "usage": [ - { - "meter": "email.recipients", - "quantity": 120000, - "included": null - } - ], - "estimate": { - "usage": 4800, - "discount": 0, - "credit": 2500, - "total": 2300 - }, - "credit_remaining": 0, - "spend_limit": null, - "credit_limit": { - "limit": 50000, - "used": 2300, - "charge_threshold": 10000 - }, - "payment_method": { - "type": "card", - "brand": "visa", - "last4": "4242" - }, - "add_ons": [], - "features": { - "tracking_domains": true, - "audit_log": true, - "log_drains": false, - "sso": false, - "scim": false, - "dedicated_ips": false - }, - "limits": { - "domains": 10, - "members": 10, - "webhook_endpoints_per_environment": 10, - "contacts_per_environment": 50000, - "inboxes": 1000 - } + "subject": "Our October news" } ] }, - "UsageList": { + "DeletedBroadcast": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "list" + "broadcast" ] }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Usage" - } + "id": { + "type": "string" }, - "has_more": { + "deleted": { "type": "boolean", "enum": [ - false + true ] } }, "required": [ "object", - "data", - "has_more" + "id", + "deleted" ], "examples": [ { - "object": "list", - "data": [ - { - "object": "usage", - "meter": "email.recipients", - "dimension": null, - "environment_id": null, - "project_id": null, - "api_key_id": null, - "user_id": null, - "start": "2026-10-12T00:00:00.000Z", - "end": "2026-10-13T00:00:00.000Z", - "quantity": 4200 - } - ], - "has_more": false + "object": "broadcast", + "id": "bcast_01k7c8m4q2f3e9v6t5r1n0p8yz", + "deleted": true } ] }, - "Usage": { + "SendBroadcastRequest": { + "type": "object", + "properties": { + "scheduled_at": { + "type": "string", + "format": "date-time", + "description": "Omit to start right away." + } + }, + "additionalProperties": false, + "examples": [ + { + "scheduled_at": "2026-10-20T09:00:00Z" + } + ] + }, + "BroadcastTest": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "usage" + "broadcast_test" ] }, - "meter": { - "type": "string", - "enum": [ - "email.recipients", - "email.dedicated_ip", - "email.attachment_data", - "email.received", - "email.received_data", - "email.inboxes", - "sms.segments", - "sms.received_segments", - "sms.numbers", - "sms.registrations", - "sign.completed_requests", - "risk.phone_checks", - "risk.assessments", - "risk.content_checks", - "verify.verifications", - "verify.sms", - "forms.submissions", - "forms.file_uploads", - "scheduling.bookings", - "scheduling.hosts", - "hooks.messages" - ], - "description": "What is counted, e.g. `email.recipients`." - }, - "dimension": { - "type": [ - "string", - "null" - ], - "description": "e.g. `country=US` for SMS; `null` for none." - }, - "environment_id": { - "type": [ - "string", - "null" - ], - "description": "Set with `group_by=environment`; `null` there for organization-wide usage (SMS numbers and registration fees)." - }, - "project_id": { - "type": [ - "string", - "null" - ], - "description": "Set with `group_by=project` (`null`: organization-wide usage, or usage recorded before projects were)." - }, - "api_key_id": { - "type": [ - "string", - "null" - ], - "description": "Set with `group_by=api_key` (`null`: the dashboard, or usage recorded before keys were)." - }, - "user_id": { - "type": [ - "string", - "null" - ], - "description": "Set with `group_by=user`: the member whose personal API key caused it (`null`: organization keys and the dashboard)." - }, - "start": { - "type": "string", - "format": "date-time", - "description": "When this bucket starts (inclusive)." + "email_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The ids of the test emails." + } + }, + "required": [ + "object", + "email_ids" + ], + "examples": [ + { + "object": "broadcast_test", + "email_ids": [ + "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + ] + }, + "TestBroadcastRequest": { + "type": "object", + "properties": { + "to": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 5, + "description": "Who gets the test, at most 5 addresses." }, - "end": { + "contact": { "type": "string", - "format": "date-time", - "description": "When this bucket ends (exclusive)." - }, - "quantity": { - "type": "integer", - "minimum": 0, - "description": "Units used in this bucket." + "description": "A contact (id or address) whose values fill the template; default: fallbacks." } }, "required": [ - "object", - "meter", - "dimension", - "environment_id", - "project_id", - "api_key_id", - "user_id", - "start", - "end", - "quantity" + "to" ], + "additionalProperties": false, "examples": [ { - "object": "usage", - "meter": "email.recipients", - "dimension": null, - "environment_id": null, - "project_id": null, - "api_key_id": null, - "user_id": null, - "start": "2026-10-12T00:00:00.000Z", - "end": "2026-10-13T00:00:00.000Z", - "quantity": 4200 + "to": [ + "me@acme.com" + ] } ] }, - "InvoiceList": { + "BroadcastEmailList": { "type": "object", "properties": { "object": { @@ -28544,7 +28403,32 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/InvoiceSummary" + "$ref": "#/components/schemas/Email" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ] + }, + "AudienceList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Audience" } }, "has_more": { @@ -28561,412 +28445,205 @@ "object": "list", "data": [ { - "object": "invoice", - "id": "inv_01k7c8m4q2f3e9v6t5r1n0p8yz", - "number": "COSS-000042", - "kind": "period", - "status": "paid", - "currency": "usd", - "period": { - "start": "2026-10-01T00:00:00.000Z", - "end": "2026-11-01T00:00:00.000Z" - }, - "subtotal": 7300, - "discount": 0, - "credit": 2500, - "tax": 0, - "total": 4800, - "amount_paid": 4800, - "amount_due": 0, - "collection_method": "charge_automatically", - "due_at": "2026-11-01T00:20:00.000Z", - "finalized_at": "2026-11-01T00:20:00.000Z", - "paid_at": "2026-11-01T00:21:00.000Z", - "voided_at": null, - "hosted_url": "https://invoice.stripe.com/i/acct_123/test_YWNjdF8x", - "pdf_url": "https://pay.stripe.com/invoice/acct_123/test_YWNjdF8x/pdf" + "object": "audience", + "id": "aud_01k7c8m4q2f3e9v6t5r1n0p8yz", + "name": "Beta testers", + "contact_count": 42, + "created_at": "2026-10-10T12:00:00.000Z", + "updated_at": "2026-10-10T12:00:00.000Z" } ], "has_more": false } ] }, - "Invoice": { + "Audience": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "invoice" + "audience" ] }, "id": { "type": "string" }, - "number": { - "type": [ - "string", - "null" - ], - "description": "The invoice number. `null` until it's finalized." - }, - "kind": { - "type": "string", - "enum": [ - "period", - "plan_change", - "one_off", - "final", - "threshold" - ], - "description": "`period`: a billing month. `plan_change`: a change of plan. `one_off`: a single charge. `final`: the last invoice of an account. `threshold`: usage charged early, taken off the month's invoice." - }, - "status": { - "type": "string", - "enum": [ - "open", - "paid", - "void", - "uncollectible" - ], - "description": "`open`: not paid yet. `paid`: settled. `void`: canceled. `uncollectible`: written off." - }, - "currency": { + "name": { "type": "string", - "enum": [ - "usd" - ], - "description": "Always `usd`." - }, - "period": { - "type": "object", - "properties": { - "start": { - "type": "string", - "format": "date-time", - "description": "When the period starts (inclusive)." - }, - "end": { - "type": "string", - "format": "date-time", - "description": "When the period ends (exclusive)." - } - }, - "required": [ - "start", - "end" - ], - "description": "What period it covers." - }, - "subtotal": { - "type": "integer", - "description": "An amount in cents (USD)." - }, - "discount": { - "type": "integer", - "description": "An amount in cents (USD)." - }, - "credit": { - "type": "integer", - "description": "An amount in cents (USD)." - }, - "tax": { - "type": "integer", - "description": "An amount in cents (USD)." - }, - "total": { - "type": "integer", - "description": "An amount in cents (USD)." - }, - "amount_paid": { - "type": "integer", - "description": "An amount in cents (USD)." + "description": "Your name for the list, unique in the environment." }, - "amount_due": { + "contact_count": { "type": "integer", - "description": "An amount in cents (USD)." + "description": "Members, whatever their status." }, - "collection_method": { + "created_at": { "type": "string", - "enum": [ - "charge_automatically", - "send_invoice" - ], - "description": "`charge_automatically`: charged to the payment method. `send_invoice`: paid by `due_at`." + "format": "date-time" }, - "due_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When payment is due. `null`: not set." - }, - "finalized_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When it was finalized. `null`: not yet." - }, - "paid_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When it was paid. `null`: not paid." - }, - "voided_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When it was voided. `null`: it wasn't." - }, - "hosted_url": { - "type": [ - "string", - "null" - ], - "description": "A page to view (and pay) the invoice." - }, - "pdf_url": { - "type": [ - "string", - "null" - ], - "description": "The invoice as a PDF. `null`: not available." - }, - "lines": { - "type": "array", - "items": { - "$ref": "#/components/schemas/InvoiceLine" - } + "updated_at": { + "type": "string", + "format": "date-time" } }, "required": [ "object", "id", - "number", - "kind", - "status", - "currency", - "period", - "subtotal", - "discount", - "credit", - "tax", - "total", - "amount_paid", - "amount_due", - "collection_method", - "due_at", - "finalized_at", - "paid_at", - "voided_at", - "hosted_url", - "pdf_url", - "lines" + "name", + "contact_count", + "created_at", + "updated_at" ], "examples": [ { - "object": "invoice", - "id": "inv_01k7c8m4q2f3e9v6t5r1n0p8yz", - "number": "COSS-000042", - "kind": "period", - "status": "paid", - "currency": "usd", - "period": { - "start": "2026-10-01T00:00:00.000Z", - "end": "2026-11-01T00:00:00.000Z" - }, - "subtotal": 7300, - "discount": 0, - "credit": 2500, - "tax": 0, - "total": 4800, - "amount_paid": 4800, - "amount_due": 0, - "collection_method": "charge_automatically", - "due_at": "2026-11-01T00:20:00.000Z", - "finalized_at": "2026-11-01T00:20:00.000Z", - "paid_at": "2026-11-01T00:21:00.000Z", - "voided_at": null, - "hosted_url": "https://invoice.stripe.com/i/acct_123/test_YWNjdF8x", - "pdf_url": "https://pay.stripe.com/invoice/acct_123/test_YWNjdF8x/pdf", - "lines": [ - { - "object": "invoice_line", - "id": "invl_01k7c8m4q2f3e9v6t5r1n0p8yz", - "kind": "usage", - "description": "Email: 120,000 recipients", - "meter": "email.recipients", - "dimension": null, - "quantity": 120000, - "included_quantity": 0, - "unit_price": "0.0004", - "amount": 4800, - "period": { - "start": "2026-10-01T00:00:00.000Z", - "end": "2026-11-01T00:00:00.000Z" - } - } - ] + "object": "audience", + "id": "aud_01k7c8m4q2f3e9v6t5r1n0p8yz", + "name": "Beta testers", + "contact_count": 42, + "created_at": "2026-10-10T12:00:00.000Z", + "updated_at": "2026-10-10T12:00:00.000Z" } ] }, - "InvoiceLine": { + "CreateAudienceRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Your name for the list, unique in the environment." + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "examples": [ + { + "name": "Beta testers" + } + ] + }, + "UpdateAudienceRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Your name for the list, unique in the environment." + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "examples": [ + { + "name": "Early access" + } + ] + }, + "DeletedAudience": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "invoice_line" + "audience" ] }, "id": { "type": "string" }, - "kind": { - "type": "string", + "deleted": { + "type": "boolean", "enum": [ - "plan_fee", - "proration", - "usage", - "credit", - "discount", - "commitment_fee", - "commitment_shortfall", - "add_on", - "adjustment", - "write_off" - ], - "description": "What it's for: `plan_fee`, `proration`, `usage`, `credit`, `discount`, `commitment_fee`, `commitment_shortfall`, `add_on`, `adjustment` or `write_off`." - }, - "description": { + true + ] + } + }, + "required": [ + "object", + "id", + "deleted" + ], + "examples": [ + { + "object": "audience", + "id": "aud_01k7c8m4q2f3e9v6t5r1n0p8yz", + "deleted": true + } + ] + }, + "AudienceContactsAdded": { + "type": "object", + "properties": { + "object": { "type": "string", - "description": "The line as it reads on the invoice." - }, - "meter": { - "type": [ - "string", - "null" - ], "enum": [ - "email.recipients", - "email.dedicated_ip", - "email.attachment_data", - "email.received", - "email.received_data", - "email.inboxes", - "sms.segments", - "sms.received_segments", - "sms.numbers", - "sms.registrations", - "sign.completed_requests", - "risk.phone_checks", - "risk.assessments", - "risk.content_checks", - "verify.verifications", - "verify.sms", - "forms.submissions", - "forms.file_uploads", - "scheduling.bookings", - "scheduling.hosts", - "hooks.messages", - null - ], - "description": "The meter of a `usage` line. `null` for other lines." - }, - "dimension": { - "type": [ - "string", - "null" - ], - "description": "What the meter is split by, such as `country=US`. `null`: not split." - }, - "quantity": { - "type": [ - "integer", - "null" - ], - "description": "Units on this line. `null` when it isn't priced by the unit." - }, - "included_quantity": { - "type": [ - "integer", - "null" - ], - "description": "Units covered by an allowance or committed volume (not charged)." + "audience_contacts" + ] }, - "unit_price": { - "type": [ - "string", - "null" - ], - "description": "Dollars per unit, e.g. `0.0004`; `null` when the price varies by volume." + "added": { + "type": "integer", + "description": "Contacts newly added." }, - "amount": { + "existing": { "type": "integer", - "description": "An amount in cents (USD)." + "description": "Contacts that were already members." }, - "period": { - "type": "object", - "properties": { - "start": { - "type": "string", - "format": "date-time", - "description": "When the period starts (inclusive)." - }, - "end": { - "type": "string", - "format": "date-time", - "description": "When the period ends (exclusive)." - } + "missing": { + "type": "array", + "items": { + "type": "string" }, - "required": [ - "start", - "end" - ], - "description": "What period it covers." + "description": "Ids or addresses of no contact." } }, "required": [ "object", - "id", - "kind", - "description", - "meter", - "dimension", - "quantity", - "included_quantity", - "unit_price", - "amount", - "period" + "added", + "existing", + "missing" ], "examples": [ { - "object": "invoice_line", - "id": "invl_01k7c8m4q2f3e9v6t5r1n0p8yz", - "kind": "usage", - "description": "Email: 120,000 recipients", - "meter": "email.recipients", - "dimension": null, - "quantity": 120000, - "included_quantity": 0, - "unit_price": "0.0004", - "amount": 4800, - "period": { - "start": "2026-10-01T00:00:00.000Z", - "end": "2026-11-01T00:00:00.000Z" - } + "object": "audience_contacts", + "added": 1, + "existing": 0, + "missing": [] } ] }, - "CreditList": { + "AddAudienceContactsRequest": { + "type": "object", + "properties": { + "contacts": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 1000, + "description": "Contact ids or addresses of existing contacts." + } + }, + "required": [ + "contacts" + ], + "additionalProperties": false, + "examples": [ + { + "contacts": [ + "contact_01k7c8m4q2f3e9v6t5r1n0p8yz", + "grace@example.com" + ] + } + ] + }, + "SegmentList": { "type": "object", "properties": { "object": { @@ -28978,7 +28655,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Credit" + "$ref": "#/components/schemas/Segment" } }, "has_more": { @@ -28995,98 +28672,62 @@ "object": "list", "data": [ { - "object": "credit", - "id": "bcg_01k7c8m4q2f3e9v6t5r1n0p8yz", - "kind": "units", - "amount": 50000, - "remaining": 12000, - "meter": "email.recipients", - "meters": null, - "applies_to": "usage", - "source": "support", - "description": "Migration credit", - "effective_at": "2026-10-01T00:00:00.000Z", - "expires_at": "2027-01-01T00:00:00.000Z", - "voided_at": null, - "created_at": "2026-10-01T09:30:00.000Z" + "object": "segment", + "id": "seg_01k7c8m4q2f3e9v6t5r1n0p8yz", + "name": "Pro customers", + "filter": { + "version": 1, + "match": "all", + "conditions": [ + { + "field": "property", + "key": "plan", + "op": "eq", + "value": "pro" + }, + { + "match": "any", + "conditions": [ + { + "field": "email", + "op": "ends_with", + "value": "@acme.com" + }, + { + "field": "audience", + "op": "in", + "audience_id": "aud_01k7c8m4q2f3e9v6t5r1n0p8yz" + } + ] + } + ] + }, + "created_at": "2026-10-10T12:00:00.000Z", + "updated_at": "2026-10-10T12:00:00.000Z" } ], "has_more": false } ] }, - "LogDrainWithSecret": { + "Segment": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "log_drain" + "segment" ] }, "id": { - "type": "string", - "examples": [ - "drain_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "type": "string" }, - "url": { + "name": { "type": "string", - "description": "Where batches are posted.", - "examples": [ - "https://logs.example.com/coss" - ] - }, - "description": { - "type": [ - "string", - "null" - ], - "description": "Your note about it. `null`: none." - }, - "sources": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LogDrainSource" - }, - "description": "What the drain forwards: `api` (requests made with the environment's API keys), `email` (email events), `webhook` (webhook delivery attempts) and `audit` (changes to the environment, from the organization's audit log). An empty array means every source, including ones added later." - }, - "format": { - "$ref": "#/components/schemas/LogDrainFormat" - }, - "auth_header_name": { - "type": [ - "string", - "null" - ], - "description": "The header sent with every batch to authenticate it (e.g. `Authorization`, `DD-API-KEY`), or `null`. Its value is never returned.", - "examples": [ - "DD-API-KEY" - ] - }, - "enabled": { - "type": "boolean", - "description": "A disabled drain receives nothing, and lines logged while it is disabled are not kept for it." - }, - "disabled_reason": { - "type": [ - "string", - "null" - ], - "enum": [ - "manual", - "failing", - null - ], - "description": "`manual`: someone disabled it. `failing`: a batch failed every attempt, so coss.com disabled it. `null` while enabled." + "description": "Your name for the segment, unique in the environment." }, - "disabled_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When it was disabled. `null` while enabled." + "filter": { + "$ref": "#/components/schemas/SegmentFilter" }, "created_at": { "type": "string", @@ -29095,809 +28736,628 @@ "updated_at": { "type": "string", "format": "date-time" - }, - "secret": { - "type": "string", - "description": "The drain's `whsec_…` signing secret, used to verify the batches coss.com sends. Returned only when the drain is created — store it now; it cannot be read back through the API.", - "examples": [ - "whsec_6Jk…" - ] } }, "required": [ "object", "id", - "url", - "description", - "sources", - "format", - "auth_header_name", - "enabled", - "disabled_reason", - "disabled_at", + "name", + "filter", "created_at", - "updated_at", - "secret" + "updated_at" ], "examples": [ { - "object": "log_drain", - "id": "drain_01k7c8m4q2f3e9v6t5r1n0p8yz", - "url": "https://logs.example.com/coss", - "description": "Production logs", - "sources": [ - "api", - "email" - ], - "format": "ndjson", - "auth_header_name": null, - "enabled": true, - "disabled_reason": null, - "disabled_at": null, - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z", - "secret": "whsec_6Jk…" - } - ] - }, - "LogDrainSource": { - "type": "string", - "enum": [ - "api", - "email", - "webhook", - "risk", - "smtp", - "sms", - "sign", - "verify", - "forms", - "scheduling", - "audit" - ], - "description": "What the drain forwards: `api` (requests made with the environment's API keys), `email` (email events), `webhook` (webhook delivery attempts) and `audit` (changes to the environment, from the organization's audit log). An empty array means every source, including ones added later." - }, - "LogDrainFormat": { - "type": "string", - "enum": [ - "ndjson", - "loki", - "datadog", - "otlp" - ], - "description": "How batches are encoded. `ndjson`: one JSON object per line (`application/x-ndjson`), for your own endpoint. `loki`: Loki's push API (`/loki/api/v1/push`, Grafana Cloud Logs). `datadog`: Datadog's log intake (`/api/v2/logs`). `otlp`: OTLP/HTTP logs in JSON (`/v1/logs`, Grafana Cloud, Honeycomb, an OpenTelemetry Collector…)." - }, - "CreateLogDrainRequest": { - "type": "object", - "properties": { - "url": { - "type": "string", - "minLength": 1, - "maxLength": 2048, - "description": "An `https://` URL coss.com posts batches of log lines to.", - "examples": [ - "https://logs.example.com/coss" - ] - }, - "description": { - "type": [ - "string", - "null" - ], - "maxLength": 256, - "description": "A note for your own reference. `null` clears it." - }, - "sources": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LogDrainSource" + "object": "segment", + "id": "seg_01k7c8m4q2f3e9v6t5r1n0p8yz", + "name": "Pro customers", + "filter": { + "version": 1, + "match": "all", + "conditions": [ + { + "field": "property", + "key": "plan", + "op": "eq", + "value": "pro" + }, + { + "match": "any", + "conditions": [ + { + "field": "email", + "op": "ends_with", + "value": "@acme.com" + }, + { + "field": "audience", + "op": "in", + "audience_id": "aud_01k7c8m4q2f3e9v6t5r1n0p8yz" + } + ] + } + ] }, - "maxItems": 11, - "description": "What the drain forwards: `api` (requests made with the environment's API keys), `email` (email events), `webhook` (webhook delivery attempts) and `audit` (changes to the environment, from the organization's audit log). An empty array means every source, including ones added later. Defaults to every source." - }, - "format": { - "type": "string", - "enum": [ - "ndjson", - "loki", - "datadog", - "otlp" - ], - "description": "How batches are encoded. `ndjson`: one JSON object per line (`application/x-ndjson`), for your own endpoint. `loki`: Loki's push API (`/loki/api/v1/push`, Grafana Cloud Logs). `datadog`: Datadog's log intake (`/api/v2/logs`). `otlp`: OTLP/HTTP logs in JSON (`/v1/logs`, Grafana Cloud, Honeycomb, an OpenTelemetry Collector…). Defaults to `ndjson`." - }, - "auth_header": { - "$ref": "#/components/schemas/LogDrainAuthHeader" - } - }, - "required": [ - "url" - ], - "additionalProperties": false, - "examples": [ - { - "url": "https://logs.example.com/coss", - "description": "Production logs", - "sources": [ - "api", - "email" - ] - }, - { - "url": "https://http-intake.logs.datadoghq.com/api/v2/logs", - "description": "Datadog", - "format": "datadog", - "auth_header": { - "name": "DD-API-KEY", - "value": "" - } + "created_at": "2026-10-10T12:00:00.000Z", + "updated_at": "2026-10-10T12:00:00.000Z" } ] }, - "LogDrainAuthHeader": { - "type": [ - "object", - "null" - ], - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "description": "The header's name.", - "examples": [ - "Authorization" - ] - }, - "value": { - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^[\\t\\x20-\\x7e]+$", - "description": "The header's value. Encrypted, and never returned.", - "examples": [ - "Bearer " - ] - } - }, - "required": [ - "name", - "value" - ], - "additionalProperties": false, - "description": "A header sent with every batch, for intake APIs that take a key: `Authorization` (`Basic …`, `Bearer …`), `DD-API-KEY`… Its value is stored encrypted and never returned. `null` removes it." - }, - "LogDrainList": { + "SegmentFilter": { "type": "object", "properties": { - "object": { - "type": "string", + "version": { + "type": "number", "enum": [ - "list" - ] - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LogDrain" - } - }, - "has_more": { - "type": "boolean" - } - }, - "required": [ - "object", - "data", - "has_more" - ], - "examples": [ - { - "object": "list", - "data": [ - { - "object": "log_drain", - "id": "drain_01k7c8m4q2f3e9v6t5r1n0p8yz", - "url": "https://logs.example.com/coss", - "description": "Production logs", - "sources": [ - "api", - "email" - ], - "format": "ndjson", - "auth_header_name": null, - "enabled": true, - "disabled_reason": null, - "disabled_at": null, - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z" - } + 1 ], - "has_more": false - } - ] - }, - "LogDrain": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "log_drain" - ] - }, - "id": { - "type": "string", - "examples": [ - "drain_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "description": "The filter's format. Always 1." }, - "url": { + "match": { "type": "string", - "description": "Where batches are posted.", - "examples": [ - "https://logs.example.com/coss" - ] - }, - "description": { - "type": [ - "string", - "null" - ], - "description": "Your note about it. `null`: none." - }, - "sources": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LogDrainSource" - }, - "description": "What the drain forwards: `api` (requests made with the environment's API keys), `email` (email events), `webhook` (webhook delivery attempts) and `audit` (changes to the environment, from the organization's audit log). An empty array means every source, including ones added later." - }, - "format": { - "$ref": "#/components/schemas/LogDrainFormat" - }, - "auth_header_name": { - "type": [ - "string", - "null" - ], - "description": "The header sent with every batch to authenticate it (e.g. `Authorization`, `DD-API-KEY`), or `null`. Its value is never returned.", - "examples": [ - "DD-API-KEY" - ] - }, - "enabled": { - "type": "boolean", - "description": "A disabled drain receives nothing, and lines logged while it is disabled are not kept for it." - }, - "disabled_reason": { - "type": [ - "string", - "null" - ], "enum": [ - "manual", - "failing", - null - ], - "description": "`manual`: someone disabled it. `failing`: a batch failed every attempt, so coss.com disabled it. `null` while enabled." - }, - "disabled_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When it was disabled. `null` while enabled." - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" - } - }, - "required": [ - "object", - "id", - "url", - "description", - "sources", - "format", - "auth_header_name", - "enabled", - "disabled_reason", - "disabled_at", - "created_at", - "updated_at" - ], - "examples": [ - { - "object": "log_drain", - "id": "drain_01k7c8m4q2f3e9v6t5r1n0p8yz", - "url": "https://logs.example.com/coss", - "description": "Production logs", - "sources": [ - "api", - "email" - ], - "format": "ndjson", - "auth_header_name": null, - "enabled": true, - "disabled_reason": null, - "disabled_at": null, - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z" - } - ] - }, - "UpdateLogDrainRequest": { - "type": "object", - "properties": { - "url": { - "type": "string", - "minLength": 1, - "maxLength": 2048, - "description": "An `https://` URL coss.com posts batches of log lines to.", - "examples": [ - "https://logs.example.com/coss" - ] - }, - "description": { - "type": [ - "string", - "null" + "all", + "any" ], - "maxLength": 256, - "description": "A note for your own reference. `null` clears it." - }, - "sources": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LogDrainSource" - }, - "maxItems": 11, - "description": "What the drain forwards: `api` (requests made with the environment's API keys), `email` (email events), `webhook` (webhook delivery attempts) and `audit` (changes to the environment, from the organization's audit log). An empty array means every source, including ones added later." - }, - "format": { - "$ref": "#/components/schemas/LogDrainFormat" - }, - "auth_header": { - "$ref": "#/components/schemas/LogDrainAuthHeader" - }, - "enabled": { - "type": "boolean", - "description": "`false` disables it, and `true` enables it again." - } - }, - "additionalProperties": false, - "examples": [ - { - "enabled": false - } - ] - }, - "DeletedLogDrain": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "log_drain" - ] - }, - "id": { - "type": "string" - }, - "deleted": { - "type": "boolean", - "enum": [ - true - ] - } - }, - "required": [ - "object", - "id", - "deleted" - ], - "examples": [ - { - "object": "log_drain", - "id": "drain_01k7c8m4q2f3e9v6t5r1n0p8yz", - "deleted": true - } - ] - }, - "LogDrainBatchList": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "list" - ] + "description": "`all`: every condition must hold; `any`: one is enough." }, - "data": { + "conditions": { "type": "array", "items": { - "$ref": "#/components/schemas/LogDrainBatch" - } - }, - "has_more": { - "type": "boolean" - } - }, - "required": [ - "object", - "data", - "has_more" - ], - "examples": [ - { - "object": "list", - "data": [ - { - "object": "log_drain_batch", - "id": "drainbat_01k7c8n2b5d7f9h1k3m5p7r9tv", - "drain_id": "drain_01k7c8m4q2f3e9v6t5r1n0p8yz", - "status": "succeeded", - "line_count": 42, - "byte_size": 31870, - "attempt_count": 1, - "next_attempt_at": null, - "last_status_code": 200, - "last_error": null, - "attempts": [ - { - "object": "log_drain_attempt", - "id": "drainatt_01k7c8n2c9e1g3j5l7n9q1s3uw", - "attempted_at": "2026-01-15T09:30:01.000Z", - "status_code": 200, - "duration_ms": 182, - "error": null, - "response_excerpt": "ok" - } - ], - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:01.000Z" - } - ], - "has_more": false - } - ] - }, - "LogDrainBatch": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "log_drain_batch" - ] - }, - "id": { - "type": "string", - "description": "The batch's id: the `webhook-id` header, the same on every attempt.", - "examples": [ - "drainbat_01k7c8n2b5d7f9h1k3m5p7r9tv" - ] - }, - "drain_id": { - "type": "string", - "description": "The id of the drain it's posted to." - }, - "status": { - "$ref": "#/components/schemas/LogDrainBatchStatus" - }, - "line_count": { - "type": "integer", - "description": "Lines in the batch." - }, - "byte_size": { - "type": "integer", - "description": "Size of the request body in bytes." - }, - "attempt_count": { - "type": "integer", - "description": "How many attempts were made so far." - }, - "next_attempt_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When the next attempt is due; `null` once the batch is finished." - }, - "last_status_code": { - "type": [ - "integer", - "null" - ], - "description": "The drain's HTTP status on the latest attempt. `null`: no response arrived, or no attempt yet." - }, - "last_error": { - "type": [ - "string", - "null" - ], - "description": "Why the latest attempt failed. `null`: it didn't, or no attempt yet." - }, - "attempts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LogDrainAttempt" + "anyOf": [ + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": [ + "property" + ] + }, + "key": { + "type": "string", + "description": "The custom property to test." + }, + "op": { + "type": "string", + "enum": [ + "eq", + "neq", + "in", + "not_in", + "gt", + "gte", + "lt", + "lte", + "contains", + "starts_with", + "exists", + "not_exists" + ], + "description": "How to compare: `eq`, `neq`, `in`, `not_in`, `gt`, `gte`, `lt`, `lte`, `contains`, `starts_with`, or `exists` and `not_exists`, which take no value." + }, + "value": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "minItems": 1, + "maxItems": 100 + } + ], + "description": "A list for `in`/`not_in`; none for `exists`/`not_exists`." + } + }, + "required": [ + "field", + "key", + "op" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": [ + "email" + ] + }, + "op": { + "type": "string", + "enum": [ + "eq", + "ends_with", + "contains" + ], + "description": "How to compare the contact's address." + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 254, + "description": "What to compare it with." + } + }, + "required": [ + "field", + "op", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": [ + "audience" + ] + }, + "op": { + "type": "string", + "enum": [ + "in", + "not_in" + ], + "description": "Whether the contact is in the audience." + }, + "audience_id": { + "type": "string", + "description": "The audience's id." + } + }, + "required": [ + "field", + "op", + "audience_id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": [ + "topic" + ] + }, + "op": { + "type": "string", + "enum": [ + "subscribed", + "not_subscribed" + ], + "description": "Whether emails about the topic reach the contact." + }, + "topic": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The topic's key." + } + }, + "required": [ + "field", + "op", + "topic" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": [ + "created_at" + ] + }, + "op": { + "type": "string", + "enum": [ + "before", + "after", + "within_days" + ], + "description": "`before` or `after` a time, or created `within_days` the last days." + }, + "value": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "integer", + "minimum": 1, + "maximum": 3650 + } + ], + "description": "A timestamp for `before`/`after`, a number of days for `within_days`." + } + }, + "required": [ + "field", + "op", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "match": { + "type": "string", + "enum": [ + "all", + "any" + ], + "description": "`all`: every condition must hold; `any`: one is enough." + }, + "conditions": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": [ + "property" + ] + }, + "key": { + "type": "string", + "description": "The custom property to test." + }, + "op": { + "type": "string", + "enum": [ + "eq", + "neq", + "in", + "not_in", + "gt", + "gte", + "lt", + "lte", + "contains", + "starts_with", + "exists", + "not_exists" + ], + "description": "How to compare: `eq`, `neq`, `in`, `not_in`, `gt`, `gte`, `lt`, `lte`, `contains`, `starts_with`, or `exists` and `not_exists`, which take no value." + }, + "value": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "minItems": 1, + "maxItems": 100 + } + ], + "description": "A list for `in`/`not_in`; none for `exists`/`not_exists`." + } + }, + "required": [ + "field", + "key", + "op" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": [ + "email" + ] + }, + "op": { + "type": "string", + "enum": [ + "eq", + "ends_with", + "contains" + ], + "description": "How to compare the contact's address." + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 254, + "description": "What to compare it with." + } + }, + "required": [ + "field", + "op", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": [ + "audience" + ] + }, + "op": { + "type": "string", + "enum": [ + "in", + "not_in" + ], + "description": "Whether the contact is in the audience." + }, + "audience_id": { + "type": "string", + "description": "The audience's id." + } + }, + "required": [ + "field", + "op", + "audience_id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": [ + "topic" + ] + }, + "op": { + "type": "string", + "enum": [ + "subscribed", + "not_subscribed" + ], + "description": "Whether emails about the topic reach the contact." + }, + "topic": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The topic's key." + } + }, + "required": [ + "field", + "op", + "topic" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": [ + "created_at" + ] + }, + "op": { + "type": "string", + "enum": [ + "before", + "after", + "within_days" + ], + "description": "`before` or `after` a time, or created `within_days` the last days." + }, + "value": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "integer", + "minimum": 1, + "maximum": 3650 + } + ], + "description": "A timestamp for `before`/`after`, a number of days for `within_days`." + } + }, + "required": [ + "field", + "op", + "value" + ], + "additionalProperties": false + } + ] + }, + "minItems": 1, + "description": "The group's conditions." + } + }, + "required": [ + "match", + "conditions" + ], + "additionalProperties": false + } + ] }, - "description": "Every HTTP attempt of this batch, oldest first." - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" + "minItems": 1, + "description": "Conditions, or `{ match, conditions }` groups of them. At most 20 in all." } }, "required": [ - "object", - "id", - "drain_id", - "status", - "line_count", - "byte_size", - "attempt_count", - "next_attempt_at", - "last_status_code", - "last_error", - "attempts", - "created_at", - "updated_at" - ], - "examples": [ - { - "object": "log_drain_batch", - "id": "drainbat_01k7c8n2b5d7f9h1k3m5p7r9tv", - "drain_id": "drain_01k7c8m4q2f3e9v6t5r1n0p8yz", - "status": "succeeded", - "line_count": 42, - "byte_size": 31870, - "attempt_count": 1, - "next_attempt_at": null, - "last_status_code": 200, - "last_error": null, - "attempts": [ - { - "object": "log_drain_attempt", - "id": "drainatt_01k7c8n2c9e1g3j5l7n9q1s3uw", - "attempted_at": "2026-01-15T09:30:01.000Z", - "status_code": 200, - "duration_ms": 182, - "error": null, - "response_excerpt": "ok" - } - ], - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:01.000Z" - } - ] - }, - "LogDrainBatchStatus": { - "type": "string", - "enum": [ - "pending", - "succeeded", - "failed" + "version", + "match", + "conditions" ], - "description": "`pending`: waiting for its next attempt. `succeeded`: the drain answered with a 2xx status. `failed`: every attempt failed, or the drain was disabled." + "additionalProperties": false, + "description": "Which contacts belong to the segment. Conditions on `property` (custom properties), `email`, `audience` membership, `topic` subscription and `created_at`; one level of nested `{ match, conditions }` groups." }, - "LogDrainAttempt": { + "CreateSegmentRequest": { "type": "object", "properties": { - "object": { + "name": { "type": "string", - "enum": [ - "log_drain_attempt" - ] - }, - "id": { - "type": "string" - }, - "attempted_at": { - "type": "string", - "format": "date-time", - "description": "When the attempt ran." - }, - "status_code": { - "type": [ - "integer", - "null" - ], - "description": "The drain's HTTP status; `null` when no response arrived." - }, - "duration_ms": { - "type": "integer", - "description": "How long it took, in milliseconds." - }, - "error": { - "type": [ - "string", - "null" - ], - "description": "Why the attempt failed (e.g. `HTTP 500`, a timeout); `null` on success." + "minLength": 1, + "maxLength": 200, + "description": "Your name for the segment, unique in the environment." }, - "response_excerpt": { - "type": [ - "string", - "null" - ], - "description": "At most 1 KB of the response body, as text." + "filter": { + "$ref": "#/components/schemas/SegmentFilter" } }, "required": [ - "object", - "id", - "attempted_at", - "status_code", - "duration_ms", - "error", - "response_excerpt" + "name", + "filter" ], + "additionalProperties": false, "examples": [ { - "object": "log_drain_attempt", - "id": "drainatt_01k7c8n2c9e1g3j5l7n9q1s3uw", - "attempted_at": "2026-01-15T09:30:01.000Z", - "status_code": 200, - "duration_ms": 182, - "error": null, - "response_excerpt": "ok" + "name": "Pro customers", + "filter": { + "version": 1, + "match": "all", + "conditions": [ + { + "field": "property", + "key": "plan", + "op": "eq", + "value": "pro" + }, + { + "match": "any", + "conditions": [ + { + "field": "email", + "op": "ends_with", + "value": "@acme.com" + }, + { + "field": "audience", + "op": "in", + "audience_id": "aud_01k7c8m4q2f3e9v6t5r1n0p8yz" + } + ] + } + ] + } } ] }, - "HookEventType": { + "UpdateSegmentRequest": { "type": "object", "properties": { - "object": { - "type": "string", - "enum": [ - "hook_event_type" - ] - }, "name": { "type": "string", - "description": "The type's name, which messages send as `event_type`.", - "examples": [ - "invoice.paid" - ] - }, - "description": { - "type": [ - "string", - "null" - ], - "description": "What this event means, shown to your users. `null`: none." - }, - "schema": { - "type": [ - "object", - "null" - ], - "additionalProperties": {}, - "description": "A JSON Schema of the payload, shown to your users (not enforced). `null`: none." - }, - "examples": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": {} - }, - "description": "Sample payloads, shown to your users. Test sends use the first." - }, - "archived": { - "type": "boolean", - "description": "Archived types are hidden from the portal and can't be subscribed to; messages of the type are still delivered to endpoints that subscribed before." - }, - "created_at": { - "type": "string", - "format": "date-time" + "minLength": 1, + "maxLength": 200, + "description": "Your name for the segment, unique in the environment." }, - "updated_at": { - "type": "string", - "format": "date-time" + "filter": { + "$ref": "#/components/schemas/SegmentFilter" } }, - "required": [ - "object", - "name", - "description", - "schema", - "examples", - "archived", - "created_at", - "updated_at" - ], + "additionalProperties": false, "examples": [ { - "object": "hook_event_type", - "name": "invoice.paid", - "description": "An invoice was paid in full.", - "schema": { - "type": "object", - "properties": { - "invoice_id": { - "type": "string" - }, - "amount": { - "type": "integer" - } - } - }, - "examples": [ - { - "invoice_id": "in_1042", - "amount": 4200, - "currency": "usd" - } - ], - "archived": false, - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z" + "name": "Pro and Scale customers" } ] }, - "CreateHookEventTypeRequest": { + "DeletedSegment": { "type": "object", "properties": { - "name": { + "object": { "type": "string", - "pattern": "^[A-Za-z0-9_.-]{1,64}$", - "description": "The type's name, which messages send as `event_type`. Letters, digits and `. _ -`.", - "examples": [ - "invoice.paid" + "enum": [ + "segment" ] }, - "description": { - "type": "string", - "maxLength": 256, - "description": "What this event means, shown to your users." - }, - "schema": { - "type": "object", - "additionalProperties": {}, - "description": "A JSON Schema of the payload, shown to your users (not enforced)." + "id": { + "type": "string" }, - "examples": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": {} - }, - "maxItems": 5, - "description": "Up to 5 example payloads; the first is what a test send delivers." + "deleted": { + "type": "boolean", + "enum": [ + true + ] } }, "required": [ - "name" + "object", + "id", + "deleted" ], - "additionalProperties": false, "examples": [ { - "name": "invoice.paid", - "description": "An invoice was paid in full.", - "examples": [ - { - "invoice_id": "in_1042", - "amount": 4200, - "currency": "usd" - } - ] + "object": "segment", + "id": "seg_01k7c8m4q2f3e9v6t5r1n0p8yz", + "deleted": true } ] }, - "HookEventTypeList": { + "TopicList": { "type": "object", "properties": { "object": { @@ -29909,7 +29369,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/HookEventType" + "$ref": "#/components/schemas/Topic" } }, "has_more": { @@ -29926,522 +29386,493 @@ "object": "list", "data": [ { - "object": "hook_event_type", - "name": "invoice.paid", - "description": "An invoice was paid in full.", - "schema": { - "type": "object", - "properties": { - "invoice_id": { - "type": "string" - }, - "amount": { - "type": "integer" - } - } - }, - "examples": [ - { - "invoice_id": "in_1042", - "amount": 4200, - "currency": "usd" - } - ], - "archived": false, - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z" + "object": "topic", + "id": "topic_01k7c8m4q2f3e9v6t5r1n0p8yz", + "key": "newsletter", + "name": "Newsletter", + "description": "Monthly product news.", + "default_subscription": "opt_in", + "visibility": "public", + "position": 0, + "created_at": "2026-10-10T12:00:00.000Z", + "updated_at": "2026-10-10T12:00:00.000Z" } ], "has_more": false } ] }, - "UpdateHookEventTypeRequest": { + "Topic": { "type": "object", "properties": { + "object": { + "type": "string", + "enum": [ + "topic" + ] + }, + "id": { + "type": "string" + }, + "key": { + "type": "string", + "description": "The `category` of the emails about this topic. Can't change." + }, + "name": { + "type": "string", + "description": "Shown in the preference center." + }, "description": { "type": [ "string", "null" ], - "maxLength": 256, - "description": "What this event means, shown to your users. `null` removes it." + "description": "Shown in the preference center under its name. `null`: none." }, - "schema": { - "type": [ - "object", - "null" + "default_subscription": { + "type": "string", + "enum": [ + "opt_in", + "opt_out" ], - "additionalProperties": {}, - "description": "A JSON Schema of the payload, shown to your users (not enforced)." + "description": "`opt_in`: every subscribed contact gets the topic's emails unless they opt out. `opt_out`: only contacts who opted in do." }, - "examples": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": {} - }, - "maxItems": 5, - "description": "Up to 5 example payloads; the first is what a test send delivers." + "visibility": { + "type": "string", + "enum": [ + "public", + "private" + ], + "description": "`private` topics never show in the preference center." }, - "archived": { - "type": "boolean", - "description": "`false` restores an archived type." + "position": { + "type": "integer", + "description": "Order in the preference center (ascending)." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" } }, - "additionalProperties": false, + "required": [ + "object", + "id", + "key", + "name", + "description", + "default_subscription", + "visibility", + "position", + "created_at", + "updated_at" + ], "examples": [ { - "archived": false + "object": "topic", + "id": "topic_01k7c8m4q2f3e9v6t5r1n0p8yz", + "key": "newsletter", + "name": "Newsletter", + "description": "Monthly product news.", + "default_subscription": "opt_in", + "visibility": "public", + "position": 0, + "created_at": "2026-10-10T12:00:00.000Z", + "updated_at": "2026-10-10T12:00:00.000Z" } ] }, - "HookApplication": { + "CreateTopicRequest": { "type": "object", "properties": { - "object": { + "key": { "type": "string", - "enum": [ - "hook_application" - ] + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The `category` of the emails about this topic. Can't change." }, - "id": { + "name": { "type": "string", - "examples": [ - "happ_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "minLength": 1, + "maxLength": 200, + "description": "Shown in the preference center." }, - "uid": { + "description": { "type": [ "string", "null" ], - "description": "Your own id for it (e.g. your customer's id), unique in the environment. Every path that takes an application accepts its `id` or its `uid`." + "maxLength": 500, + "description": "Shown in the preference center under its name. `null`: none." }, - "name": { + "default_subscription": { "type": "string", - "description": "Shown to the application's users in the portal." - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Your own key-value pairs." + "enum": [ + "opt_in", + "opt_out" + ], + "description": "`opt_in`: every subscribed contact gets the topic's emails unless they opt out. `opt_out`: only contacts who opted in do." }, - "created_at": { + "visibility": { "type": "string", - "format": "date-time" + "enum": [ + "public", + "private" + ], + "description": "`private` topics never show in the preference center (default `public`)." }, - "updated_at": { - "type": "string", - "format": "date-time" + "position": { + "type": "integer", + "minimum": 0, + "maximum": 1000, + "description": "Order in the preference center (ascending)." } }, "required": [ - "object", - "id", - "uid", + "key", "name", - "metadata", - "created_at", - "updated_at" + "default_subscription" ], + "additionalProperties": false, "examples": [ { - "object": "hook_application", - "id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", - "uid": "customer_8812", - "name": "Acme Corp", - "metadata": { - "plan": "pro" - }, - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z" + "key": "newsletter", + "name": "Newsletter", + "default_subscription": "opt_in" } ] }, - "CreateHookApplicationRequest": { + "UpdateTopicRequest": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, - "maxLength": 256, - "description": "Shown to the application's users in the portal.", - "examples": [ - "Acme Corp" - ] + "maxLength": 200, + "description": "Shown in the preference center." }, - "uid": { + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 500, + "description": "Shown in the preference center under its name. `null`: none." + }, + "default_subscription": { "type": "string", - "pattern": "^[A-Za-z0-9_.:@|-]{1,256}$", - "description": "Your own id for it, to address it by in every path instead of its `id`.", - "examples": [ - "customer_8812" - ] + "enum": [ + "opt_in", + "opt_out" + ], + "description": "`opt_in`: every subscribed contact gets the topic's emails unless they opt out. `opt_out`: only contacts who opted in do." }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string", - "maxLength": 500 - }, - "description": "Your own key-value pairs (at most 16; keys up to 40 characters, values up to 500).", - "examples": [ - { - "plan": "pro" - } - ] + "visibility": { + "type": "string", + "enum": [ + "public", + "private" + ], + "description": "`private` topics never show in the preference center (default `public`)." + }, + "position": { + "type": "integer", + "minimum": 0, + "maximum": 1000, + "description": "Order in the preference center (ascending)." } }, - "required": [ - "name" - ], "additionalProperties": false, "examples": [ { - "name": "Acme Corp", - "uid": "customer_8812", - "metadata": { - "plan": "pro" - } + "visibility": "private" } ] }, - "HookApplicationList": { + "DeletedTopic": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "list" + "topic" ] }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/HookApplication" - } + "id": { + "type": "string" }, - "has_more": { - "type": "boolean" + "deleted": { + "type": "boolean", + "enum": [ + true + ] } }, "required": [ "object", - "data", - "has_more" + "id", + "deleted" ], "examples": [ { - "object": "list", - "data": [ - { - "object": "hook_application", - "id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", - "uid": "customer_8812", - "name": "Acme Corp", - "metadata": { - "plan": "pro" - }, - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z" - } - ], - "has_more": false + "object": "topic", + "id": "topic_01k7c8m4q2f3e9v6t5r1n0p8yz", + "deleted": true } ] }, - "UpdateHookApplicationRequest": { + "MarketingSettings": { "type": "object", "properties": { - "name": { + "object": { "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Shown to the application's users in the portal." + "enum": [ + "marketing_settings" + ] }, - "uid": { + "display_name": { "type": [ "string", "null" ], - "pattern": "^[A-Za-z0-9_.:@|-]{1,256}$", - "description": "`null` removes it." - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string", - "maxLength": 500 - }, - "description": "Replaces the metadata.", - "examples": [ - { - "plan": "pro" - } - ] - } - }, - "additionalProperties": false, - "examples": [ - { - "name": "Acme Corporation" - } - ] - }, - "DeletedHookApplication": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "hook_application" - ] + "description": "The sender's name in the preference center (default: the organization's)." }, - "id": { - "type": "string" + "mailing_address": { + "type": [ + "string", + "null" + ], + "description": "The postal address every marketing email carries (CAN-SPAM). Marketing emails can't be sent without one." }, - "deleted": { + "double_opt_in_default": { "type": "boolean", - "enum": [ - true - ] + "description": "Whether contacts upserted without `double_opt_in` must confirm first." + }, + "confirmation_from": { + "type": [ + "string", + "null" + ], + "description": "The sender of confirmation emails (a verified domain's address for live keys)." + }, + "confirmation_subject": { + "type": [ + "string", + "null" + ], + "description": "`null`: coss.com's default." + }, + "confirmation_html": { + "type": [ + "string", + "null" + ], + "description": "`null`: coss.com's default. Must contain `{{{confirmation_url}}}`." + }, + "confirmation_text": { + "type": [ + "string", + "null" + ], + "description": "`null`: coss.com's default. Must contain `{{{confirmation_url}}}`." + }, + "confirmed_redirect_url": { + "type": [ + "string", + "null" + ], + "description": "Where confirming sends the contact (https); `null`: a coss.com confirmation page." } }, "required": [ "object", - "id", - "deleted" + "display_name", + "mailing_address", + "double_opt_in_default", + "confirmation_from", + "confirmation_subject", + "confirmation_html", + "confirmation_text", + "confirmed_redirect_url" ], "examples": [ { - "object": "hook_application", - "id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", - "deleted": true + "object": "marketing_settings", + "display_name": "Acme", + "mailing_address": "Acme Inc., 548 Market St, San Francisco, CA 94104, USA", + "double_opt_in_default": true, + "confirmation_from": "Acme ", + "confirmation_subject": null, + "confirmation_html": null, + "confirmation_text": null, + "confirmed_redirect_url": "https://acme.com/welcome" } ] }, - "HookEndpointWithSecret": { + "UpdateMarketingSettingsRequest": { "type": "object", "properties": { - "object": { - "type": "string", - "enum": [ - "hook_endpoint" - ] - }, - "id": { - "type": "string", - "examples": [ - "hep_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "application_id": { - "type": "string", - "description": "The id of the application it belongs to." - }, - "url": { - "type": "string", - "description": "Where deliveries are sent." - }, - "description": { + "display_name": { "type": [ "string", "null" ], - "description": "Your note about it. `null`: none." - }, - "filter_types": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Only these event types are delivered. Empty: every type." + "minLength": 1, + "maxLength": 200, + "description": "The sender's name in the preference center. `null`: the organization's." }, - "rate_limit_per_second": { + "mailing_address": { "type": [ - "integer", + "string", "null" ], - "description": "At most this many requests per second to this endpoint. `null`: no limit of its own." + "minLength": 1, + "maxLength": 500, + "description": "The postal address every marketing email carries (CAN-SPAM). Marketing emails can't be sent without one." }, - "enabled": { + "double_opt_in_default": { "type": "boolean", - "description": "Whether new messages are delivered to it." + "description": "Whether contacts upserted without `double_opt_in` must confirm first." }, - "disabled_reason": { + "confirmation_from": { "type": [ "string", "null" ], - "enum": [ - "manual", - "failing", - null - ], - "description": "`manual`: disabled by you or its user. `failing`: its deliveries failed every attempt for five days, so coss.com disabled it. `null` while enabled." + "description": "The sender of confirmation emails (a verified domain's address for live keys)." }, - "disabled_at": { + "confirmation_subject": { "type": [ "string", "null" ], - "format": "date-time", - "description": "When it was disabled. `null`: it's enabled." + "minLength": 1, + "maxLength": 998, + "description": "`null`: coss.com's default." }, - "failing_since": { + "confirmation_html": { "type": [ "string", "null" ], - "format": "date-time", - "description": "When its current run of failed deliveries began; cleared by the next success. `null` while healthy." + "maxLength": 200000, + "description": "`null`: coss.com's default. Must contain `{{{confirmation_url}}}`." }, - "previous_secret_expires_at": { + "confirmation_text": { "type": [ "string", "null" ], - "format": "date-time", - "description": "After a secret roll, requests are also signed with the previous secret until then." - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Your own key-value pairs." - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" + "maxLength": 200000, + "description": "`null`: coss.com's default. Must contain `{{{confirmation_url}}}`." }, - "secret": { - "type": "string", - "description": "The endpoint's `whsec_…` signing secret (Standard Webhooks).", - "examples": [ - "whsec_6Jk…" - ] + "confirmed_redirect_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "format": "uri", + "description": "Where a confirmed contact is sent, an https URL. `null`: coss.com's own page." } }, - "required": [ - "object", - "id", - "application_id", - "url", - "description", - "filter_types", - "rate_limit_per_second", - "enabled", - "disabled_reason", - "disabled_at", - "failing_since", - "previous_secret_expires_at", - "metadata", - "created_at", - "updated_at", - "secret" - ], + "additionalProperties": false, "examples": [ { - "object": "hook_endpoint", - "id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", - "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", - "url": "https://example.com/webhooks", - "description": "Production", - "filter_types": [ - "invoice.paid" - ], - "rate_limit_per_second": null, - "enabled": true, - "disabled_reason": null, - "disabled_at": null, - "failing_since": null, - "previous_secret_expires_at": null, - "metadata": {}, - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z", - "secret": "whsec_6Jk…" + "mailing_address": "Acme Inc., 548 Market St, San Francisco, CA 94104, USA" } ] }, - "CreateHookEndpointRequest": { + "Reputation": { "type": "object", "properties": { - "url": { + "object": { "type": "string", - "maxLength": 2048, - "examples": [ - "https://example.com/webhooks" + "enum": [ + "reputation" ] }, - "description": { + "status": { "type": "string", - "maxLength": 256, - "description": "Your note about it." + "enum": [ + "good", + "warning", + "suspended" + ], + "description": "`good`, `warning` (a rate is approaching the suspension threshold) or `suspended` (live sending is refused until coss.com support reinstates the organization)." }, - "filter_types": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[A-Za-z0-9_.-]{1,64}$" - }, - "maxItems": 250, - "description": "Event types delivered to this endpoint (existing, unarchived ones). An empty array means every type, including ones added later.", - "examples": [ - [ - "invoice.paid" - ] - ] + "reason": { + "type": [ + "string", + "null" + ], + "description": "The trigger behind a `warning` or `suspended` status.", + "example": "complaint_rate" }, - "secret": { - "type": "string", - "description": "A `whsec_` + base64 secret of 24-64 bytes to sign with, e.g. when migrating from another provider. Generated when omitted." + "status_changed_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When `status` last changed. `null`: it never has." }, - "rate_limit_per_second": { - "type": "integer", - "minimum": 1, - "maximum": 1000, - "description": "At most this many requests per second to this endpoint." + "evaluated_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When the checks below were computed; `null` before the first evaluation." }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string", - "maxLength": 500 - }, - "description": "Your own key-value pairs (at most 16; keys up to 40 characters, values up to 500).", - "examples": [ - { - "plan": "pro" + "window": { + "type": [ + "object", + "null" + ], + "properties": { + "start": { + "type": "string", + "format": "date-time", + "description": "When the period starts." + }, + "end": { + "type": "string", + "format": "date-time", + "description": "When the period ends." } - ] + }, + "required": [ + "start", + "end" + ], + "description": "The period the checks cover (live environments only)." }, - "enabled": { - "type": "boolean", - "description": "Default `true`. `false` creates it disabled, with `disabled_reason: manual`." + "checks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReputationCheck" + }, + "description": "The measurements behind `status`." + }, + "marketing": { + "$ref": "#/components/schemas/MarketingStanding" } }, "required": [ - "url" - ], - "additionalProperties": false, - "examples": [ - { - "url": "https://example.com/webhooks", - "filter_types": [ - "invoice.paid" - ] - } + "object", + "status", + "reason", + "status_changed_at", + "evaluated_at", + "window", + "checks", + "marketing" ] }, - "HookEndpointList": { + "LogList": { "type": "object", "properties": { "object": { @@ -30453,7 +29884,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/HookEndpoint" + "$ref": "#/components/schemas/Log" } }, "has_more": { @@ -30470,730 +29901,1002 @@ "object": "list", "data": [ { - "object": "hook_endpoint", - "id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", - "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", - "url": "https://example.com/webhooks", - "description": "Production", - "filter_types": [ - "invoice.paid" - ], - "rate_limit_per_second": null, - "enabled": true, - "disabled_reason": null, - "disabled_at": null, - "failing_since": null, - "previous_secret_expires_at": null, - "metadata": {}, - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z" + "object": "log", + "id": "log_01k7c8m4q2f3e9v6t5r1n0p8yz", + "occurred_at": "2026-01-15T09:30:00.000Z", + "source": "api", + "level": "info", + "type": "api.request", + "message": "POST /v1/emails 202", + "request_id": "req_01k7c8m4q2f3e9v6t5r1n0p8yz", + "api_key_id": "key_01k7c8m4q2f3e9v6t5r1n0p8yz", + "method": "POST", + "path": "/v1/emails", + "route": "/v1/emails", + "status_code": 202, + "duration_ms": 48, + "client_ip": "203.0.113.7", + "user_agent": "coss-node/0.1.0", + "error_code": null, + "resource_type": "email", + "resource_id": "email_01k7c8m4q2f3e9v6t5r1n0p8yz", + "recipient": null, + "attributes": { + "request": { + "body": { + "from": "hello@example.com", + "to": [ + "user@example.com" + ], + "subject": "Hi" + } + }, + "response": { + "body": { + "object": "email", + "id": "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + } + } + } } ], "has_more": false } ] }, - "HookEndpoint": { + "Log": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "hook_endpoint" + "log" ] }, "id": { "type": "string", "examples": [ - "hep_01k7c8m4q2f3e9v6t5r1n0p8yz" + "log_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, - "application_id": { + "occurred_at": { "type": "string", - "description": "The id of the application it belongs to." + "format": "date-time", + "description": "When it happened." }, - "url": { + "source": { + "$ref": "#/components/schemas/LogSource" + }, + "level": { + "$ref": "#/components/schemas/LogLevel" + }, + "type": { "type": "string", - "description": "Where deliveries are sent." + "description": "`api.request`, `webhook.attempt`, `webhook.forward` (an event `coss listen` forwarded), or an email event type such as `email.delivered`." }, - "description": { + "message": { + "type": "string", + "description": "A one-line summary." + }, + "request_id": { "type": [ "string", "null" ], - "description": "Your note about it. `null`: none." - }, - "filter_types": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Only these event types are delivered. Empty: every type." + "description": "The `X-Request-Id` of the API request this line belongs to." }, - "rate_limit_per_second": { + "api_key_id": { "type": [ - "integer", + "string", "null" ], - "description": "At most this many requests per second to this endpoint. `null`: no limit of its own." - }, - "enabled": { - "type": "boolean", - "description": "Whether new messages are delivered to it." + "description": "The API key that made the request. `null`: not an API request." }, - "disabled_reason": { + "method": { "type": [ "string", "null" ], - "enum": [ - "manual", - "failing", - null - ], - "description": "`manual`: disabled by you or its user. `failing`: its deliveries failed every attempt for five days, so coss.com disabled it. `null` while enabled." + "description": "The HTTP method. `null`: not an HTTP line." }, - "disabled_at": { + "path": { "type": [ "string", "null" ], - "format": "date-time", - "description": "When it was disabled. `null`: it's enabled." + "description": "The request's path. `null`: not an API request." }, - "failing_since": { + "route": { "type": [ "string", "null" ], - "format": "date-time", - "description": "When its current run of failed deliveries began; cleared by the next success. `null` while healthy." + "description": "The matched route, e.g. `/v1/emails/:id`." }, - "previous_secret_expires_at": { + "status_code": { "type": [ - "string", + "integer", "null" ], - "format": "date-time", - "description": "After a secret roll, requests are also signed with the previous secret until then." - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Your own key-value pairs." - }, - "created_at": { - "type": "string", - "format": "date-time" + "description": "The API response status, or the webhook endpoint's response status." }, - "updated_at": { - "type": "string", - "format": "date-time" - } - }, - "required": [ - "object", - "id", - "application_id", - "url", - "description", - "filter_types", - "rate_limit_per_second", - "enabled", - "disabled_reason", - "disabled_at", - "failing_since", - "previous_secret_expires_at", - "metadata", - "created_at", - "updated_at" - ], - "examples": [ - { - "object": "hook_endpoint", - "id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", - "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", - "url": "https://example.com/webhooks", - "description": "Production", - "filter_types": [ - "invoice.paid" + "duration_ms": { + "type": [ + "integer", + "null" ], - "rate_limit_per_second": null, - "enabled": true, - "disabled_reason": null, - "disabled_at": null, - "failing_since": null, - "previous_secret_expires_at": null, - "metadata": {}, - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z" - } - ] - }, - "UpdateHookEndpointRequest": { - "type": "object", - "properties": { - "url": { - "type": "string", - "maxLength": 2048, - "description": "Where deliveries are sent." + "description": "How long it took, in milliseconds. `null`: not timed." }, - "description": { + "client_ip": { "type": [ "string", "null" ], - "maxLength": 256, - "description": "Your note about it. `null` removes it." + "description": "The caller's address. `null`: not an API request." }, - "filter_types": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[A-Za-z0-9_.-]{1,64}$" - }, - "maxItems": 250, - "description": "Event types delivered to this endpoint (existing, unarchived ones). An empty array means every type, including ones added later.", - "examples": [ - [ - "invoice.paid" - ] - ] + "user_agent": { + "type": [ + "string", + "null" + ], + "description": "The caller's `User-Agent`. `null`: not sent." }, - "rate_limit_per_second": { + "error_code": { "type": [ - "integer", + "string", "null" ], - "minimum": 1, - "maximum": 1000, - "description": "At most this many requests per second to this endpoint." + "description": "The error's `code`. `null`: no error." }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string", - "maxLength": 500 - }, - "description": "Your own key-value pairs (at most 16; keys up to 40 characters, values up to 500).", - "examples": [ - { - "plan": "pro" - } - ] + "resource_type": { + "type": [ + "string", + "null" + ], + "description": "What the line is about: `email`, `domain`, `webhook_endpoint`…" }, - "enabled": { - "type": "boolean", - "description": "`false` disables it with `disabled_reason: manual`; `true` enables it again and clears the reason." - } - }, - "additionalProperties": false, - "examples": [ - { - "enabled": false - } - ] - }, - "DeletedHookEndpoint": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "hook_endpoint" - ] + "resource_id": { + "type": [ + "string", + "null" + ], + "description": "The id of what it's about. `null`: nothing in particular." }, - "id": { - "type": "string" + "recipient": { + "type": [ + "string", + "null" + ], + "description": "The recipient an email event is about. `null`: none." }, - "deleted": { - "type": "boolean", - "enum": [ - true - ] + "attributes": { + "type": "object", + "additionalProperties": {}, + "description": "Details: redacted request and response bodies for API requests (secrets, attachment contents and long strings are never stored), event data for emails and webhooks." } }, "required": [ "object", "id", - "deleted" + "occurred_at", + "source", + "level", + "type", + "message", + "request_id", + "api_key_id", + "method", + "path", + "route", + "status_code", + "duration_ms", + "client_ip", + "user_agent", + "error_code", + "resource_type", + "resource_id", + "recipient", + "attributes" ], "examples": [ { - "object": "hook_endpoint", - "id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", - "deleted": true + "object": "log", + "id": "log_01k7c8m4q2f3e9v6t5r1n0p8yz", + "occurred_at": "2026-01-15T09:30:00.000Z", + "source": "api", + "level": "info", + "type": "api.request", + "message": "POST /v1/emails 202", + "request_id": "req_01k7c8m4q2f3e9v6t5r1n0p8yz", + "api_key_id": "key_01k7c8m4q2f3e9v6t5r1n0p8yz", + "method": "POST", + "path": "/v1/emails", + "route": "/v1/emails", + "status_code": 202, + "duration_ms": 48, + "client_ip": "203.0.113.7", + "user_agent": "coss-node/0.1.0", + "error_code": null, + "resource_type": "email", + "resource_id": "email_01k7c8m4q2f3e9v6t5r1n0p8yz", + "recipient": null, + "attributes": { + "request": { + "body": { + "from": "hello@example.com", + "to": [ + "user@example.com" + ], + "subject": "Hi" + } + }, + "response": { + "body": { + "object": "email", + "id": "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + } + } + } } ] }, - "HookEndpointSecret": { + "LogSource": { + "type": "string", + "enum": [ + "api", + "email", + "webhook", + "risk", + "smtp", + "sms", + "sign", + "verify", + "forms", + "scheduling" + ], + "description": "`api`: a request to the API. `email`: an email event. `webhook`: a delivery attempt. `risk`: a risk assessment or its feedback. `smtp`: a message submitted to the SMTP relay. `sms`: an SMS event." + }, + "LogLevel": { + "type": "string", + "enum": [ + "info", + "warning", + "error" + ], + "description": "`error`: 5xx responses, bounces, complaints, failures. `warning`: 4xx responses, delays, suppressions, failed webhook attempts that will be retried. `info`: everything else." + }, + "AuditEventList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "hook_endpoint_secret" + "list" ] }, - "secret": { - "type": "string", - "description": "The endpoint's `whsec_…` signing secret (Standard Webhooks)." + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuditEvent" + } + }, + "has_more": { + "type": "boolean" } }, "required": [ "object", - "secret" + "data", + "has_more" ], "examples": [ { - "object": "hook_endpoint_secret", - "secret": "whsec_6Jk…" - } - ] - }, - "RollHookEndpointSecretRequest": { - "type": "object", - "properties": { - "previous_secret_expires_in": { - "type": "integer", - "minimum": 0, - "maximum": 604800, - "description": "Seconds the current secret keeps signing next to the new one. Default 86400 (a day), at most 604800 (a week); `0` stops it immediately." - } - }, - "additionalProperties": false, - "examples": [ - { - "previous_secret_expires_in": 3600 + "object": "list", + "data": [ + { + "object": "audit_event", + "id": "oaudit_01k7c8m4q2f3e9v6t5r1n0p8yz", + "created_at": "2026-01-15T09:30:00.000Z", + "actor_type": "api_key", + "actor_id": "key_01k7c8m4q2f3e9v6t5r1n0p8yz", + "actor_label": "key_01k7c8m4q2f3e9v6t5r1n0p8yz", + "action": "suppression.created", + "target_type": "suppression", + "target_id": "sup_01k7c8m4q2f3e9v6t5r1n0p8yz", + "target_label": "user@example.com", + "project_id": "proj_01k7c8m4q2f3e9v6t5r1n0p8yz", + "environment_id": "env_01k7c8m4q2f3e9v6t5r1n0p8yz", + "metadata": { + "scope": "*", + "request_id": "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + }, + "client_ip": "203.0.113.7", + "user_agent": "coss-node/0.1.0" + } + ], + "has_more": false } ] }, - "HookEndpointRecovery": { + "AuditEvent": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "hook_endpoint_recovery" + "audit_event" ] }, "id": { "type": "string", "examples": [ - "hrec_01k7c8m4q2f3e9v6t5r1n0p8yz" + "oaudit_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, - "endpoint_id": { + "created_at": { "type": "string", - "description": "The id of the endpoint being recovered." + "format": "date-time" }, - "since": { - "type": "string", - "format": "date-time", - "description": "Deliveries of messages sent from this time on are requeued." + "actor_type": { + "$ref": "#/components/schemas/AuditActorType" }, - "until": { + "actor_id": { + "type": [ + "string", + "null" + ], + "description": "The user's or API key's id; `null` for the system." + }, + "actor_label": { "type": "string", - "format": "date-time", - "description": "Up to this time: when the recovery was asked for." + "description": "Who made the change, copied when it was recorded so it outlives them: the user's email, the API key's id, or `coss.com support`." }, - "status": { + "action": { "type": "string", - "enum": [ - "pending", - "completed" + "description": "`.`, e.g. `api_key.created`, `domain.deleted`, `member.role_changed`." + }, + "target_type": { + "type": [ + "string", + "null" ], - "description": "`pending` while coss.com is still requeueing deliveries (in the background)." + "description": "What was changed: `domain`, `api_key`, `member`…" }, - "requeued": { - "type": "integer", - "description": "Deliveries requeued so far." + "target_id": { + "type": [ + "string", + "null" + ], + "description": "The id of what it acted on. `null`: nothing in particular." }, - "created_at": { - "type": "string", - "format": "date-time" + "target_label": { + "type": [ + "string", + "null" + ], + "description": "A readable name for the target when it was changed." }, - "completed_at": { + "project_id": { "type": [ "string", "null" ], - "format": "date-time", - "description": "When requeueing finished. `null`: still `pending`." + "description": "`null` for organization-wide changes (domains, members…)." + }, + "environment_id": { + "type": [ + "string", + "null" + ], + "description": "`null` for organization-wide changes (domains, members…)." + }, + "metadata": { + "type": "object", + "additionalProperties": {}, + "description": "Details of the change; API changes include their `request_id`." + }, + "client_ip": { + "type": [ + "string", + "null" + ], + "description": "The actor's address. `null`: unknown." + }, + "user_agent": { + "type": [ + "string", + "null" + ], + "description": "The actor's `User-Agent`. `null`: unknown." } }, "required": [ "object", "id", - "endpoint_id", - "since", - "until", - "status", - "requeued", "created_at", - "completed_at" + "actor_type", + "actor_id", + "actor_label", + "action", + "target_type", + "target_id", + "target_label", + "project_id", + "environment_id", + "metadata", + "client_ip", + "user_agent" ], "examples": [ { - "object": "hook_endpoint_recovery", - "id": "hrec_01k7c8m4q2f3e9v6t5r1n0p8yz", - "endpoint_id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", - "since": "2026-01-15T00:00:00.000Z", - "until": "2026-01-15T09:30:00.000Z", - "status": "pending", - "requeued": 0, + "object": "audit_event", + "id": "oaudit_01k7c8m4q2f3e9v6t5r1n0p8yz", "created_at": "2026-01-15T09:30:00.000Z", - "completed_at": null + "actor_type": "api_key", + "actor_id": "key_01k7c8m4q2f3e9v6t5r1n0p8yz", + "actor_label": "key_01k7c8m4q2f3e9v6t5r1n0p8yz", + "action": "suppression.created", + "target_type": "suppression", + "target_id": "sup_01k7c8m4q2f3e9v6t5r1n0p8yz", + "target_label": "user@example.com", + "project_id": "proj_01k7c8m4q2f3e9v6t5r1n0p8yz", + "environment_id": "env_01k7c8m4q2f3e9v6t5r1n0p8yz", + "metadata": { + "scope": "*", + "request_id": "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + }, + "client_ip": "203.0.113.7", + "user_agent": "coss-node/0.1.0" } ] }, - "RecoverHookEndpointRequest": { - "type": "object", - "properties": { - "since": { - "type": "string", - "format": "date-time", - "description": "Requeue deliveries that failed, of messages sent from this time until now.", - "examples": [ - "2026-01-15T00:00:00.000Z" - ] - } - }, - "required": [ - "since" + "AuditActorType": { + "type": "string", + "enum": [ + "user", + "api_key", + "system" ], - "additionalProperties": false, - "examples": [ - { - "since": "2026-01-15T00:00:00.000Z" - } - ] + "description": "`user`: a member, in the dashboard (`actor_id` is the user's id, `actor_label` their email). `api_key`: a request made with an API key (`actor_id` is the key's id). `system`: coss.com itself, including coss.com support." }, - "HookMessage": { + "Billing": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "hook_message" - ] - }, - "id": { - "type": "string", - "description": "Also the `webhook-id` header of every request delivering it.", - "examples": [ - "hmsg_01k7c8m4q2f3e9v6t5r1n0p8yz" + "billing" ] }, - "application_id": { + "organization_id": { "type": "string", - "description": "The id of the application it was sent to." + "description": "The organization's id." }, - "event_type": { + "plan": { "type": "string", - "description": "The event type it was sent as." - }, - "payload": { - "type": "object", - "additionalProperties": {}, - "description": "The JSON body delivered to each endpoint." + "enum": [ + "free", + "pro", + "scale", + "enterprise" + ], + "description": "The plan in effect now (`free` without a contract)." }, - "created_at": { - "type": "string", - "format": "date-time" - } - }, - "required": [ - "object", - "id", - "application_id", - "event_type", - "payload", - "created_at" - ], - "examples": [ - { - "object": "hook_message", - "id": "hmsg_01k7c8m4q2f3e9v6t5r1n0p8yz", - "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", - "event_type": "invoice.paid", - "payload": { - "invoice_id": "in_1042", - "amount": 4200, - "currency": "usd" + "contract": { + "type": [ + "object", + "null" + ], + "properties": { + "id": { + "type": "string", + "description": "The contract's id." + }, + "kind": { + "type": "string", + "enum": [ + "self_serve", + "sales", + "internal" + ], + "description": "`self_serve`: chosen in the dashboard. `sales`: agreed with coss.com. `internal`: coss.com's own." + }, + "effective_from": { + "type": "string", + "format": "date-time", + "description": "When it took effect." + }, + "effective_until": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When it ends. `null`: no end is set." + }, + "term_end": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "End of a committed term." + }, + "auto_renew": { + "type": "boolean", + "description": "Whether a committed term renews when it ends." + } }, - "created_at": "2026-01-15T09:30:00.000Z" - } - ] - }, - "SendHookTestRequest": { - "type": "object", - "properties": { - "event_type": { - "type": "string", - "pattern": "^[A-Za-z0-9_.-]{1,64}$", - "description": "The event type to send as.", - "examples": [ - "invoice.paid" - ] + "required": [ + "id", + "kind", + "effective_from", + "effective_until", + "term_end", + "auto_renew" + ], + "description": "The contract in effect; `null` on the Free plan." }, - "payload": { - "type": "object", - "additionalProperties": {}, - "description": "Defaults to the event type's first example." - } - }, - "required": [ - "event_type" - ], - "additionalProperties": false, - "examples": [ - { - "event_type": "invoice.paid" - } - ] - }, - "HookDeliveryList": { - "type": "object", - "properties": { - "object": { + "scheduled_change": { + "type": [ + "object", + "null" + ], + "properties": { + "effective_at": { + "type": "string", + "format": "date-time", + "description": "When it takes effect." + }, + "plan": { + "type": "string", + "enum": [ + "free", + "pro", + "scale", + "enterprise" + ], + "description": "The plan it changes to." + } + }, + "required": [ + "effective_at", + "plan" + ], + "description": "A plan change that takes effect later (a downgrade, a cancellation)." + }, + "status": { "type": "string", "enum": [ - "list" - ] + "active", + "past_due", + "delinquent" + ], + "description": "`past_due`: an invoice is overdue; `delinquent`: past its grace period, and live sending is held." }, - "data": { + "holds": { "type": "array", "items": { - "$ref": "#/components/schemas/HookDelivery" - } + "type": "object", + "properties": { + "reason": { + "type": "string", + "enum": [ + "delinquent", + "spend_limit", + "credit_limit", + "admin" + ], + "description": "`delinquent`: an invoice is past its grace period. `spend_limit`: the spend limit was reached. `credit_limit`: the credit limit was reached, or a usage charge failed. `admin`: placed by coss.com." + }, + "since": { + "type": "string", + "format": "date-time", + "description": "When the hold began." + } + }, + "required": [ + "reason", + "since" + ] + }, + "description": "Billing holds on live sending (sandbox keys keep working)." }, - "has_more": { - "type": "boolean" - } - }, - "required": [ - "object", - "data", - "has_more" - ], - "examples": [ - { - "object": "list", - "data": [ - { - "object": "hook_delivery", - "id": "hdel_01k7c8m4q2f3e9v6t5r1n0p8yz", - "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", - "message_id": "hmsg_01k7c8m4q2f3e9v6t5r1n0p8yz", - "endpoint_id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", - "status": "succeeded", - "attempt_count": 1, - "next_attempt_at": null, - "last_status_code": 200, - "last_error": null, - "created_at": "2026-01-15T09:30:00.000Z", - "finished_at": "2026-01-15T09:30:00.000Z" - } - ], - "has_more": false - } - ] - }, - "HookDelivery": { - "type": "object", - "properties": { - "object": { + "currency": { "type": "string", "enum": [ - "hook_delivery" - ] - }, - "id": { - "type": "string", - "examples": [ - "hdel_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "application_id": { - "type": "string", - "description": "The id of the application." + "usd" + ], + "description": "Always `usd`." }, - "message_id": { - "type": "string", - "description": "The id of the message being delivered." + "period": { + "type": "object", + "properties": { + "start": { + "type": "string", + "format": "date-time", + "description": "When the period starts (inclusive)." + }, + "end": { + "type": "string", + "format": "date-time", + "description": "When the period ends (exclusive)." + } + }, + "required": [ + "start", + "end" + ], + "description": "The current billing month (UTC)." }, - "endpoint_id": { - "type": "string", - "description": "The id of the endpoint it's delivered to." + "usage": { + "type": "array", + "items": { + "type": "object", + "properties": { + "meter": { + "type": "string", + "enum": [ + "email.recipients", + "email.dedicated_ip", + "email.attachment_data", + "email.received", + "email.received_data", + "email.inboxes", + "sms.segments", + "sms.received_segments", + "sms.numbers", + "sms.registrations", + "sign.completed_requests", + "risk.phone_checks", + "risk.assessments", + "risk.content_checks", + "verify.verifications", + "verify.sms", + "forms.submissions", + "forms.file_uploads", + "scheduling.bookings", + "scheduling.hosts", + "hooks.messages" + ], + "description": "What is counted, e.g. `email.recipients`." + }, + "quantity": { + "type": "integer", + "minimum": 0, + "description": "Units used so far this month." + }, + "included": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "description": "The plan's monthly allowance where usage stops (Free), else `null`." + } + }, + "required": [ + "meter", + "quantity", + "included" + ] + }, + "description": "This month's usage per meter." }, - "status": { - "type": "string", - "enum": [ - "pending", - "succeeded", - "failed" + "estimate": { + "type": "object", + "properties": { + "usage": { + "type": "integer", + "description": "Usage this month at your rates." + }, + "discount": { + "type": "integer", + "description": "An amount in cents (USD)." + }, + "credit": { + "type": "integer", + "description": "Credit applied to this estimate (your plan's fee is credit)." + }, + "total": { + "type": "integer", + "description": "Estimated total including usage and fees billed in arrears, after discounts and credits." + } + }, + "required": [ + "usage", + "discount", + "credit", + "total" ], - "description": "`pending`: waiting for its next attempt. `succeeded`: the endpoint answered 2xx. `failed`: every attempt failed, or the endpoint was disabled or deleted." + "description": "This month so far, as it would be invoiced if it ended now." }, - "attempt_count": { + "credit_remaining": { "type": "integer", - "description": "How many attempts were made so far." - }, - "next_attempt_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "When the next attempt runs. `null`: none is due, because it finished." + "description": "Unused credit usable this month." }, - "last_status_code": { + "spend_limit": { "type": [ "integer", "null" ], - "description": "The endpoint's HTTP status on the latest attempt. `null`: no response arrived, or no attempt yet." + "description": "Usage beyond the credit past which live sending is held; `null`: none." }, - "last_error": { + "credit_limit": { "type": [ - "string", + "object", "null" ], - "description": "Why the latest attempt failed. `null`: it didn't, or no attempt yet." - }, - "created_at": { - "type": "string", - "format": "date-time" + "properties": { + "limit": { + "type": "integer", + "description": "What coss.com carries before live sending is held. It rises as payments settle." + }, + "used": { + "type": "integer", + "description": "Usage not invoiced yet, unpaid invoices and recent usage charges." + }, + "charge_threshold": { + "type": [ + "integer", + "null" + ], + "description": "Usage not invoiced yet is charged early at this amount; `null`: it isn't charged automatically." + } + }, + "required": [ + "limit", + "used", + "charge_threshold" + ], + "description": "Where the organization stands against its credit limit; `null`: it has none." }, - "finished_at": { + "payment_method": { "type": [ - "string", + "object", "null" ], - "format": "date-time", - "description": "When it `succeeded` or `failed`. `null`: still `pending`." - } - }, - "required": [ - "object", - "id", - "application_id", - "message_id", - "endpoint_id", - "status", - "attempt_count", - "next_attempt_at", - "last_status_code", - "last_error", - "created_at", - "finished_at" - ], - "examples": [ - { - "object": "hook_delivery", - "id": "hdel_01k7c8m4q2f3e9v6t5r1n0p8yz", - "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", - "message_id": "hmsg_01k7c8m4q2f3e9v6t5r1n0p8yz", - "endpoint_id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", - "status": "succeeded", - "attempt_count": 1, - "next_attempt_at": null, - "last_status_code": 200, - "last_error": null, - "created_at": "2026-01-15T09:30:00.000Z", - "finished_at": "2026-01-15T09:30:00.000Z" - } - ] - }, - "SendHookMessageRequest": { - "type": "object", - "properties": { - "event_type": { - "type": "string", - "pattern": "^[A-Za-z0-9_.-]{1,64}$", - "description": "What happened, e.g. `invoice.paid`. Endpoints that filter by type only get the types they chose. The type doesn't have to be registered.", - "examples": [ - "invoice.paid" - ] - }, - "payload": { - "type": "object", - "additionalProperties": {}, - "description": "The JSON object delivered as the request body (at most 256 KB serialized). It is parsed and serialized again: send integers beyond 2^53 as strings.", - "examples": [ - { - "invoice_id": "in_1042", - "amount": 4200, - "currency": "usd" + "properties": { + "type": { + "type": "string", + "description": "Its type, such as `card`." + }, + "brand": { + "type": [ + "string", + "null" + ], + "description": "The card's brand. `null` for other types." + }, + "last4": { + "type": [ + "string", + "null" + ], + "description": "The card's last four digits. `null` for other types." } - ] - } - }, - "required": [ - "event_type", - "payload" - ], - "additionalProperties": false, - "examples": [ - { - "event_type": "invoice.paid", - "payload": { - "invoice_id": "in_1042", - "amount": 4200, - "currency": "usd" - } - } - ] - }, - "HookMessageList": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "list" - ] + }, + "required": [ + "type", + "brand", + "last4" + ], + "description": "The payment method on file. `null`: none." }, - "data": { + "add_ons": { "type": "array", "items": { - "$ref": "#/components/schemas/HookMessage" - } - }, - "has_more": { - "type": "boolean" - } - }, - "required": [ - "object", - "data", - "has_more" - ], - "examples": [ - { - "object": "list", - "data": [ - { - "object": "hook_message", - "id": "hmsg_01k7c8m4q2f3e9v6t5r1n0p8yz", - "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", - "event_type": "invoice.paid", - "payload": { - "invoice_id": "in_1042", - "amount": 4200, - "currency": "usd" + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "e.g. `dedicated_ips`." }, - "created_at": "2026-01-15T09:30:00.000Z" + "name": { + "type": "string", + "description": "The add-on's name." + }, + "quantity": { + "type": "integer", + "description": "How many units are on the contract." + }, + "unit_amount": { + "type": "integer", + "description": "The price per unit and interval, in cents." + }, + "interval": { + "type": "string", + "enum": [ + "month", + "quarter", + "year" + ], + "description": "How often it's charged." + } + }, + "required": [ + "key", + "name", + "quantity", + "unit_amount", + "interval" + ] + }, + "description": "Add-ons of the contract in effect, charged on top of the plan." + }, + "features": { + "type": "object", + "properties": { + "tracking_domains": { + "type": "boolean", + "description": "Custom tracking domains." + }, + "audit_log": { + "type": "boolean", + "description": "The organization's audit log." + }, + "log_drains": { + "type": "boolean", + "description": "Log drains." + }, + "sso": { + "type": "boolean", + "description": "Single sign-on." + }, + "scim": { + "type": "boolean", + "description": "Directory sync (SCIM)." + }, + "dedicated_ips": { + "type": "boolean", + "description": "Dedicated IP pools." } + }, + "required": [ + "tracking_domains", + "audit_log", + "log_drains", + "sso", + "scim", + "dedicated_ips" ], - "has_more": false + "description": "Which features the plan includes." + }, + "limits": { + "type": "object", + "properties": { + "domains": { + "type": [ + "integer", + "null" + ], + "description": "Sending domains. `null`: unlimited." + }, + "members": { + "type": [ + "integer", + "null" + ], + "description": "Members of the organization. `null`: unlimited." + }, + "webhook_endpoints_per_environment": { + "type": [ + "integer", + "null" + ], + "description": "Webhook endpoints per environment. `null`: unlimited." + }, + "contacts_per_environment": { + "type": [ + "integer", + "null" + ], + "description": "Contacts per environment. `null`: unlimited." + }, + "inboxes": { + "type": [ + "integer", + "null" + ], + "description": "Inboxes. `null`: unlimited." + } + }, + "required": [ + "domains", + "members", + "webhook_endpoints_per_environment", + "contacts_per_environment", + "inboxes" + ], + "description": "Resource limits (`null`: unlimited)." + } + }, + "required": [ + "object", + "organization_id", + "plan", + "contract", + "scheduled_change", + "status", + "holds", + "currency", + "period", + "usage", + "estimate", + "credit_remaining", + "spend_limit", + "credit_limit", + "payment_method", + "add_ons", + "features", + "limits" + ], + "examples": [ + { + "object": "billing", + "organization_id": "org_01k7c8m4q2f3e9v6t5r1n0p8yz", + "plan": "pro", + "contract": { + "id": "bcon_01k7c8m4q2f3e9v6t5r1n0p8yz", + "kind": "self_serve", + "effective_from": "2026-10-01T00:00:00.000Z", + "effective_until": null, + "term_end": null, + "auto_renew": false + }, + "scheduled_change": null, + "status": "active", + "holds": [], + "currency": "usd", + "period": { + "start": "2026-10-01T00:00:00.000Z", + "end": "2026-11-01T00:00:00.000Z" + }, + "usage": [ + { + "meter": "email.recipients", + "quantity": 120000, + "included": null + } + ], + "estimate": { + "usage": 4800, + "discount": 0, + "credit": 2500, + "total": 2300 + }, + "credit_remaining": 0, + "spend_limit": null, + "credit_limit": { + "limit": 50000, + "used": 2300, + "charge_threshold": 10000 + }, + "payment_method": { + "type": "card", + "brand": "visa", + "last4": "4242" + }, + "add_ons": [], + "features": { + "tracking_domains": true, + "audit_log": true, + "log_drains": false, + "sso": false, + "scim": false, + "dedicated_ips": false + }, + "limits": { + "domains": 10, + "members": 10, + "webhook_endpoints_per_environment": 10, + "contacts_per_environment": 50000, + "inboxes": 1000 + } } ] }, - "HookAttemptList": { + "UsageList": { "type": "object", "properties": { "object": { @@ -31205,11 +30908,14 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/HookAttempt" + "$ref": "#/components/schemas/Usage" } }, "has_more": { - "type": "boolean" + "type": "boolean", + "enum": [ + false + ] } }, "required": [ @@ -31222,180 +30928,137 @@ "object": "list", "data": [ { - "object": "hook_attempt", - "id": "hatt_01k7c8m4q2f3e9v6t5r1n0p8yz", - "delivery_id": "hdel_01k7c8m4q2f3e9v6t5r1n0p8yz", - "endpoint_id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", - "attempted_at": "2026-01-15T09:30:00.000Z", - "status_code": 200, - "duration_ms": 182, - "error": null, - "response_excerpt": "ok" + "object": "usage", + "meter": "email.recipients", + "dimension": null, + "environment_id": null, + "project_id": null, + "api_key_id": null, + "user_id": null, + "start": "2026-10-12T00:00:00.000Z", + "end": "2026-10-13T00:00:00.000Z", + "quantity": 4200 } ], "has_more": false } ] }, - "HookAttempt": { + "Usage": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "hook_attempt" - ] - }, - "id": { - "type": "string", - "examples": [ - "hatt_01k7c8m4q2f3e9v6t5r1n0p8yz" + "usage" ] }, - "delivery_id": { - "type": "string", - "description": "The id of the delivery this attempt belongs to." - }, - "endpoint_id": { - "type": "string", - "description": "The id of the endpoint it was sent to." - }, - "attempted_at": { + "meter": { "type": "string", - "format": "date-time", - "description": "When the attempt ran." + "enum": [ + "email.recipients", + "email.dedicated_ip", + "email.attachment_data", + "email.received", + "email.received_data", + "email.inboxes", + "sms.segments", + "sms.received_segments", + "sms.numbers", + "sms.registrations", + "sign.completed_requests", + "risk.phone_checks", + "risk.assessments", + "risk.content_checks", + "verify.verifications", + "verify.sms", + "forms.submissions", + "forms.file_uploads", + "scheduling.bookings", + "scheduling.hosts", + "hooks.messages" + ], + "description": "What is counted, e.g. `email.recipients`." }, - "status_code": { + "dimension": { "type": [ - "integer", + "string", "null" ], - "description": "The endpoint's HTTP status; `null` when no response arrived." - }, - "duration_ms": { - "type": "integer", - "description": "How long it took, in milliseconds." + "description": "e.g. `country=US` for SMS; `null` for none." }, - "error": { + "environment_id": { "type": [ "string", "null" ], - "description": "Why it failed. `null`: it didn't." + "description": "Set with `group_by=environment`; `null` there for organization-wide usage (SMS numbers and registration fees)." }, - "response_excerpt": { + "project_id": { "type": [ "string", "null" ], - "description": "At most 1 KB of the response body, as text." - } - }, - "required": [ - "object", - "id", - "delivery_id", - "endpoint_id", - "attempted_at", - "status_code", - "duration_ms", - "error", - "response_excerpt" - ], - "examples": [ - { - "object": "hook_attempt", - "id": "hatt_01k7c8m4q2f3e9v6t5r1n0p8yz", - "delivery_id": "hdel_01k7c8m4q2f3e9v6t5r1n0p8yz", - "endpoint_id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", - "attempted_at": "2026-01-15T09:30:00.000Z", - "status_code": 200, - "duration_ms": 182, - "error": null, - "response_excerpt": "ok" - } - ] - }, - "HookPortalSession": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "hook_portal_session" - ] - }, - "id": { - "type": "string", - "examples": [ - "hps_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "description": "Set with `group_by=project` (`null`: organization-wide usage, or usage recorded before projects were)." }, - "application_id": { - "type": "string", - "description": "The id of the application this session manages." + "api_key_id": { + "type": [ + "string", + "null" + ], + "description": "Set with `group_by=api_key` (`null`: the dashboard, or usage recorded before keys were)." }, - "url": { - "type": "string", - "description": "Open it or embed it in an iframe (from an allowed origin). The token is in the URL fragment, so it never reaches a server log: hand the URL only to that application's user." + "user_id": { + "type": [ + "string", + "null" + ], + "description": "Set with `group_by=user`: the member whose personal API key caused it (`null`: organization keys and the dashboard)." }, - "token": { + "start": { "type": "string", - "description": "The secret part of `url`, shown only here." - }, - "read_only": { - "type": "boolean", - "description": "Whether the user can only look." + "format": "date-time", + "description": "When this bucket starts (inclusive)." }, - "expires_at": { + "end": { "type": "string", "format": "date-time", - "description": "When the session stops working." + "description": "When this bucket ends (exclusive)." + }, + "quantity": { + "type": "integer", + "minimum": 0, + "description": "Units used in this bucket." } }, "required": [ "object", - "id", - "application_id", - "url", - "token", - "read_only", - "expires_at" + "meter", + "dimension", + "environment_id", + "project_id", + "api_key_id", + "user_id", + "start", + "end", + "quantity" ], "examples": [ { - "object": "hook_portal_session", - "id": "hps_01k7c8m4q2f3e9v6t5r1n0p8yz", - "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", - "url": "https://portal.example/s/hps_01k7c8m4q2f3e9v6t5r1n0p8yz#cossp_…", - "token": "cossp_…", - "read_only": false, - "expires_at": "2026-01-15T10:30:00.000Z" - } - ] - }, - "CreateHookPortalSessionRequest": { - "type": "object", - "properties": { - "expires_in": { - "type": "integer", - "minimum": 60, - "maximum": 86400, - "description": "Seconds until it expires: default 3600, at most 86400." - }, - "read_only": { - "type": "boolean", - "description": "The user can look but not change anything." - } - }, - "additionalProperties": false, - "examples": [ - { - "expires_in": 3600 + "object": "usage", + "meter": "email.recipients", + "dimension": null, + "environment_id": null, + "project_id": null, + "api_key_id": null, + "user_id": null, + "start": "2026-10-12T00:00:00.000Z", + "end": "2026-10-13T00:00:00.000Z", + "quantity": 4200 } ] }, - "ReceivedEmailList": { + "InvoiceList": { "type": "object", "properties": { "object": { @@ -31407,7 +31070,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ReceivedEmailSummary" + "$ref": "#/components/schemas/InvoiceSummary" } }, "has_more": { @@ -31424,751 +31087,712 @@ "object": "list", "data": [ { - "object": "received_email", - "id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", - "status": "received", - "blocked_reason": null, - "mail_from": "jane@example.com", - "recipients": [ - "support@inbound.acme.com" - ], - "from": "Jane Doe ", - "to": [ - "support@inbound.acme.com" - ], - "cc": [], - "subject": "Re: Your order", - "message_id": "", - "attachments": [ - { - "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", - "filename": "invoice.pdf", - "content_type": "application/pdf", - "content_id": null, - "disposition": "attachment", - "size": 48213 - } - ], - "size": 61432, - "spam": false, - "verdicts": { - "spam": "pass", - "virus": "pass", - "spf": "pass", - "dkim": "pass", - "dmarc": "pass", - "dmarc_policy": null + "object": "invoice", + "id": "inv_01k7c8m4q2f3e9v6t5r1n0p8yz", + "number": "COSS-000042", + "kind": "period", + "status": "paid", + "currency": "usd", + "period": { + "start": "2026-10-01T00:00:00.000Z", + "end": "2026-11-01T00:00:00.000Z" }, - "sandbox": false, - "received_at": "2026-10-10T12:00:00.000Z" + "subtotal": 7300, + "discount": 0, + "credit": 2500, + "tax": 0, + "total": 4800, + "amount_paid": 4800, + "amount_due": 0, + "collection_method": "charge_automatically", + "due_at": "2026-11-01T00:20:00.000Z", + "finalized_at": "2026-11-01T00:20:00.000Z", + "paid_at": "2026-11-01T00:21:00.000Z", + "voided_at": null, + "hosted_url": "https://invoice.stripe.com/i/acct_123/test_YWNjdF8x", + "pdf_url": "https://pay.stripe.com/invoice/acct_123/test_YWNjdF8x/pdf" } ], "has_more": false } ] }, - "ReceivedEmailSummary": { + "Invoice": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "received_email" + "invoice" ] }, "id": { - "type": "string", - "examples": [ - "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "type": "string" }, - "status": { + "number": { + "type": [ + "string", + "null" + ], + "description": "The invoice number. `null` until it's finalized." + }, + "kind": { "type": "string", "enum": [ - "received", - "blocked" + "period", + "plan_change", + "one_off", + "final", + "threshold" ], - "description": "`received`, or `blocked` when the message carried a virus: then only its envelope and verdicts are kept, never its content." + "description": "`period`: a billing month. `plan_change`: a change of plan. `one_off`: a single charge. `final`: the last invoice of an account. `threshold`: usage charged early, taken off the month's invoice." }, - "blocked_reason": { - "anyOf": [ - { + "status": { + "type": "string", + "enum": [ + "open", + "paid", + "void", + "uncollectible" + ], + "description": "`open`: not paid yet. `paid`: settled. `void`: canceled. `uncollectible`: written off." + }, + "currency": { + "type": "string", + "enum": [ + "usd" + ], + "description": "Always `usd`." + }, + "period": { + "type": "object", + "properties": { + "start": { "type": "string", - "enum": [ - "virus" - ] + "format": "date-time", + "description": "When the period starts (inclusive)." }, - { - "type": "null" + "end": { + "type": "string", + "format": "date-time", + "description": "When the period ends (exclusive)." } + }, + "required": [ + "start", + "end" ], - "description": "Why it's `blocked`. `null`: it isn't." + "description": "What period it covers." }, - "mail_from": { + "subtotal": { + "type": "integer", + "description": "An amount in cents (USD)." + }, + "discount": { + "type": "integer", + "description": "An amount in cents (USD)." + }, + "credit": { + "type": "integer", + "description": "An amount in cents (USD)." + }, + "tax": { + "type": "integer", + "description": "An amount in cents (USD)." + }, + "total": { + "type": "integer", + "description": "An amount in cents (USD)." + }, + "amount_paid": { + "type": "integer", + "description": "An amount in cents (USD)." + }, + "amount_due": { + "type": "integer", + "description": "An amount in cents (USD)." + }, + "collection_method": { "type": "string", - "description": "The envelope sender (`MAIL FROM`); empty for bounces." + "enum": [ + "charge_automatically", + "send_invoice" + ], + "description": "`charge_automatically`: charged to the payment method. `send_invoice`: paid by `due_at`." }, - "recipients": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The envelope recipients this environment received the message for (Bcc'd addresses included, other organizations' never)." + "due_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When payment is due. `null`: not set." }, - "from": { + "finalized_at": { "type": [ "string", "null" ], - "description": "The `From` header." + "format": "date-time", + "description": "When it was finalized. `null`: not yet." }, - "to": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The `To` header's addresses." + "paid_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When it was paid. `null`: not paid." }, - "cc": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The `Cc` header's addresses." + "voided_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When it was voided. `null`: it wasn't." }, - "subject": { + "hosted_url": { "type": [ "string", "null" ], - "description": "The `Subject` header. `null`: none." + "description": "A page to view (and pay) the invoice." }, - "message_id": { + "pdf_url": { "type": [ "string", "null" ], - "description": "The `Message-ID` header." + "description": "The invoice as a PDF. `null`: not available." }, - "attachments": { + "lines": { "type": "array", "items": { - "$ref": "#/components/schemas/ReceivedEmailAttachment" - }, - "description": "Its attachments, without their bytes." - }, - "size": { - "type": "integer", - "description": "Size of the raw message in bytes." - }, - "spam": { - "type": "boolean", - "description": "Whether the spam verdict is `fail`." - }, - "verdicts": { - "$ref": "#/components/schemas/ReceivedEmailVerdicts" - }, - "sandbox": { - "type": "boolean", - "description": "Whether it was received by a sandbox environment." - }, - "received_at": { - "type": "string", - "format": "date-time", - "description": "When it arrived." + "$ref": "#/components/schemas/InvoiceLine" + } } }, "required": [ "object", "id", + "number", + "kind", "status", - "blocked_reason", - "mail_from", - "recipients", - "from", - "to", - "cc", - "subject", - "message_id", - "attachments", - "size", - "spam", - "verdicts", - "sandbox", - "received_at" + "currency", + "period", + "subtotal", + "discount", + "credit", + "tax", + "total", + "amount_paid", + "amount_due", + "collection_method", + "due_at", + "finalized_at", + "paid_at", + "voided_at", + "hosted_url", + "pdf_url", + "lines" ], "examples": [ { - "object": "received_email", - "id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", - "status": "received", - "blocked_reason": null, - "mail_from": "jane@example.com", - "recipients": [ - "support@inbound.acme.com" - ], - "from": "Jane Doe ", - "to": [ - "support@inbound.acme.com" - ], - "cc": [], - "subject": "Re: Your order", - "message_id": "", - "attachments": [ + "object": "invoice", + "id": "inv_01k7c8m4q2f3e9v6t5r1n0p8yz", + "number": "COSS-000042", + "kind": "period", + "status": "paid", + "currency": "usd", + "period": { + "start": "2026-10-01T00:00:00.000Z", + "end": "2026-11-01T00:00:00.000Z" + }, + "subtotal": 7300, + "discount": 0, + "credit": 2500, + "tax": 0, + "total": 4800, + "amount_paid": 4800, + "amount_due": 0, + "collection_method": "charge_automatically", + "due_at": "2026-11-01T00:20:00.000Z", + "finalized_at": "2026-11-01T00:20:00.000Z", + "paid_at": "2026-11-01T00:21:00.000Z", + "voided_at": null, + "hosted_url": "https://invoice.stripe.com/i/acct_123/test_YWNjdF8x", + "pdf_url": "https://pay.stripe.com/invoice/acct_123/test_YWNjdF8x/pdf", + "lines": [ { - "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", - "filename": "invoice.pdf", - "content_type": "application/pdf", - "content_id": null, - "disposition": "attachment", - "size": 48213 + "object": "invoice_line", + "id": "invl_01k7c8m4q2f3e9v6t5r1n0p8yz", + "kind": "usage", + "description": "Email: 120,000 recipients", + "meter": "email.recipients", + "dimension": null, + "quantity": 120000, + "included_quantity": 0, + "unit_price": "0.0004", + "amount": 4800, + "period": { + "start": "2026-10-01T00:00:00.000Z", + "end": "2026-11-01T00:00:00.000Z" + } } - ], - "size": 61432, - "spam": false, - "verdicts": { - "spam": "pass", - "virus": "pass", - "spf": "pass", - "dkim": "pass", - "dmarc": "pass", - "dmarc_policy": null - }, - "sandbox": false, - "received_at": "2026-10-10T12:00:00.000Z" + ] } ] }, - "ReceivedEmailAttachment": { + "InvoiceLine": { "type": "object", "properties": { + "object": { + "type": "string", + "enum": [ + "invoice_line" + ] + }, "id": { "type": "string" }, - "filename": { + "kind": { + "type": "string", + "enum": [ + "plan_fee", + "proration", + "usage", + "credit", + "discount", + "commitment_fee", + "commitment_shortfall", + "add_on", + "adjustment", + "write_off" + ], + "description": "What it's for: `plan_fee`, `proration`, `usage`, `credit`, `discount`, `commitment_fee`, `commitment_shortfall`, `add_on`, `adjustment` or `write_off`." + }, + "description": { + "type": "string", + "description": "The line as it reads on the invoice." + }, + "meter": { "type": [ "string", "null" ], - "description": "`null` when the part named none." - }, - "content_type": { - "type": "string" + "enum": [ + "email.recipients", + "email.dedicated_ip", + "email.attachment_data", + "email.received", + "email.received_data", + "email.inboxes", + "sms.segments", + "sms.received_segments", + "sms.numbers", + "sms.registrations", + "sign.completed_requests", + "risk.phone_checks", + "risk.assessments", + "risk.content_checks", + "verify.verifications", + "verify.sms", + "forms.submissions", + "forms.file_uploads", + "scheduling.bookings", + "scheduling.hosts", + "hooks.messages", + null + ], + "description": "The meter of a `usage` line. `null` for other lines." }, - "content_id": { + "dimension": { "type": [ "string", "null" ], - "description": "Set for parts the HTML references (`cid:` URLs)." + "description": "What the meter is split by, such as `country=US`. `null`: not split." }, - "disposition": { - "type": "string", - "enum": [ - "attachment", - "inline" - ] + "quantity": { + "type": [ + "integer", + "null" + ], + "description": "Units on this line. `null` when it isn't priced by the unit." }, - "size": { + "included_quantity": { + "type": [ + "integer", + "null" + ], + "description": "Units covered by an allowance or committed volume (not charged)." + }, + "unit_price": { + "type": [ + "string", + "null" + ], + "description": "Dollars per unit, e.g. `0.0004`; `null` when the price varies by volume." + }, + "amount": { "type": "integer", - "description": "Size in bytes." + "description": "An amount in cents (USD)." + }, + "period": { + "type": "object", + "properties": { + "start": { + "type": "string", + "format": "date-time", + "description": "When the period starts (inclusive)." + }, + "end": { + "type": "string", + "format": "date-time", + "description": "When the period ends (exclusive)." + } + }, + "required": [ + "start", + "end" + ], + "description": "What period it covers." } }, "required": [ + "object", "id", - "filename", - "content_type", - "content_id", - "disposition", - "size" + "kind", + "description", + "meter", + "dimension", + "quantity", + "included_quantity", + "unit_price", + "amount", + "period" ], "examples": [ { - "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", - "filename": "invoice.pdf", - "content_type": "application/pdf", - "content_id": null, - "disposition": "attachment", - "size": 48213 + "object": "invoice_line", + "id": "invl_01k7c8m4q2f3e9v6t5r1n0p8yz", + "kind": "usage", + "description": "Email: 120,000 recipients", + "meter": "email.recipients", + "dimension": null, + "quantity": 120000, + "included_quantity": 0, + "unit_price": "0.0004", + "amount": 4800, + "period": { + "start": "2026-10-01T00:00:00.000Z", + "end": "2026-11-01T00:00:00.000Z" + } } ] }, - "InboundAddress": { + "CreditList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "inbound_address" + "list" ] }, - "environment_id": { - "type": "string" + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Credit" + } }, - "domain": { - "type": "string", - "description": "Email sent to any address at this domain is received by the environment, with no DNS setup. It never changes." + "has_more": { + "type": "boolean" } }, "required": [ "object", - "environment_id", - "domain" + "data", + "has_more" ], "examples": [ { - "object": "inbound_address", - "environment_id": "env_01k7c8m4q2f3e9v6t5r1n0p8yz", - "domain": "k3j9x2m7qp4l.inbound.coss.com" + "object": "list", + "data": [ + { + "object": "credit", + "id": "bcg_01k7c8m4q2f3e9v6t5r1n0p8yz", + "kind": "units", + "amount": 50000, + "remaining": 12000, + "meter": "email.recipients", + "meters": null, + "applies_to": "usage", + "source": "support", + "description": "Migration credit", + "effective_at": "2026-10-01T00:00:00.000Z", + "expires_at": "2027-01-01T00:00:00.000Z", + "voided_at": null, + "created_at": "2026-10-01T09:30:00.000Z" + } + ], + "has_more": false } ] }, - "ReceivedEmail": { + "LogDrainWithSecret": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "received_email" + "log_drain" ] }, "id": { "type": "string", "examples": [ - "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz" + "drain_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, - "status": { + "url": { "type": "string", - "enum": [ - "received", - "blocked" - ], - "description": "`received`, or `blocked` when the message carried a virus: then only its envelope and verdicts are kept, never its content." + "description": "Where batches are posted.", + "examples": [ + "https://logs.example.com/coss" + ] }, - "blocked_reason": { - "anyOf": [ - { - "type": "string", - "enum": [ - "virus" - ] - }, - { - "type": "null" - } + "description": { + "type": [ + "string", + "null" ], - "description": "Why it's `blocked`. `null`: it isn't." - }, - "mail_from": { - "type": "string", - "description": "The envelope sender (`MAIL FROM`); empty for bounces." + "description": "Your note about it. `null`: none." }, - "recipients": { + "sources": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/LogDrainSource" }, - "description": "The envelope recipients this environment received the message for (Bcc'd addresses included, other organizations' never)." + "description": "What the drain forwards: `api` (requests made with the environment's API keys), `email` (email events), `webhook` (webhook delivery attempts) and `audit` (changes to the environment, from the organization's audit log). An empty array means every source, including ones added later." }, - "from": { + "format": { + "$ref": "#/components/schemas/LogDrainFormat" + }, + "auth_header_name": { "type": [ "string", "null" ], - "description": "The `From` header." - }, - "to": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The `To` header's addresses." - }, - "cc": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The `Cc` header's addresses." - }, - "subject": { - "type": [ - "string", - "null" - ], - "description": "The `Subject` header. `null`: none." - }, - "message_id": { - "type": [ - "string", - "null" - ], - "description": "The `Message-ID` header." - }, - "attachments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ReceivedEmailAttachment" - }, - "description": "Its attachments, without their bytes." - }, - "size": { - "type": "integer", - "description": "Size of the raw message in bytes." - }, - "spam": { - "type": "boolean", - "description": "Whether the spam verdict is `fail`." - }, - "verdicts": { - "$ref": "#/components/schemas/ReceivedEmailVerdicts" + "description": "The header sent with every batch to authenticate it (e.g. `Authorization`, `DD-API-KEY`), or `null`. Its value is never returned.", + "examples": [ + "DD-API-KEY" + ] }, - "sandbox": { + "enabled": { "type": "boolean", - "description": "Whether it was received by a sandbox environment." - }, - "received_at": { - "type": "string", - "format": "date-time", - "description": "When it arrived." - }, - "reply_to": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The `Reply-To` header's addresses." + "description": "A disabled drain receives nothing, and lines logged while it is disabled are not kept for it." }, - "in_reply_to": { + "disabled_reason": { "type": [ "string", "null" ], - "description": "The `In-Reply-To` header. `null`: none." - }, - "references": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Message ids of the `References` header." + "enum": [ + "manual", + "failing", + null + ], + "description": "`manual`: someone disabled it. `failing`: a batch failed every attempt, so coss.com disabled it. `null` while enabled." }, - "sent_at": { + "disabled_at": { "type": [ "string", "null" ], "format": "date-time", - "description": "The `Date` header, when valid." - }, - "headers": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The header's name." - }, - "value": { - "type": "string", - "description": "The header's value." - } - }, - "required": [ - "name", - "value" - ] - }, - "description": "Every header in message order (duplicates included)." + "description": "When it was disabled. `null` while enabled." }, - "text": { - "type": [ - "string", - "null" - ], - "description": "The plain text body. `null`: none, or it's `blocked`." + "created_at": { + "type": "string", + "format": "date-time" }, - "html": { - "type": [ - "string", - "null" - ], - "description": "The HTML body. `null`: none, or it's `blocked`." + "updated_at": { + "type": "string", + "format": "date-time" }, - "truncated": { - "type": "boolean", - "description": "Whether `text`, `html` or `headers` were shortened (2 MB per body, 64 KB of headers). The raw message always has all of it." + "secret": { + "type": "string", + "description": "The drain's `whsec_…` signing secret, used to verify the batches coss.com sends. Returned only when the drain is created — store it now; it cannot be read back through the API.", + "examples": [ + "whsec_6Jk…" + ] } }, "required": [ "object", "id", - "status", - "blocked_reason", - "mail_from", - "recipients", - "from", - "to", - "cc", - "subject", - "message_id", - "attachments", - "size", - "spam", - "verdicts", - "sandbox", - "received_at", - "reply_to", - "in_reply_to", - "references", - "sent_at", - "headers", - "text", - "html", - "truncated" + "url", + "description", + "sources", + "format", + "auth_header_name", + "enabled", + "disabled_reason", + "disabled_at", + "created_at", + "updated_at", + "secret" ], "examples": [ { - "object": "received_email", - "id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", - "status": "received", - "blocked_reason": null, - "mail_from": "jane@example.com", - "recipients": [ - "support@inbound.acme.com" - ], - "from": "Jane Doe ", - "to": [ - "support@inbound.acme.com" - ], - "cc": [], - "subject": "Re: Your order", - "message_id": "", - "attachments": [ - { - "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", - "filename": "invoice.pdf", - "content_type": "application/pdf", - "content_id": null, - "disposition": "attachment", - "size": 48213 - } - ], - "size": 61432, - "spam": false, - "verdicts": { - "spam": "pass", - "virus": "pass", - "spf": "pass", - "dkim": "pass", - "dmarc": "pass", - "dmarc_policy": null - }, - "sandbox": false, - "received_at": "2026-10-10T12:00:00.000Z", - "reply_to": [], - "in_reply_to": "", - "references": [ - "" - ], - "sent_at": "2026-10-10T11:59:58.000Z", - "headers": [ - { - "name": "From", - "value": "Jane Doe " - }, - { - "name": "To", - "value": "support@inbound.acme.com" - }, - { - "name": "Subject", - "value": "Re: Your order" - } + "object": "log_drain", + "id": "drain_01k7c8m4q2f3e9v6t5r1n0p8yz", + "url": "https://logs.example.com/coss", + "description": "Production logs", + "sources": [ + "api", + "email" ], - "text": "Hi, the order arrived damaged.", - "html": "

Hi, the order arrived damaged.

", - "truncated": false + "format": "ndjson", + "auth_header_name": null, + "enabled": true, + "disabled_reason": null, + "disabled_at": null, + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z", + "secret": "whsec_6Jk…" } ] }, - "DeletedReceivedEmail": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "received_email" - ] - }, - "id": { - "type": "string" - }, - "deleted": { - "type": "boolean", - "enum": [ - true - ] - } - }, - "required": [ - "object", - "id", - "deleted" + "LogDrainSource": { + "type": "string", + "enum": [ + "api", + "email", + "webhook", + "risk", + "smtp", + "sms", + "sign", + "verify", + "forms", + "scheduling", + "audit" ], - "examples": [ - { - "object": "received_email", - "id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", - "deleted": true - } - ] + "description": "What the drain forwards: `api` (requests made with the environment's API keys), `email` (email events), `webhook` (webhook delivery attempts) and `audit` (changes to the environment, from the organization's audit log). An empty array means every source, including ones added later." }, - "Inbox": { + "LogDrainFormat": { + "type": "string", + "enum": [ + "ndjson", + "loki", + "datadog", + "otlp" + ], + "description": "How batches are encoded. `ndjson`: one JSON object per line (`application/x-ndjson`), for your own endpoint. `loki`: Loki's push API (`/loki/api/v1/push`, Grafana Cloud Logs). `datadog`: Datadog's log intake (`/api/v2/logs`). `otlp`: OTLP/HTTP logs in JSON (`/v1/logs`, Grafana Cloud, Honeycomb, an OpenTelemetry Collector…)." + }, + "CreateLogDrainRequest": { "type": "object", "properties": { - "object": { + "url": { "type": "string", - "enum": [ - "inbox" + "minLength": 1, + "maxLength": 2048, + "description": "An `https://` URL coss.com posts batches of log lines to.", + "examples": [ + "https://logs.example.com/coss" ] }, - "id": { - "type": "string" - }, - "address": { - "type": "string", - "description": "The inbox's email address (`username@domain`)." - }, - "username": { - "type": "string", - "description": "The part of the address before the `@`." - }, - "domain": { - "type": "string", - "description": "The part of the address after the `@`." - }, - "kind": { - "type": "string", - "enum": [ - "managed", - "domain" - ], - "description": "`managed`: at the environment's own domain under coss.com's inbound domain (no DNS setup). `domain`: at one of the environment's receiving domains." - }, - "display_name": { - "type": [ - "string", - "null" - ], - "description": "The name messages from the inbox are sent with." - }, - "client_id": { + "description": { "type": [ "string", "null" ], - "description": "Your own id for the inbox, e.g. your agent's or user's (unique per environment)." - }, - "can_send": { - "type": "boolean", - "description": "Whether messages can be sent from the inbox. Inboxes at coss.com's inbound domain only receive in live environments: create the inbox at a receiving domain under one of your verified domains to send from it." + "maxLength": 256, + "description": "A note for your own reference. `null` clears it." }, - "sandbox": { - "type": "boolean", - "description": "Whether it belongs to a sandbox environment." + "sources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogDrainSource" + }, + "maxItems": 11, + "description": "What the drain forwards: `api` (requests made with the environment's API keys), `email` (email events), `webhook` (webhook delivery attempts) and `audit` (changes to the environment, from the organization's audit log). An empty array means every source, including ones added later. Defaults to every source." }, - "created_at": { + "format": { "type": "string", - "format": "date-time" + "enum": [ + "ndjson", + "loki", + "datadog", + "otlp" + ], + "description": "How batches are encoded. `ndjson`: one JSON object per line (`application/x-ndjson`), for your own endpoint. `loki`: Loki's push API (`/loki/api/v1/push`, Grafana Cloud Logs). `datadog`: Datadog's log intake (`/api/v2/logs`). `otlp`: OTLP/HTTP logs in JSON (`/v1/logs`, Grafana Cloud, Honeycomb, an OpenTelemetry Collector…). Defaults to `ndjson`." }, - "updated_at": { - "type": "string", - "format": "date-time" + "auth_header": { + "$ref": "#/components/schemas/LogDrainAuthHeader" } }, "required": [ - "object", - "id", - "address", - "username", - "domain", - "kind", - "display_name", - "client_id", - "can_send", - "sandbox", - "created_at", - "updated_at" + "url" ], + "additionalProperties": false, "examples": [ { - "object": "inbox", - "id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", - "address": "support-agent@k3j9x2m7qp4l.inbound.coss.com", - "username": "support-agent", - "domain": "k3j9x2m7qp4l.inbound.coss.com", - "kind": "managed", - "display_name": "Acme Support", - "client_id": "agent-42", - "can_send": true, - "sandbox": true, - "created_at": "2026-10-10T12:00:00.000Z", - "updated_at": "2026-10-10T12:00:00.000Z" + "url": "https://logs.example.com/coss", + "description": "Production logs", + "sources": [ + "api", + "email" + ] + }, + { + "url": "https://http-intake.logs.datadoghq.com/api/v2/logs", + "description": "Datadog", + "format": "datadog", + "auth_header": { + "name": "DD-API-KEY", + "value": "" + } } ] }, - "CreateInboxRequest": { - "type": "object", + "LogDrainAuthHeader": { + "type": [ + "object", + "null" + ], "properties": { - "username": { - "type": "string", - "maxLength": 64, - "pattern": "^(?!.*\\.\\.)[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$", - "description": "The part before the @. Omit it for a random one. Mail to `username+anything@domain` reaches the inbox too.", - "examples": [ - "support-agent" - ] - }, - "domain": { + "name": { "type": "string", "minLength": 1, - "maxLength": 253, - "description": "One of the environment's receiving domains (its hostname). Omit it for the environment's own domain under coss.com's inbound domain, which needs no DNS setup.", + "maxLength": 128, + "description": "The header's name.", "examples": [ - "agents.acme.com" + "Authorization" ] }, - "display_name": { + "value": { "type": "string", "minLength": 1, - "maxLength": 64, - "description": "The name messages from the inbox are sent with.", - "examples": [ - "Acme Support" - ] - }, - "client_id": { - "type": "string", - "pattern": "^[\\x21-\\x7e]{1,128}$", - "description": "Your own id for the inbox (unique per environment). Creating an inbox with a `client_id` that already has one returns that inbox instead (200), so retries and \"one inbox per user\" need no bookkeeping.", + "maxLength": 4096, + "pattern": "^[\\t\\x20-\\x7e]+$", + "description": "The header's value. Encrypted, and never returned.", "examples": [ - "agent-42" + "Bearer " ] } }, + "required": [ + "name", + "value" + ], "additionalProperties": false, - "examples": [ - { - "username": "support-agent", - "display_name": "Acme Support", - "client_id": "agent-42" - } - ] + "description": "A header sent with every batch, for intake APIs that take a key: `Authorization` (`Basic …`, `Bearer …`), `DD-API-KEY`… Its value is stored encrypted and never returned. `null` removes it." }, - "InboxList": { + "LogDrainList": { "type": "object", "properties": { "object": { @@ -32180,7 +31804,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Inbox" + "$ref": "#/components/schemas/LogDrain" } }, "has_more": { @@ -32197,55 +31821,196 @@ "object": "list", "data": [ { - "object": "inbox", - "id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", - "address": "support-agent@k3j9x2m7qp4l.inbound.coss.com", - "username": "support-agent", - "domain": "k3j9x2m7qp4l.inbound.coss.com", - "kind": "managed", - "display_name": "Acme Support", - "client_id": "agent-42", - "can_send": true, - "sandbox": true, - "created_at": "2026-10-10T12:00:00.000Z", - "updated_at": "2026-10-10T12:00:00.000Z" + "object": "log_drain", + "id": "drain_01k7c8m4q2f3e9v6t5r1n0p8yz", + "url": "https://logs.example.com/coss", + "description": "Production logs", + "sources": [ + "api", + "email" + ], + "format": "ndjson", + "auth_header_name": null, + "enabled": true, + "disabled_reason": null, + "disabled_at": null, + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z" } ], "has_more": false } ] }, - "UpdateInboxRequest": { + "LogDrain": { "type": "object", "properties": { - "display_name": { - "anyOf": [ - { - "type": "string", - "minLength": 1, - "maxLength": 64 - }, - { - "type": "null" - } - ], - "description": "The name messages are sent with; `null` sends with the bare address." + "object": { + "type": "string", + "enum": [ + "log_drain" + ] + }, + "id": { + "type": "string", + "examples": [ + "drain_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "url": { + "type": "string", + "description": "Where batches are posted.", + "examples": [ + "https://logs.example.com/coss" + ] + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Your note about it. `null`: none." + }, + "sources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogDrainSource" + }, + "description": "What the drain forwards: `api` (requests made with the environment's API keys), `email` (email events), `webhook` (webhook delivery attempts) and `audit` (changes to the environment, from the organization's audit log). An empty array means every source, including ones added later." + }, + "format": { + "$ref": "#/components/schemas/LogDrainFormat" + }, + "auth_header_name": { + "type": [ + "string", + "null" + ], + "description": "The header sent with every batch to authenticate it (e.g. `Authorization`, `DD-API-KEY`), or `null`. Its value is never returned.", + "examples": [ + "DD-API-KEY" + ] + }, + "enabled": { + "type": "boolean", + "description": "A disabled drain receives nothing, and lines logged while it is disabled are not kept for it." + }, + "disabled_reason": { + "type": [ + "string", + "null" + ], + "enum": [ + "manual", + "failing", + null + ], + "description": "`manual`: someone disabled it. `failing`: a batch failed every attempt, so coss.com disabled it. `null` while enabled." + }, + "disabled_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When it was disabled. `null` while enabled." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "object", + "id", + "url", + "description", + "sources", + "format", + "auth_header_name", + "enabled", + "disabled_reason", + "disabled_at", + "created_at", + "updated_at" + ], + "examples": [ + { + "object": "log_drain", + "id": "drain_01k7c8m4q2f3e9v6t5r1n0p8yz", + "url": "https://logs.example.com/coss", + "description": "Production logs", + "sources": [ + "api", + "email" + ], + "format": "ndjson", + "auth_header_name": null, + "enabled": true, + "disabled_reason": null, + "disabled_at": null, + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z" + } + ] + }, + "UpdateLogDrainRequest": { + "type": "object", + "properties": { + "url": { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "description": "An `https://` URL coss.com posts batches of log lines to.", + "examples": [ + "https://logs.example.com/coss" + ] + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "A note for your own reference. `null` clears it." + }, + "sources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogDrainSource" + }, + "maxItems": 11, + "description": "What the drain forwards: `api` (requests made with the environment's API keys), `email` (email events), `webhook` (webhook delivery attempts) and `audit` (changes to the environment, from the organization's audit log). An empty array means every source, including ones added later." + }, + "format": { + "$ref": "#/components/schemas/LogDrainFormat" + }, + "auth_header": { + "$ref": "#/components/schemas/LogDrainAuthHeader" + }, + "enabled": { + "type": "boolean", + "description": "`false` disables it, and `true` enables it again." } }, "additionalProperties": false, "examples": [ { - "display_name": "Acme Help" + "enabled": false } ] }, - "DeletedInbox": { + "DeletedLogDrain": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "inbox" + "log_drain" ] }, "id": { @@ -32265,13 +32030,13 @@ ], "examples": [ { - "object": "inbox", - "id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", + "object": "log_drain", + "id": "drain_01k7c8m4q2f3e9v6t5r1n0p8yz", "deleted": true } ] }, - "InboxThreadList": { + "LogDrainBatchList": { "type": "object", "properties": { "object": { @@ -32283,7 +32048,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/InboxThreadSummary" + "$ref": "#/components/schemas/LogDrainBatch" } }, "has_more": { @@ -32300,563 +32065,365 @@ "object": "list", "data": [ { - "object": "inbox_thread", - "id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", - "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", - "subject": "My order arrived damaged", - "participants": [ - "jane@example.com" + "object": "log_drain_batch", + "id": "drainbat_01k7c8n2b5d7f9h1k3m5p7r9tv", + "drain_id": "drain_01k7c8m4q2f3e9v6t5r1n0p8yz", + "status": "succeeded", + "line_count": 42, + "byte_size": 31870, + "attempt_count": 1, + "next_attempt_at": null, + "last_status_code": 200, + "last_error": null, + "attempts": [ + { + "object": "log_drain_attempt", + "id": "drainatt_01k7c8n2c9e1g3j5l7n9q1s3uw", + "attempted_at": "2026-01-15T09:30:01.000Z", + "status_code": 200, + "duration_ms": 182, + "error": null, + "response_excerpt": "ok" + } ], - "message_count": 2, - "preview": "Sorry about that, Jane! A new one is on its way.", - "last_message_at": "2026-10-10T12:01:00.000Z", - "last_inbound_at": "2026-10-10T12:00:00.000Z", - "created_at": "2026-10-10T12:00:00.000Z" + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:01.000Z" } ], "has_more": false } ] }, - "InboxThreadSummary": { + "LogDrainBatch": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "inbox_thread" + "log_drain_batch" ] }, "id": { - "type": "string" + "type": "string", + "description": "The batch's id: the `webhook-id` header, the same on every attempt.", + "examples": [ + "drainbat_01k7c8n2b5d7f9h1k3m5p7r9tv" + ] }, - "inbox_id": { + "drain_id": { "type": "string", - "description": "The inbox's id." + "description": "The id of the drain it's posted to." }, - "subject": { - "type": [ - "string", - "null" - ], - "description": "The first message's subject." + "status": { + "$ref": "#/components/schemas/LogDrainBatchStatus" }, - "participants": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Addresses of everyone in the conversation but the inbox (Bcc'd ones never)." + "line_count": { + "type": "integer", + "description": "Lines in the batch." }, - "message_count": { + "byte_size": { "type": "integer", - "description": "Messages in the thread." + "description": "Size of the request body in bytes." }, - "preview": { + "attempt_count": { + "type": "integer", + "description": "How many attempts were made so far." + }, + "next_attempt_at": { "type": [ "string", "null" ], - "description": "The start of the latest message." - }, - "last_message_at": { - "type": "string", "format": "date-time", - "description": "When its latest message was received or sent." + "description": "When the next attempt is due; `null` once the batch is finished." }, - "last_inbound_at": { + "last_status_code": { + "type": [ + "integer", + "null" + ], + "description": "The drain's HTTP status on the latest attempt. `null`: no response arrived, or no attempt yet." + }, + "last_error": { "type": [ "string", "null" ], - "format": "date-time", - "description": "When its latest inbound message arrived: later than `last_message_at`'s outbound one means the conversation awaits a reply." + "description": "Why the latest attempt failed. `null`: it didn't, or no attempt yet." + }, + "attempts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogDrainAttempt" + }, + "description": "Every HTTP attempt of this batch, oldest first." }, "created_at": { "type": "string", "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" } }, "required": [ "object", "id", - "inbox_id", - "subject", - "participants", - "message_count", - "preview", - "last_message_at", - "last_inbound_at", - "created_at" + "drain_id", + "status", + "line_count", + "byte_size", + "attempt_count", + "next_attempt_at", + "last_status_code", + "last_error", + "attempts", + "created_at", + "updated_at" ], "examples": [ { - "object": "inbox_thread", - "id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", - "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", - "subject": "My order arrived damaged", - "participants": [ - "jane@example.com" + "object": "log_drain_batch", + "id": "drainbat_01k7c8n2b5d7f9h1k3m5p7r9tv", + "drain_id": "drain_01k7c8m4q2f3e9v6t5r1n0p8yz", + "status": "succeeded", + "line_count": 42, + "byte_size": 31870, + "attempt_count": 1, + "next_attempt_at": null, + "last_status_code": 200, + "last_error": null, + "attempts": [ + { + "object": "log_drain_attempt", + "id": "drainatt_01k7c8n2c9e1g3j5l7n9q1s3uw", + "attempted_at": "2026-01-15T09:30:01.000Z", + "status_code": 200, + "duration_ms": 182, + "error": null, + "response_excerpt": "ok" + } ], - "message_count": 2, - "preview": "Sorry about that, Jane! A new one is on its way.", - "last_message_at": "2026-10-10T12:01:00.000Z", - "last_inbound_at": "2026-10-10T12:00:00.000Z", - "created_at": "2026-10-10T12:00:00.000Z" + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:01.000Z" } ] }, - "InboxThread": { + "LogDrainBatchStatus": { + "type": "string", + "enum": [ + "pending", + "succeeded", + "failed" + ], + "description": "`pending`: waiting for its next attempt. `succeeded`: the drain answered with a 2xx status. `failed`: every attempt failed, or the drain was disabled." + }, + "LogDrainAttempt": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "inbox_thread" + "log_drain_attempt" ] }, "id": { "type": "string" }, - "inbox_id": { + "attempted_at": { "type": "string", - "description": "The inbox's id." + "format": "date-time", + "description": "When the attempt ran." }, - "subject": { + "status_code": { "type": [ - "string", + "integer", "null" ], - "description": "The first message's subject." - }, - "participants": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Addresses of everyone in the conversation but the inbox (Bcc'd ones never)." + "description": "The drain's HTTP status; `null` when no response arrived." }, - "message_count": { + "duration_ms": { "type": "integer", - "description": "Messages in the thread." + "description": "How long it took, in milliseconds." }, - "preview": { + "error": { "type": [ "string", "null" ], - "description": "The start of the latest message." - }, - "last_message_at": { - "type": "string", - "format": "date-time", - "description": "When its latest message was received or sent." + "description": "Why the attempt failed (e.g. `HTTP 500`, a timeout); `null` on success." }, - "last_inbound_at": { + "response_excerpt": { "type": [ "string", "null" ], - "format": "date-time", - "description": "When its latest inbound message arrived: later than `last_message_at`'s outbound one means the conversation awaits a reply." - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "messages": { - "type": "array", - "items": { - "$ref": "#/components/schemas/InboxMessage" - }, - "description": "The thread's messages, oldest first: its latest 50 (see `message_count`; list the inbox's messages for older ones)." + "description": "At most 1 KB of the response body, as text." } }, "required": [ "object", "id", - "inbox_id", - "subject", - "participants", - "message_count", - "preview", - "last_message_at", - "last_inbound_at", - "created_at", - "messages" + "attempted_at", + "status_code", + "duration_ms", + "error", + "response_excerpt" ], "examples": [ { - "object": "inbox_thread", - "id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", - "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", - "subject": "My order arrived damaged", - "participants": [ - "jane@example.com" - ], - "message_count": 2, - "preview": "Sorry about that, Jane! A new one is on its way.", - "last_message_at": "2026-10-10T12:01:00.000Z", - "last_inbound_at": "2026-10-10T12:00:00.000Z", - "created_at": "2026-10-10T12:00:00.000Z", - "messages": [ - { - "object": "inbox_message", - "id": "imsg_01k7c8m4s1d3c9x0z4e6g7h8j9", - "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", - "thread_id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", - "direction": "inbound", - "status": "received", - "from": "Jane Doe ", - "to": [ - "support-agent@k3j9x2m7qp4l.inbound.coss.com" - ], - "cc": [], - "subject": "My order arrived damaged", - "preview": "Hi, the order arrived damaged. Can you send a new one?", - "attachments": [ - { - "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", - "filename": "photo.jpg", - "content_type": "image/jpeg", - "content_id": null, - "disposition": "attachment", - "size": 48213 - } - ], - "spam": false, - "received_email_id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", - "email_id": null, - "created_at": "2026-10-10T12:00:00.000Z", - "reply_to": [], - "message_id": "", - "in_reply_to": null, - "references": [], - "text": "Hi, the order arrived damaged. Can you send a new one?\n\nJane", - "extracted_text": "Hi, the order arrived damaged. Can you send a new one?\n\nJane", - "html": null, - "truncated": false - }, - { - "object": "inbox_message", - "id": "imsg_01k7c8m5t2e4d0y1a5f7h8j9k0", - "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", - "thread_id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", - "direction": "outbound", - "status": "queued", - "from": "\"Acme Support\" ", - "to": [ - "jane@example.com" - ], - "cc": [], - "subject": "Re: My order arrived damaged", - "preview": "Sorry about that, Jane! A new one is on its way.", - "attachments": [], - "spam": false, - "received_email_id": null, - "email_id": "email_01k7c8m5t2e4d0y1a5f7h8j9k0", - "created_at": "2026-10-10T12:01:00.000Z", - "reply_to": [], - "message_id": null, - "in_reply_to": "", - "references": [ - "" - ], - "text": "Sorry about that, Jane! A new one is on its way.", - "extracted_text": "Sorry about that, Jane! A new one is on its way.", - "html": null, - "truncated": false - } - ] + "object": "log_drain_attempt", + "id": "drainatt_01k7c8n2c9e1g3j5l7n9q1s3uw", + "attempted_at": "2026-01-15T09:30:01.000Z", + "status_code": 200, + "duration_ms": 182, + "error": null, + "response_excerpt": "ok" } ] }, - "InboxMessage": { + "HookEventType": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "inbox_message" + "hook_event_type" ] }, - "id": { - "type": "string" - }, - "inbox_id": { - "type": "string" - }, - "thread_id": { - "type": "string" - }, - "direction": { + "name": { "type": "string", - "enum": [ - "inbound", - "outbound" + "description": "The type's name, which messages send as `event_type`.", + "examples": [ + "invoice.paid" + ] + }, + "description": { + "type": [ + "string", + "null" ], - "description": "`inbound`: received by the inbox. `outbound`: sent from it." + "description": "What this event means, shown to your users. `null`: none." }, - "status": { - "type": "string", - "enum": [ - "received", - "queued", - "sent", - "failed", - "scheduled", - "canceled" - ], - "description": "`received` for inbound messages; an outbound message's email status (`queued`, `sent`, `failed`, …): its email has each recipient's delivery." - }, - "from": { - "type": [ - "string", - "null" - ], - "description": "The `From` header." - }, - "to": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Addresses in the `To` header." - }, - "cc": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Addresses in the `Cc` header." - }, - "subject": { - "type": [ - "string", - "null" - ], - "description": "The subject. `null`: none." - }, - "preview": { + "schema": { "type": [ - "string", + "object", "null" ], - "description": "The start of the message's own text (without the quoted conversation)." + "additionalProperties": {}, + "description": "A JSON Schema of the payload, shown to your users (not enforced). `null`: none." }, - "attachments": { + "examples": { "type": "array", "items": { - "$ref": "#/components/schemas/ReceivedEmailAttachment" + "type": "object", + "additionalProperties": {} }, - "description": "An inbound message's attachments (download them from the message)." + "description": "Sample payloads, shown to your users. Test sends use the first." }, - "spam": { + "archived": { "type": "boolean", - "description": "Whether the receiving server judged it spam." - }, - "received_email_id": { - "type": [ - "string", - "null" - ], - "description": "The received email of an inbound message." - }, - "email_id": { - "type": [ - "string", - "null" - ], - "description": "The email of an outbound message: its delivery events and webhooks name it." + "description": "Archived types are hidden from the portal and can't be subscribed to; messages of the type are still delivered to endpoints that subscribed before." }, "created_at": { "type": "string", - "format": "date-time", - "description": "When it was received or sent." - }, - "reply_to": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Addresses in the `Reply-To` header." - }, - "message_id": { - "type": [ - "string", - "null" - ], - "description": "An inbound message's `Message-ID` header (outbound ones get theirs at sending)." - }, - "in_reply_to": { - "type": [ - "string", - "null" - ], - "description": "The message it answers, by `Message-ID`. `null`: none." - }, - "references": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The `Message-ID`s of the conversation so far." - }, - "text": { - "type": [ - "string", - "null" - ], - "description": "The plain text part. `null`: none." - }, - "extracted_text": { - "type": [ - "string", - "null" - ], - "description": "The message's own text: `text` without the quoted conversation and signature below it (a heuristic; when nothing is left, all of `text`). Made from `html` when there is no `text`." - }, - "html": { - "type": [ - "string", - "null" - ], - "description": "The sender's HTML: sanitize it before showing it to anyone." + "format": "date-time" }, - "truncated": { - "type": "boolean", - "description": "Whether `text` or `html` were shortened (2 MB each)." + "updated_at": { + "type": "string", + "format": "date-time" } }, "required": [ "object", - "id", - "inbox_id", - "thread_id", - "direction", - "status", - "from", - "to", - "cc", - "subject", - "preview", - "attachments", - "spam", - "received_email_id", - "email_id", + "name", + "description", + "schema", + "examples", + "archived", "created_at", - "reply_to", - "message_id", - "in_reply_to", - "references", - "text", - "extracted_text", - "html", - "truncated" + "updated_at" ], "examples": [ { - "object": "inbox_message", - "id": "imsg_01k7c8m4s1d3c9x0z4e6g7h8j9", - "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", - "thread_id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", - "direction": "inbound", - "status": "received", - "from": "Jane Doe ", - "to": [ - "support-agent@k3j9x2m7qp4l.inbound.coss.com" - ], - "cc": [], - "subject": "My order arrived damaged", - "preview": "Hi, the order arrived damaged. Can you send a new one?", - "attachments": [ + "object": "hook_event_type", + "name": "invoice.paid", + "description": "An invoice was paid in full.", + "schema": { + "type": "object", + "properties": { + "invoice_id": { + "type": "string" + }, + "amount": { + "type": "integer" + } + } + }, + "examples": [ { - "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", - "filename": "photo.jpg", - "content_type": "image/jpeg", - "content_id": null, - "disposition": "attachment", - "size": 48213 + "invoice_id": "in_1042", + "amount": 4200, + "currency": "usd" } ], - "spam": false, - "received_email_id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", - "email_id": null, - "created_at": "2026-10-10T12:00:00.000Z", - "reply_to": [], - "message_id": "", - "in_reply_to": null, - "references": [], - "text": "Hi, the order arrived damaged. Can you send a new one?\n\nJane", - "extracted_text": "Hi, the order arrived damaged. Can you send a new one?\n\nJane", - "html": null, - "truncated": false - }, - { - "object": "inbox_message", - "id": "imsg_01k7c8m5t2e4d0y1a5f7h8j9k0", - "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", - "thread_id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", - "direction": "outbound", - "status": "queued", - "from": "\"Acme Support\" ", - "to": [ - "jane@example.com" - ], - "cc": [], - "subject": "Re: My order arrived damaged", - "preview": "Sorry about that, Jane! A new one is on its way.", - "attachments": [], - "spam": false, - "received_email_id": null, - "email_id": "email_01k7c8m5t2e4d0y1a5f7h8j9k0", - "created_at": "2026-10-10T12:01:00.000Z", - "reply_to": [], - "message_id": null, - "in_reply_to": "", - "references": [ - "" - ], - "text": "Sorry about that, Jane! A new one is on its way.", - "extracted_text": "Sorry about that, Jane! A new one is on its way.", - "html": null, - "truncated": false + "archived": false, + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z" } ] }, - "DeletedInboxThread": { + "CreateHookEventTypeRequest": { "type": "object", "properties": { - "object": { + "name": { "type": "string", - "enum": [ - "inbox_thread" + "pattern": "^[A-Za-z0-9_.-]{1,64}$", + "description": "The type's name, which messages send as `event_type`. Letters, digits and `. _ -`.", + "examples": [ + "invoice.paid" ] }, - "id": { - "type": "string" + "description": { + "type": "string", + "maxLength": 256, + "description": "What this event means, shown to your users." }, - "deleted": { - "type": "boolean", - "enum": [ - true - ] + "schema": { + "type": "object", + "additionalProperties": {}, + "description": "A JSON Schema of the payload, shown to your users (not enforced)." + }, + "examples": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} + }, + "maxItems": 5, + "description": "Up to 5 example payloads; the first is what a test send delivers." } }, "required": [ - "object", - "id", - "deleted" + "name" ], + "additionalProperties": false, "examples": [ { - "object": "inbox_thread", - "id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", - "deleted": true + "name": "invoice.paid", + "description": "An invoice was paid in full.", + "examples": [ + { + "invoice_id": "in_1042", + "amount": 4200, + "currency": "usd" + } + ] } ] }, - "InboxMessageList": { + "HookEventTypeList": { "type": "object", "properties": { "object": { @@ -32868,7 +32435,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/InboxMessageSummary" + "$ref": "#/components/schemas/HookEventType" } }, "has_more": { @@ -32885,1278 +32452,1036 @@ "object": "list", "data": [ { - "object": "inbox_message", - "id": "imsg_01k7c8m4s1d3c9x0z4e6g7h8j9", - "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", - "thread_id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", - "direction": "inbound", - "status": "received", - "from": "Jane Doe ", - "to": [ - "support-agent@k3j9x2m7qp4l.inbound.coss.com" - ], - "cc": [], - "subject": "My order arrived damaged", - "preview": "Hi, the order arrived damaged. Can you send a new one?", - "attachments": [ + "object": "hook_event_type", + "name": "invoice.paid", + "description": "An invoice was paid in full.", + "schema": { + "type": "object", + "properties": { + "invoice_id": { + "type": "string" + }, + "amount": { + "type": "integer" + } + } + }, + "examples": [ { - "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", - "filename": "photo.jpg", - "content_type": "image/jpeg", - "content_id": null, - "disposition": "attachment", - "size": 48213 + "invoice_id": "in_1042", + "amount": 4200, + "currency": "usd" } ], - "spam": false, - "received_email_id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", - "email_id": null, - "created_at": "2026-10-10T12:00:00.000Z" + "archived": false, + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z" } ], "has_more": false } ] }, - "InboxMessageSummary": { + "UpdateHookEventTypeRequest": { "type": "object", "properties": { - "object": { - "type": "string", - "enum": [ - "inbox_message" - ] - }, - "id": { - "type": "string" - }, - "inbox_id": { - "type": "string" - }, - "thread_id": { - "type": "string" - }, - "direction": { - "type": "string", - "enum": [ - "inbound", - "outbound" - ], - "description": "`inbound`: received by the inbox. `outbound`: sent from it." - }, - "status": { - "type": "string", - "enum": [ - "received", - "queued", - "sent", - "failed", - "scheduled", - "canceled" - ], - "description": "`received` for inbound messages; an outbound message's email status (`queued`, `sent`, `failed`, …): its email has each recipient's delivery." - }, - "from": { - "type": [ - "string", - "null" - ], - "description": "The `From` header." - }, - "to": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Addresses in the `To` header." - }, - "cc": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Addresses in the `Cc` header." - }, - "subject": { + "description": { "type": [ "string", "null" ], - "description": "The subject. `null`: none." + "maxLength": 256, + "description": "What this event means, shown to your users. `null` removes it." }, - "preview": { + "schema": { "type": [ - "string", + "object", "null" ], - "description": "The start of the message's own text (without the quoted conversation)." + "additionalProperties": {}, + "description": "A JSON Schema of the payload, shown to your users (not enforced)." }, - "attachments": { + "examples": { "type": "array", "items": { - "$ref": "#/components/schemas/ReceivedEmailAttachment" + "type": "object", + "additionalProperties": {} }, - "description": "An inbound message's attachments (download them from the message)." + "maxItems": 5, + "description": "Up to 5 example payloads; the first is what a test send delivers." }, - "spam": { + "archived": { "type": "boolean", - "description": "Whether the receiving server judged it spam." + "description": "`false` restores an archived type." + } + }, + "additionalProperties": false, + "examples": [ + { + "archived": false + } + ] + }, + "HookApplication": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "hook_application" + ] }, - "received_email_id": { - "type": [ - "string", - "null" - ], - "description": "The received email of an inbound message." + "id": { + "type": "string", + "examples": [ + "happ_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "email_id": { + "uid": { "type": [ "string", "null" ], - "description": "The email of an outbound message: its delivery events and webhooks name it." + "description": "Your own id for it (e.g. your customer's id), unique in the environment. Every path that takes an application accepts its `id` or its `uid`." + }, + "name": { + "type": "string", + "description": "Shown to the application's users in the portal." + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Your own key-value pairs." }, "created_at": { "type": "string", - "format": "date-time", - "description": "When it was received or sent." + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" } }, "required": [ "object", "id", - "inbox_id", - "thread_id", - "direction", - "status", - "from", - "to", - "cc", - "subject", - "preview", - "attachments", - "spam", - "received_email_id", - "email_id", - "created_at" + "uid", + "name", + "metadata", + "created_at", + "updated_at" ], "examples": [ { - "object": "inbox_message", - "id": "imsg_01k7c8m4s1d3c9x0z4e6g7h8j9", - "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", - "thread_id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", - "direction": "inbound", - "status": "received", - "from": "Jane Doe ", - "to": [ - "support-agent@k3j9x2m7qp4l.inbound.coss.com" - ], - "cc": [], - "subject": "My order arrived damaged", - "preview": "Hi, the order arrived damaged. Can you send a new one?", - "attachments": [ - { - "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", - "filename": "photo.jpg", - "content_type": "image/jpeg", - "content_id": null, - "disposition": "attachment", - "size": 48213 - } - ], - "spam": false, - "received_email_id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", - "email_id": null, - "created_at": "2026-10-10T12:00:00.000Z" + "object": "hook_application", + "id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", + "uid": "customer_8812", + "name": "Acme Corp", + "metadata": { + "plan": "pro" + }, + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z" } ] }, - "SendInboxMessageRequest": { + "CreateHookApplicationRequest": { "type": "object", "properties": { - "to": { - "anyOf": [ - { - "type": "string", - "description": "An email address, optionally with a display name: \"Acme \"." - }, - { - "type": "array", - "items": { - "type": "string", - "description": "An email address, optionally with a display name: \"Acme \"." - }, - "minItems": 1, - "maxItems": 50 - } - ], - "description": "Who it goes to." - }, - "cc": { - "anyOf": [ - { - "type": "string", - "description": "An email address, optionally with a display name: \"Acme \"." - }, - { - "type": "array", - "items": { - "type": "string", - "description": "An email address, optionally with a display name: \"Acme \"." - }, - "minItems": 0, - "maxItems": 50 - } - ], - "description": "Who it's copied to." - }, - "bcc": { - "anyOf": [ - { - "type": "string", - "description": "An email address, optionally with a display name: \"Acme \"." - }, - { - "type": "array", - "items": { - "type": "string", - "description": "An email address, optionally with a display name: \"Acme \"." - }, - "minItems": 0, - "maxItems": 50 - } - ], - "description": "Who it's blind copied to." - }, - "subject": { + "name": { "type": "string", "minLength": 1, - "maxLength": 998, - "description": "The new thread's subject." - }, - "text": { - "type": "string", - "description": "Plain-text body. Without it, an `html` email goes out with a text part generated from the HTML." + "maxLength": 256, + "description": "Shown to the application's users in the portal.", + "examples": [ + "Acme Corp" + ] }, - "html": { + "uid": { "type": "string", - "description": "HTML body. Provide `html`, `text`, or both." - }, - "attachments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SendEmailAttachment" - }, - "maxItems": 20, - "description": "Up to 20 files, at most 25 MB combined." + "pattern": "^[A-Za-z0-9_.:@|-]{1,256}$", + "description": "Your own id for it, to address it by in every path instead of its `id`.", + "examples": [ + "customer_8812" + ] }, - "tags": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The tag's name, unique on the email." - }, - "value": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The tag's value." - } - }, - "required": [ - "name", - "value" - ], - "additionalProperties": false + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 500 }, - "maxItems": 50, - "description": "Your tags on the email." + "description": "Your own key-value pairs (at most 16; keys up to 40 characters, values up to 500).", + "examples": [ + { + "plan": "pro" + } + ] } }, "required": [ - "to", - "subject" + "name" ], "additionalProperties": false, - "description": "Starts a thread: the message is sent from the inbox's address.", "examples": [ { - "to": "jane@example.com", - "subject": "Your order", - "text": "Hi Jane, your order has shipped." + "name": "Acme Corp", + "uid": "customer_8812", + "metadata": { + "plan": "pro" + } } ] }, - "ReplyInboxMessageRequest": { + "HookApplicationList": { "type": "object", "properties": { - "reply_all": { - "type": "boolean", - "description": "Also reply to everyone else on the message (its `to` and `cc`, but the inbox). Default `false`: only the sender (its `Reply-To`, when it has one)." + "object": { + "type": "string", + "enum": [ + "list" + ] }, - "cc": { - "anyOf": [ - { - "type": "string", - "description": "An email address, optionally with a display name: \"Acme \"." - }, - { - "type": "array", - "items": { - "type": "string", - "description": "An email address, optionally with a display name: \"Acme \"." - }, - "minItems": 0, - "maxItems": 50 - } - ], - "description": "Who it's copied to." + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HookApplication" + } }, - "bcc": { - "anyOf": [ - { - "type": "string", - "description": "An email address, optionally with a display name: \"Acme \"." - }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ { - "type": "array", - "items": { - "type": "string", - "description": "An email address, optionally with a display name: \"Acme \"." + "object": "hook_application", + "id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", + "uid": "customer_8812", + "name": "Acme Corp", + "metadata": { + "plan": "pro" }, - "minItems": 0, - "maxItems": 50 + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z" } ], - "description": "Who it's blind copied to." - }, - "text": { - "type": "string", - "description": "Plain-text body. Without it, an `html` email goes out with a text part generated from the HTML." - }, - "html": { + "has_more": false + } + ] + }, + "UpdateHookApplicationRequest": { + "type": "object", + "properties": { + "name": { "type": "string", - "description": "HTML body. Provide `html`, `text`, or both." + "minLength": 1, + "maxLength": 256, + "description": "Shown to the application's users in the portal." }, - "attachments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SendEmailAttachment" - }, - "maxItems": 20, - "description": "Up to 20 files, at most 25 MB combined." + "uid": { + "type": [ + "string", + "null" + ], + "pattern": "^[A-Za-z0-9_.:@|-]{1,256}$", + "description": "`null` removes it." }, - "tags": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The tag's name, unique on the email." - }, - "value": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The tag's value." - } - }, - "required": [ - "name", - "value" - ], - "additionalProperties": false + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 500 }, - "maxItems": 50, - "description": "Your tags on the email." + "description": "Replaces the metadata.", + "examples": [ + { + "plan": "pro" + } + ] } }, "additionalProperties": false, - "description": "Replies in the message's thread, from the inbox's address, with `Re:` and the headers mail clients thread by (`In-Reply-To`, `References`).", "examples": [ { - "text": "Sorry about that, Jane! A new one is on its way." + "name": "Acme Corporation" } ] }, - "RiskAssessment": { + "DeletedHookApplication": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "risk_assessment" + "hook_application" + ] + }, + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "object", + "id", + "deleted" + ], + "examples": [ + { + "object": "hook_application", + "id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", + "deleted": true + } + ] + }, + "HookEndpointWithSecret": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "hook_endpoint" ] }, "id": { "type": "string", "examples": [ - "rsk_01k7c8m4q2f3e9v6t5r1n0p8yz" + "hep_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, - "event": { - "$ref": "#/components/schemas/RiskEvent" + "application_id": { + "type": "string", + "description": "The id of the application it belongs to." }, - "event_name": { + "url": { + "type": "string", + "description": "Where deliveries are sent." + }, + "description": { "type": [ "string", "null" ], - "description": "Your name for a `custom` event. `null`: none." - }, - "decision": { - "$ref": "#/components/schemas/RiskDecision" - }, - "score": { - "type": "number", - "minimum": 0, - "maximum": 1, - "description": "0-1: how risky the signals are together. Lists can decide regardless of the score." + "description": "Your note about it. `null`: none." }, - "reasons": { + "filter_types": { "type": "array", "items": { - "$ref": "#/components/schemas/RiskReason" + "type": "string" }, - "description": "Why, most significant first." + "description": "Only these event types are delivered. Empty: every type." }, - "signals": { - "type": "object", - "properties": { - "email": { - "anyOf": [ - { - "$ref": "#/components/schemas/RiskEmailSignals" - }, - { - "type": "null" - } - ], - "description": "What the address says. `null`: no email was given." - }, - "ip": { - "anyOf": [ - { - "$ref": "#/components/schemas/RiskIpSignals" - }, - { - "type": "null" - } - ], - "description": "What the IP address says. `null`: none was given." - }, - "velocity": { - "anyOf": [ - { - "$ref": "#/components/schemas/RiskVelocitySignals" - }, - { - "type": "null" - } - ], - "description": "`null` when velocity was unavailable (see `degraded`)." - }, - "captcha": { - "anyOf": [ - { - "$ref": "#/components/schemas/RiskCaptchaSignals" - }, - { - "type": "null" - } - ], - "description": "How the captcha went. `null`: no captcha applied." - }, - "login": { - "anyOf": [ - { - "$ref": "#/components/schemas/RiskLoginSignals" - }, - { - "type": "null" - } - ], - "description": "Whether a login is new for this user. `null`: not a login of a user with history." - }, - "content": { - "anyOf": [ - { - "$ref": "#/components/schemas/RiskContentSignals" - }, - { - "type": "null" - } - ], - "description": "`null` when no content was given or it couldn't be classified." - }, - "checkout": { - "anyOf": [ - { - "$ref": "#/components/schemas/RiskCheckoutSignals" - }, - { - "type": "null" - } - ], - "description": "What you sent in `checkout`. `null`: not given." - }, - "device": { - "anyOf": [ - { - "$ref": "#/components/schemas/RiskDeviceSignals" - }, - { - "type": "null" - } - ], - "description": "What the device token said. `null`: none was given." - } - }, - "required": [ - "email", - "ip", - "velocity", - "captcha", - "login", - "content", - "checkout", - "device" + "rate_limit_per_second": { + "type": [ + "integer", + "null" ], - "description": "What the assessment saw, by group. A group is `null` when it had nothing to go on." + "description": "At most this many requests per second to this endpoint. `null`: no limit of its own." }, - "user": { - "type": "object", - "properties": { - "id": { - "type": [ - "string", - "null" - ], - "description": "Your user id. `null`: none given." - }, - "email": { - "type": [ - "string", - "null" - ], - "description": "`null` once removed (90 days after the assessment) or when none was given." - } - }, - "required": [ - "id", - "email" - ], - "description": "Who it's about." + "enabled": { + "type": "boolean", + "description": "Whether new messages are delivered to it." }, - "ip": { + "disabled_reason": { "type": [ "string", "null" ], - "description": "`null` once removed (90 days after)." + "enum": [ + "manual", + "failing", + null + ], + "description": "`manual`: disabled by you or its user. `failing`: its deliveries failed every attempt for five days, so coss.com disabled it. `null` while enabled." }, - "degraded": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "email_dns", - "domain_age", - "ip_data", - "velocity", - "captcha", - "content", - "ip_privacy", - "usage_limit" - ] - }, - "description": "Signal groups that were unavailable for this assessment (it was made without them). `usage_limit` means the plan's included assessments are used up, or a billing hold or budget applies: the assessment was free and made without velocity, memory of earlier events, content or address-level IP data." + "disabled_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When it was disabled. `null`: it's enabled." }, - "outcome": { - "anyOf": [ - { - "$ref": "#/components/schemas/RiskOutcome" - }, - { - "type": "null" - } + "failing_since": { + "type": [ + "string", + "null" ], - "description": "The latest outcome you reported with feedback." + "format": "date-time", + "description": "When its current run of failed deliveries began; cleared by the next success. `null` while healthy." }, - "review_status": { - "anyOf": [ - { - "type": "string", - "enum": [ - "open", - "resolved" - ] - }, - { - "type": "null" - } + "previous_secret_expires_at": { + "type": [ + "string", + "null" ], - "description": "`open` for `review` decisions until you send feedback, then `resolved`." + "format": "date-time", + "description": "After a secret roll, requests are also signed with the previous secret until then." + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Your own key-value pairs." }, "created_at": { "type": "string", "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "secret": { + "type": "string", + "description": "The endpoint's `whsec_…` signing secret (Standard Webhooks).", + "examples": [ + "whsec_6Jk…" + ] } }, "required": [ "object", "id", - "event", - "event_name", - "decision", - "score", - "reasons", - "signals", - "user", - "ip", - "degraded", - "outcome", - "review_status", - "created_at" + "application_id", + "url", + "description", + "filter_types", + "rate_limit_per_second", + "enabled", + "disabled_reason", + "disabled_at", + "failing_since", + "previous_secret_expires_at", + "metadata", + "created_at", + "updated_at", + "secret" ], "examples": [ { - "object": "risk_assessment", - "id": "rsk_01k7c8m4q2f3e9v6t5r1n0p8yz", - "event": "signup", - "event_name": null, - "decision": "review", - "score": 0.62, - "reasons": [ - { - "code": "email.typo", - "message": "The domain looks like a typo of gmail.com.", - "suggestion": "gmail.com" - }, - { - "code": "velocity.signups_per_ip_1h", - "message": "7 sign-ups from this IP address in the last hour.", - "value": 7, - "threshold": 5, - "window": "1h" - } + "object": "hook_endpoint", + "id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", + "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", + "url": "https://example.com/webhooks", + "description": "Production", + "filter_types": [ + "invoice.paid" ], - "signals": { - "email": { - "domain": "gmail.con", - "valid": true, - "disposable": false, - "deliverability": "deliverable", - "role_account": false, - "free_provider": false, - "suggestion": "gmail.com", - "domain_age_days": 9 - }, - "ip": { - "version": 4, - "private": false, - "tor": false, - "hosting": false, - "hosting_provider": null, - "hosting_kind": null, - "asn": 64500, - "as_name": "Example Broadband", - "country": "GB", - "privacy": null - }, - "velocity": { - "ip_1h": 7, - "ip_24h": 9, - "subnet_1h": 7, - "mailbox_24h": 1 - }, - "captcha": { - "provided": true, - "passed": true - }, - "login": null, - "content": null, - "checkout": null, - "device": null - }, - "user": { - "id": null, - "email": "ada@gmail.con" - }, - "ip": "203.0.113.7", - "degraded": [], - "outcome": null, - "review_status": "open", - "created_at": "2026-01-15T09:30:00.000Z" + "rate_limit_per_second": null, + "enabled": true, + "disabled_reason": null, + "disabled_at": null, + "failing_since": null, + "previous_secret_expires_at": null, + "metadata": {}, + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z", + "secret": "whsec_6Jk…" } ] }, - "RiskEmailSignals": { + "CreateHookEndpointRequest": { "type": "object", "properties": { - "domain": { - "type": [ - "string", - "null" - ], - "description": "`null` once the address is removed (90 days after the assessment)." + "url": { + "type": "string", + "maxLength": 2048, + "examples": [ + "https://example.com/webhooks" + ] }, - "valid": { - "type": "boolean", - "description": "Whether it's syntactically valid." + "description": { + "type": "string", + "maxLength": 256, + "description": "Your note about it." }, - "disposable": { - "type": "boolean", - "description": "Whether its domain is a known burner service." + "filter_types": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9_.-]{1,64}$" + }, + "maxItems": 250, + "description": "Event types delivered to this endpoint (existing, unarchived ones). An empty array means every type, including ones added later.", + "examples": [ + [ + "invoice.paid" + ] + ] }, - "deliverability": { + "secret": { "type": "string", - "enum": [ - "deliverable", - "undeliverable", - "unknown" - ], - "description": "From DNS (MX, A, AAAA). `unknown` when the lookup failed or timed out." - }, - "role_account": { - "type": "boolean", - "description": "Whether it's a role mailbox, like `info@`." + "description": "A `whsec_` + base64 secret of 24-64 bytes to sign with, e.g. when migrating from another provider. Generated when omitted." }, - "free_provider": { - "type": "boolean", - "description": "Whether its domain is a free mailbox provider." + "rate_limit_per_second": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "At most this many requests per second to this endpoint." }, - "suggestion": { - "type": [ - "string", - "null" - ], - "description": "`email.typo`: the likely intended domain. `null` otherwise." + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 500 + }, + "description": "Your own key-value pairs (at most 16; keys up to 40 characters, values up to 500).", + "examples": [ + { + "plan": "pro" + } + ] }, - "domain_age_days": { - "type": [ - "integer", - "null" - ], - "description": "Days since the domain was registered (RDAP); `null` when unknown or not looked up (free providers, disposable domains)." + "enabled": { + "type": "boolean", + "description": "Default `true`. `false` creates it disabled, with `disabled_reason: manual`." } }, "required": [ - "domain", - "valid", - "disposable", - "deliverability", - "role_account", - "free_provider", - "suggestion", - "domain_age_days" + "url" ], - "description": "What the address says. `null` on an assessment with no email." + "additionalProperties": false, + "examples": [ + { + "url": "https://example.com/webhooks", + "filter_types": [ + "invoice.paid" + ] + } + ] }, - "RiskIpSignals": { + "HookEndpointList": { "type": "object", "properties": { - "version": { - "anyOf": [ - { - "type": "number", - "enum": [ - 4 - ] - }, + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HookEndpoint" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ { - "type": "number", - "enum": [ - 6 - ] + "object": "hook_endpoint", + "id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", + "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", + "url": "https://example.com/webhooks", + "description": "Production", + "filter_types": [ + "invoice.paid" + ], + "rate_limit_per_second": null, + "enabled": true, + "disabled_reason": null, + "disabled_at": null, + "failing_since": null, + "previous_secret_expires_at": null, + "metadata": {}, + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z" } ], - "description": "IPv4 or IPv6." + "has_more": false + } + ] + }, + "HookEndpoint": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "hook_endpoint" + ] }, - "private": { - "type": "boolean", - "description": "Whether it's a private or reserved address." + "id": { + "type": "string", + "examples": [ + "hep_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "tor": { - "type": "boolean", - "description": "Whether it's a Tor exit." + "application_id": { + "type": "string", + "description": "The id of the application it belongs to." }, - "hosting": { - "type": "boolean", - "description": "Whether it's in a cloud, hosting or VPN network." + "url": { + "type": "string", + "description": "Where deliveries are sent." }, - "hosting_provider": { + "description": { "type": [ "string", "null" ], - "description": "That network's operator. `null` when it isn't one." + "description": "Your note about it. `null`: none." }, - "hosting_kind": { + "filter_types": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Only these event types are delivered. Empty: every type." + }, + "rate_limit_per_second": { + "type": [ + "integer", + "null" + ], + "description": "At most this many requests per second to this endpoint. `null`: no limit of its own." + }, + "enabled": { + "type": "boolean", + "description": "Whether new messages are delivered to it." + }, + "disabled_reason": { "type": [ "string", "null" ], "enum": [ - "cloud", - "hosting", - "vpn", + "manual", + "failing", null ], - "description": "`cloud`, `hosting` or `vpn`. `null` when it isn't one." + "description": "`manual`: disabled by you or its user. `failing`: its deliveries failed every attempt for five days, so coss.com disabled it. `null` while enabled." }, - "asn": { + "disabled_at": { "type": [ - "integer", + "string", "null" ], - "description": "The network's autonomous system number. `null` when unknown." + "format": "date-time", + "description": "When it was disabled. `null`: it's enabled." }, - "as_name": { + "failing_since": { "type": [ "string", "null" ], - "description": "The network's name. `null` when unknown." + "format": "date-time", + "description": "When its current run of failed deliveries began; cleared by the next success. `null` while healthy." }, - "country": { + "previous_secret_expires_at": { "type": [ "string", "null" ], - "description": "ISO 3166-1 alpha-2." + "format": "date-time", + "description": "After a secret roll, requests are also signed with the previous secret until then." }, - "privacy": { - "anyOf": [ - { - "$ref": "#/components/schemas/RiskIpPrivacy" - }, - { - "type": "null" - } - ], - "description": "From coss.com's IP privacy data provider (VPNs, proxies, relays). `null` when it isn't available or was not asked (private addresses)." + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Your own key-value pairs." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" } }, "required": [ - "version", - "private", - "tor", - "hosting", - "hosting_provider", - "hosting_kind", - "asn", - "as_name", - "country", - "privacy" + "object", + "id", + "application_id", + "url", + "description", + "filter_types", + "rate_limit_per_second", + "enabled", + "disabled_reason", + "disabled_at", + "failing_since", + "previous_secret_expires_at", + "metadata", + "created_at", + "updated_at" ], - "description": "What the address says. `null` on an assessment with no IP." + "examples": [ + { + "object": "hook_endpoint", + "id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", + "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", + "url": "https://example.com/webhooks", + "description": "Production", + "filter_types": [ + "invoice.paid" + ], + "rate_limit_per_second": null, + "enabled": true, + "disabled_reason": null, + "disabled_at": null, + "failing_since": null, + "previous_secret_expires_at": null, + "metadata": {}, + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z" + } + ] }, - "RiskIpPrivacy": { + "UpdateHookEndpointRequest": { "type": "object", "properties": { - "vpn": { - "type": "boolean", - "description": "Whether it's a VPN exit." + "url": { + "type": "string", + "maxLength": 2048, + "description": "Where deliveries are sent." }, - "proxy": { - "type": "boolean", - "description": "Whether it's a proxy." + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 256, + "description": "Your note about it. `null` removes it." }, - "relay": { - "type": "boolean", - "description": "Whether it's a privacy relay." + "filter_types": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9_.-]{1,64}$" + }, + "maxItems": 250, + "description": "Event types delivered to this endpoint (existing, unarchived ones). An empty array means every type, including ones added later.", + "examples": [ + [ + "invoice.paid" + ] + ] }, - "service": { + "rate_limit_per_second": { "type": [ - "string", + "integer", "null" ], - "description": "The VPN or proxy service." + "minimum": 1, + "maximum": 1000, + "description": "At most this many requests per second to this endpoint." + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 500 + }, + "description": "Your own key-value pairs (at most 16; keys up to 40 characters, values up to 500).", + "examples": [ + { + "plan": "pro" + } + ] + }, + "enabled": { + "type": "boolean", + "description": "`false` disables it with `disabled_reason: manual`; `true` enables it again and clears the reason." } }, - "required": [ - "vpn", - "proxy", - "relay", - "service" + "additionalProperties": false, + "examples": [ + { + "enabled": false + } ] }, - "RiskVelocitySignals": { - "type": "object", - "additionalProperties": { - "type": "integer" - }, - "description": "Events of the same kind in this environment, this one included, keyed `_` (e.g. `ip_1h`, `mailbox_24h`)." - }, - "RiskCaptchaSignals": { + "DeletedHookEndpoint": { "type": "object", "properties": { - "provided": { - "type": "boolean", - "description": "Whether the request carried a token." + "object": { + "type": "string", + "enum": [ + "hook_endpoint" + ] }, - "passed": { - "type": [ - "boolean", - "null" - ], - "description": "`null` when it couldn't be checked (no captcha configured, or the provider was unreachable)." + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "enum": [ + true + ] } }, "required": [ - "provided", - "passed" + "object", + "id", + "deleted" ], - "description": "How the captcha went. `null` when no captcha applies." + "examples": [ + { + "object": "hook_endpoint", + "id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", + "deleted": true + } + ] }, - "RiskLoginSignals": { + "HookEndpointSecret": { "type": "object", "properties": { - "new_country": { - "type": "boolean", - "description": "Whether this user never logged in from this country in this environment." + "object": { + "type": "string", + "enum": [ + "hook_endpoint_secret" + ] }, - "new_network": { - "type": "boolean", - "description": "Whether this user never logged in from this network (ASN) in this environment." + "secret": { + "type": "string", + "description": "The endpoint's `whsec_…` signing secret (Standard Webhooks)." } }, "required": [ - "new_country", - "new_network" + "object", + "secret" ], - "description": "Logins of a user with history: whether the country or network is new for them." + "examples": [ + { + "object": "hook_endpoint_secret", + "secret": "whsec_6Jk…" + } + ] }, - "RiskContentSignals": { + "RollHookEndpointSecretRequest": { "type": "object", "properties": { - "spam": { - "type": "number", - "minimum": 0, - "maximum": 1, - "description": "The model's probability (0-1) that it's spam." - }, - "phishing": { - "type": "number", - "minimum": 0, - "maximum": 1, - "description": "The model's probability (0-1) that it's phishing." - }, - "malware": { - "type": "number", - "minimum": 0, - "maximum": 1, - "description": "The model's probability (0-1) that it's malware." - }, - "fraud": { - "type": "number", + "previous_secret_expires_in": { + "type": "integer", "minimum": 0, - "maximum": 1, - "description": "The model's probability (0-1) that it's fraud." + "maximum": 604800, + "description": "Seconds the current secret keeps signing next to the new one. Default 86400 (a day), at most 604800 (a week); `0` stops it immediately." } }, - "required": [ - "spam", - "phishing", - "malware", - "fraud" - ], - "description": "Per category, the model's probability (0-1) that the content belongs to it. Scores are not calibrated: pick thresholds by looking at your own traffic." + "additionalProperties": false, + "examples": [ + { + "previous_secret_expires_in": 3600 + } + ] }, - "RiskCheckoutSignals": { + "HookEndpointRecovery": { "type": "object", "properties": { - "amount": { - "type": "integer", - "description": "The amount you sent, in the currency's smallest unit." + "object": { + "type": "string", + "enum": [ + "hook_endpoint_recovery" + ] }, - "currency": { + "id": { "type": "string", - "description": "The currency you sent." + "examples": [ + "hrec_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "card_country": { - "type": [ - "string", - "null" - ], - "description": "The card's country. `null` when not given." + "endpoint_id": { + "type": "string", + "description": "The id of the endpoint being recovered." }, - "card_funding": { - "type": [ - "string", - "null" - ], + "since": { + "type": "string", + "format": "date-time", + "description": "Deliveries of messages sent from this time on are requeued." + }, + "until": { + "type": "string", + "format": "date-time", + "description": "Up to this time: when the recovery was asked for." + }, + "status": { + "type": "string", "enum": [ - "credit", - "debit", - "prepaid", - "unknown", - null + "pending", + "completed" ], - "description": "The card's funding type. `null` when not given." + "description": "`pending` while coss.com is still requeueing deliveries (in the background)." }, - "billing_country": { - "type": [ - "string", - "null" - ], - "description": "The billing country. `null` when not given." + "requeued": { + "type": "integer", + "description": "Deliveries requeued so far." }, - "shipping_country": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "completed_at": { "type": [ "string", "null" ], - "description": "The shipping country. `null` when not given." + "format": "date-time", + "description": "When requeueing finished. `null`: still `pending`." } }, "required": [ - "amount", - "currency", - "card_country", - "card_funding", - "billing_country", - "shipping_country" + "object", + "id", + "endpoint_id", + "since", + "until", + "status", + "requeued", + "created_at", + "completed_at" ], - "description": "What you sent in `checkout` (never the card)." + "examples": [ + { + "object": "hook_endpoint_recovery", + "id": "hrec_01k7c8m4q2f3e9v6t5r1n0p8yz", + "endpoint_id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", + "since": "2026-01-15T00:00:00.000Z", + "until": "2026-01-15T09:30:00.000Z", + "status": "pending", + "requeued": 0, + "created_at": "2026-01-15T09:30:00.000Z", + "completed_at": null + } + ] }, - "RiskDeviceSignals": { + "RecoverHookEndpointRequest": { "type": "object", "properties": { - "valid": { - "type": "boolean", - "description": "`false`: the token was malformed or too old." - }, - "automation": { - "type": "boolean", - "description": "WebDriver control or a headless browser." - }, - "timezone": { - "type": [ - "string", - "null" - ], - "description": "IANA time zone (`Europe/London`)." - }, - "language": { - "type": [ - "string", - "null" - ], - "description": "The browser's first language." - }, - "platform": { - "type": [ - "string", - "null" - ], - "description": "The browser's reported platform. `null` when it reported none." - }, - "screen": { - "type": [ - "string", - "null" - ], - "description": "`x` in CSS pixels." - }, - "touch": { - "type": [ - "boolean", - "null" - ], - "description": "Whether the device has a touch screen. `null` when it didn't say." + "since": { + "type": "string", + "format": "date-time", + "description": "Requeue deliveries that failed, of messages sent from this time until now.", + "examples": [ + "2026-01-15T00:00:00.000Z" + ] } }, "required": [ - "valid", - "automation", - "timezone", - "language", - "platform", - "screen", - "touch" + "since" ], - "description": "What the browser reported through `coss/device` (its device id is only kept as a keyed hash)." + "additionalProperties": false, + "examples": [ + { + "since": "2026-01-15T00:00:00.000Z" + } + ] }, - "CreateRiskAssessmentRequest": { + "HookMessage": { "type": "object", "properties": { - "event": { - "$ref": "#/components/schemas/RiskEvent" - }, - "event_name": { + "object": { "type": "string", - "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$", - "description": "Required for `custom` events (e.g. `invite_sent`), not allowed otherwise." - }, - "user": { - "type": "object", - "properties": { - "id": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Your id for the user, when it exists (logins always; sign-ups once the account is created)." - }, - "email": { - "type": "string", - "minLength": 1, - "maxLength": 320, - "description": "The email address the user gave. Checked as given, even if invalid." - } - }, - "additionalProperties": false - }, - "ip": { - "anyOf": [ - { - "type": "string", - "format": "ip" - }, - { - "type": "string", - "format": "ip" - } - ], - "description": "The client's IP address (not your server's or proxy's)." + "enum": [ + "hook_message" + ] }, - "user_agent": { + "id": { "type": "string", - "maxLength": 1024, - "description": "The client's user agent." + "description": "Also the `webhook-id` header of every request delivering it.", + "examples": [ + "hmsg_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "captcha": { - "type": "object", - "properties": { - "token": { - "type": "string", - "minLength": 1, - "maxLength": 4096, - "description": "The token your captcha widget (Turnstile or hCaptcha, set in the environment's risk settings) gave the browser. Verified once: don't reuse it." - } - }, - "required": [ - "token" - ], - "additionalProperties": false + "application_id": { + "type": "string", + "description": "The id of the application it was sent to." }, - "checkout": { - "$ref": "#/components/schemas/RiskCheckoutRequest" + "event_type": { + "type": "string", + "description": "The event type it was sent as." }, - "device": { + "payload": { "type": "object", - "properties": { - "token": { - "type": "string", - "minLength": 1, - "maxLength": 4096, - "description": "What `collectDevice()` from `coss/device` returned in the user's browser, passed through your server." - } - }, - "required": [ - "token" - ], - "additionalProperties": false, - "description": "The browser the event came from: recognizes it across accounts (`duplicate.device`, device velocity, fraud memory) and spots automation." + "additionalProperties": {}, + "description": "The JSON body delivered to each endpoint." }, - "content": { - "type": "object", - "properties": { - "text": { - "type": "string", - "minLength": 1, - "maxLength": 50000, - "description": "Text the user wrote (a profile bio, a message, a listing). Classified by an AI model with zero data retention and never stored: only its scores are." - } - }, - "required": [ - "text" - ], - "additionalProperties": false, - "description": "Content to classify with the event. Its scores go in `signals.content` and rules can test them; `content.*` reasons fire only at thresholds you set." + "created_at": { + "type": "string", + "format": "date-time" } }, "required": [ - "event" + "object", + "id", + "application_id", + "event_type", + "payload", + "created_at" ], - "additionalProperties": false, "examples": [ { - "event": "signup", - "user": { - "email": "ada@gmail.con" + "object": "hook_message", + "id": "hmsg_01k7c8m4q2f3e9v6t5r1n0p8yz", + "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", + "event_type": "invoice.paid", + "payload": { + "invoice_id": "in_1042", + "amount": 4200, + "currency": "usd" }, - "ip": "203.0.113.7", - "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 15_0) AppleWebKit/605.1.15" + "created_at": "2026-01-15T09:30:00.000Z" } ] }, - "RiskCheckoutRequest": { + "SendHookTestRequest": { "type": "object", "properties": { - "amount": { - "type": "integer", - "minimum": 0, - "maximum": 1000000000000, - "description": "The amount in the currency's minor unit (cents for USD: 1999 is $19.99)." - }, - "currency": { + "event_type": { "type": "string", - "pattern": "^[A-Z]{3}$", - "description": "ISO 4217, upper-case (`USD`)." + "pattern": "^[A-Za-z0-9_.-]{1,64}$", + "description": "The event type to send as.", + "examples": [ + "invoice.paid" + ] }, - "card": { + "payload": { "type": "object", - "properties": { - "fingerprint": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Your payment processor's fingerprint of the card number (Stripe: `card.fingerprint`). Counted and remembered as a keyed hash only; never send the card number." - }, - "country": { - "type": "string", - "pattern": "^[A-Z]{2}$", - "description": "Where the card was issued." - }, - "funding": { - "type": "string", - "enum": [ - "credit", - "debit", - "prepaid", - "unknown" - ] - } - }, - "additionalProperties": false, - "description": "What your payment processor tells you about the card." - }, - "billing_country": { - "type": "string", - "pattern": "^[A-Z]{2}$", - "description": "ISO 3166-1 alpha-2, upper-case (`GB`)." - }, - "shipping_country": { - "type": "string", - "pattern": "^[A-Z]{2}$", - "description": "ISO 3166-1 alpha-2, upper-case (`GB`)." + "additionalProperties": {}, + "description": "Defaults to the event type's first example." } }, "required": [ - "amount", - "currency" + "event_type" ], "additionalProperties": false, - "description": "Only with `checkout` events: what is being paid for, and with which card." + "examples": [ + { + "event_type": "invoice.paid" + } + ] }, - "RiskAssessmentList": { + "HookDeliveryList": { "type": "object", "properties": { "object": { @@ -34168,7 +33493,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/RiskAssessment" + "$ref": "#/components/schemas/HookDelivery" } }, "has_more": { @@ -34185,228 +33510,171 @@ "object": "list", "data": [ { - "object": "risk_assessment", - "id": "rsk_01k7c8m4q2f3e9v6t5r1n0p8yz", - "event": "signup", - "event_name": null, - "decision": "review", - "score": 0.62, - "reasons": [ - { - "code": "email.typo", - "message": "The domain looks like a typo of gmail.com.", - "suggestion": "gmail.com" - }, - { - "code": "velocity.signups_per_ip_1h", - "message": "7 sign-ups from this IP address in the last hour.", - "value": 7, - "threshold": 5, - "window": "1h" - } - ], - "signals": { - "email": { - "domain": "gmail.con", - "valid": true, - "disposable": false, - "deliverability": "deliverable", - "role_account": false, - "free_provider": false, - "suggestion": "gmail.com", - "domain_age_days": 9 - }, - "ip": { - "version": 4, - "private": false, - "tor": false, - "hosting": false, - "hosting_provider": null, - "hosting_kind": null, - "asn": 64500, - "as_name": "Example Broadband", - "country": "GB", - "privacy": null - }, - "velocity": { - "ip_1h": 7, - "ip_24h": 9, - "subnet_1h": 7, - "mailbox_24h": 1 - }, - "captcha": { - "provided": true, - "passed": true - }, - "login": null, - "content": null, - "checkout": null, - "device": null - }, - "user": { - "id": null, - "email": "ada@gmail.con" - }, - "ip": "203.0.113.7", - "degraded": [], - "outcome": null, - "review_status": "open", - "created_at": "2026-01-15T09:30:00.000Z" + "object": "hook_delivery", + "id": "hdel_01k7c8m4q2f3e9v6t5r1n0p8yz", + "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", + "message_id": "hmsg_01k7c8m4q2f3e9v6t5r1n0p8yz", + "endpoint_id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", + "status": "succeeded", + "attempt_count": 1, + "next_attempt_at": null, + "last_status_code": 200, + "last_error": null, + "created_at": "2026-01-15T09:30:00.000Z", + "finished_at": "2026-01-15T09:30:00.000Z" } ], "has_more": false } ] }, - "CreateRiskFeedbackRequest": { - "type": "object", - "properties": { - "outcome": { - "$ref": "#/components/schemas/RiskOutcome" - }, - "note": { - "type": "string", - "maxLength": 500, - "description": "For your team; never shared." - } - }, - "required": [ - "outcome" - ], - "additionalProperties": false, - "examples": [ - { - "outcome": "fraud" - } - ] - }, - "RiskListEntry": { + "HookDelivery": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "risk_list_entry" + "hook_delivery" ] }, "id": { - "type": "string" + "type": "string", + "examples": [ + "hdel_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "list": { - "$ref": "#/components/schemas/RiskList" + "application_id": { + "type": "string", + "description": "The id of the application." }, - "type": { - "$ref": "#/components/schemas/RiskListEntryType" + "message_id": { + "type": "string", + "description": "The id of the message being delivered." }, - "value": { + "endpoint_id": { "type": "string", - "description": "Normalized: emails as their mailbox, CIDR ranges canonical, ASNs as numbers." + "description": "The id of the endpoint it's delivered to." }, - "note": { + "status": { + "type": "string", + "enum": [ + "pending", + "succeeded", + "failed" + ], + "description": "`pending`: waiting for its next attempt. `succeeded`: the endpoint answered 2xx. `failed`: every attempt failed, or the endpoint was disabled or deleted." + }, + "attempt_count": { + "type": "integer", + "description": "How many attempts were made so far." + }, + "next_attempt_at": { "type": [ "string", "null" ], - "description": "Your note about it. `null`: none." + "format": "date-time", + "description": "When the next attempt runs. `null`: none is due, because it finished." }, - "expires_at": { + "last_status_code": { + "type": [ + "integer", + "null" + ], + "description": "The endpoint's HTTP status on the latest attempt. `null`: no response arrived, or no attempt yet." + }, + "last_error": { "type": [ "string", "null" ], - "format": "date-time", - "description": "When it stops matching. `null`: never." + "description": "Why the latest attempt failed. `null`: it didn't, or no attempt yet." }, "created_at": { "type": "string", "format": "date-time" + }, + "finished_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When it `succeeded` or `failed`. `null`: still `pending`." } }, "required": [ "object", "id", - "list", - "type", - "value", - "note", - "expires_at", - "created_at" + "application_id", + "message_id", + "endpoint_id", + "status", + "attempt_count", + "next_attempt_at", + "last_status_code", + "last_error", + "created_at", + "finished_at" ], "examples": [ { - "object": "risk_list_entry", - "id": "rskle_01k7c8m4q2f3e9v6t5r1n0p8yz", - "list": "block", - "type": "domain", - "value": "evil.example", - "note": "Card testing", - "expires_at": null, - "created_at": "2026-01-15T09:30:00.000Z" + "object": "hook_delivery", + "id": "hdel_01k7c8m4q2f3e9v6t5r1n0p8yz", + "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", + "message_id": "hmsg_01k7c8m4q2f3e9v6t5r1n0p8yz", + "endpoint_id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", + "status": "succeeded", + "attempt_count": 1, + "next_attempt_at": null, + "last_status_code": 200, + "last_error": null, + "created_at": "2026-01-15T09:30:00.000Z", + "finished_at": "2026-01-15T09:30:00.000Z" } ] }, - "RiskList": { - "type": "string", - "enum": [ - "allow", - "block" - ], - "description": "Which list: `allow` or `block`." - }, - "RiskListEntryType": { - "type": "string", - "enum": [ - "email", - "domain", - "ip", - "asn", - "country", - "user" - ], - "description": "`email` (matched by mailbox, dots and `+tags` folded), `domain` (subdomains included), `ip` (an address or CIDR range), `asn`, `country` (ISO 3166-1 alpha-2), `user` (your user id)." - }, - "CreateRiskListEntryRequest": { + "SendHookMessageRequest": { "type": "object", "properties": { - "list": { - "$ref": "#/components/schemas/RiskList" - }, - "type": { - "$ref": "#/components/schemas/RiskListEntryType" - }, - "value": { - "type": "string", - "minLength": 1, - "maxLength": 320, - "description": "What to match: the address, domain, IP, CIDR range, ASN or other value of its `type`." - }, - "note": { + "event_type": { "type": "string", - "maxLength": 500, - "description": "Your note about it." + "pattern": "^[A-Za-z0-9_.-]{1,64}$", + "description": "What happened, e.g. `invoice.paid`. Endpoints that filter by type only get the types they chose. The type doesn't have to be registered.", + "examples": [ + "invoice.paid" + ] }, - "expires_at": { - "type": "string", - "format": "date-time", - "description": "The entry stops matching at this time (it is kept until deleted)." + "payload": { + "type": "object", + "additionalProperties": {}, + "description": "The JSON object delivered as the request body (at most 256 KB serialized). It is parsed and serialized again: send integers beyond 2^53 as strings.", + "examples": [ + { + "invoice_id": "in_1042", + "amount": 4200, + "currency": "usd" + } + ] } }, "required": [ - "list", - "type", - "value" + "event_type", + "payload" ], "additionalProperties": false, "examples": [ { - "list": "block", - "type": "domain", - "value": "evil.example", - "note": "Card testing" + "event_type": "invoice.paid", + "payload": { + "invoice_id": "in_1042", + "amount": 4200, + "currency": "usd" + } } ] }, - "RiskListEntryList": { + "HookMessageList": { "type": "object", "properties": { "object": { @@ -34418,7 +33686,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/RiskListEntry" + "$ref": "#/components/schemas/HookMessage" } }, "has_more": { @@ -34435,13 +33703,15 @@ "object": "list", "data": [ { - "object": "risk_list_entry", - "id": "rskle_01k7c8m4q2f3e9v6t5r1n0p8yz", - "list": "block", - "type": "domain", - "value": "evil.example", - "note": "Card testing", - "expires_at": null, + "object": "hook_message", + "id": "hmsg_01k7c8m4q2f3e9v6t5r1n0p8yz", + "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", + "event_type": "invoice.paid", + "payload": { + "invoice_id": "in_1042", + "amount": 4200, + "currency": "usd" + }, "created_at": "2026-01-15T09:30:00.000Z" } ], @@ -34449,674 +33719,4346 @@ } ] }, - "DeletedRiskListEntry": { + "HookAttemptList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "risk_list_entry" + "list" ] }, - "id": { - "type": "string" + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HookAttempt" + } }, - "deleted": { - "type": "boolean", + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ + { + "object": "hook_attempt", + "id": "hatt_01k7c8m4q2f3e9v6t5r1n0p8yz", + "delivery_id": "hdel_01k7c8m4q2f3e9v6t5r1n0p8yz", + "endpoint_id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", + "attempted_at": "2026-01-15T09:30:00.000Z", + "status_code": 200, + "duration_ms": 182, + "error": null, + "response_excerpt": "ok" + } + ], + "has_more": false + } + ] + }, + "HookAttempt": { + "type": "object", + "properties": { + "object": { + "type": "string", "enum": [ - true + "hook_attempt" + ] + }, + "id": { + "type": "string", + "examples": [ + "hatt_01k7c8m4q2f3e9v6t5r1n0p8yz" ] + }, + "delivery_id": { + "type": "string", + "description": "The id of the delivery this attempt belongs to." + }, + "endpoint_id": { + "type": "string", + "description": "The id of the endpoint it was sent to." + }, + "attempted_at": { + "type": "string", + "format": "date-time", + "description": "When the attempt ran." + }, + "status_code": { + "type": [ + "integer", + "null" + ], + "description": "The endpoint's HTTP status; `null` when no response arrived." + }, + "duration_ms": { + "type": "integer", + "description": "How long it took, in milliseconds." + }, + "error": { + "type": [ + "string", + "null" + ], + "description": "Why it failed. `null`: it didn't." + }, + "response_excerpt": { + "type": [ + "string", + "null" + ], + "description": "At most 1 KB of the response body, as text." } }, "required": [ "object", "id", - "deleted" + "delivery_id", + "endpoint_id", + "attempted_at", + "status_code", + "duration_ms", + "error", + "response_excerpt" + ], + "examples": [ + { + "object": "hook_attempt", + "id": "hatt_01k7c8m4q2f3e9v6t5r1n0p8yz", + "delivery_id": "hdel_01k7c8m4q2f3e9v6t5r1n0p8yz", + "endpoint_id": "hep_01k7c8m4q2f3e9v6t5r1n0p8yz", + "attempted_at": "2026-01-15T09:30:00.000Z", + "status_code": 200, + "duration_ms": 182, + "error": null, + "response_excerpt": "ok" + } ] }, - "ForgottenRiskUser": { + "HookPortalSession": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "risk_user" + "hook_portal_session" ] }, "id": { "type": "string", - "description": "Your user id." + "examples": [ + "hps_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "deleted": { + "application_id": { + "type": "string", + "description": "The id of the application this session manages." + }, + "url": { + "type": "string", + "description": "Open it or embed it in an iframe (from an allowed origin). The token is in the URL fragment, so it never reaches a server log: hand the URL only to that application's user." + }, + "token": { + "type": "string", + "description": "The secret part of `url`, shown only here." + }, + "read_only": { "type": "boolean", - "enum": [ - true - ] + "description": "Whether the user can only look." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "When the session stops working." } }, "required": [ "object", "id", - "deleted" + "application_id", + "url", + "token", + "read_only", + "expires_at" ], "examples": [ { - "object": "risk_user", - "id": "u_42", - "deleted": true + "object": "hook_portal_session", + "id": "hps_01k7c8m4q2f3e9v6t5r1n0p8yz", + "application_id": "happ_01k7c8m4q2f3e9v6t5r1n0p8yz", + "url": "https://portal.example/s/hps_01k7c8m4q2f3e9v6t5r1n0p8yz#cossp_…", + "token": "cossp_…", + "read_only": false, + "expires_at": "2026-01-15T10:30:00.000Z" } ] }, - "RiskSettings": { + "CreateHookPortalSessionRequest": { + "type": "object", + "properties": { + "expires_in": { + "type": "integer", + "minimum": 60, + "maximum": 86400, + "description": "Seconds until it expires: default 3600, at most 86400." + }, + "read_only": { + "type": "boolean", + "description": "The user can look but not change anything." + } + }, + "additionalProperties": false, + "examples": [ + { + "expires_in": 3600 + } + ] + }, + "ReceivedEmailList": { "type": "object", "properties": { "object": { "type": "string", "enum": [ - "risk_settings" + "list" ] }, - "review_threshold": { - "type": "number", - "description": "Scores at or above it are `review`." - }, - "block_threshold": { - "type": "number", - "description": "Scores at or above it are `block`." - }, - "disabled_reasons": { + "data": { "type": "array", "items": { - "type": "string", - "enum": [ - "email.invalid", - "email.disposable", - "email.undeliverable", - "email.role_account", - "email.typo", - "email.domain_new", - "ip.tor", - "ip.vpn_network", - "ip.hosting_provider", - "ip.vpn", - "ip.proxy", - "ip.relay", - "ip.private", - "velocity.signups_per_ip_1h", - "velocity.signups_per_ip_24h", - "velocity.signups_per_subnet_1h", - "velocity.signups_per_email_domain_1h", - "velocity.signups_per_mailbox_24h", - "velocity.signups_per_device_24h", - "velocity.checkouts_per_ip_1h", - "velocity.checkouts_per_user_1h", - "velocity.checkouts_per_card_24h", - "velocity.logins_per_ip_1h", - "velocity.logins_per_user_1h", - "velocity.events_per_ip_1h", - "velocity.events_per_user_1h", - "duplicate.canonical_mailbox", - "duplicate.device", - "history.fraud_mailbox", - "history.fraud_user", - "history.fraud_ip", - "history.fraud_subnet", - "history.fraud_card", - "history.fraud_device", - "checkout.card_country_mismatch", - "checkout.billing_country_mismatch", - "checkout.prepaid_card", - "device.automation", - "device.invalid", - "login.new_country", - "login.new_network", - "captcha.failed", - "captcha.missing" - ] - }, - "description": "Reasons that never fire in this environment (they don't count in the score)." + "$ref": "#/components/schemas/ReceivedEmailSummary" + } }, - "captcha": { - "anyOf": [ + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ { - "type": "object", - "properties": { - "provider": { - "type": "string", - "enum": [ - "turnstile", - "hcaptcha" - ], - "description": "The captcha provider." - }, - "required": { - "type": "boolean", - "description": "Whether assessments without a token get `captcha.missing`." + "object": "received_email", + "id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", + "status": "received", + "blocked_reason": null, + "mail_from": "jane@example.com", + "recipients": [ + "support@inbound.acme.com" + ], + "from": "Jane Doe ", + "to": [ + "support@inbound.acme.com" + ], + "cc": [], + "subject": "Re: Your order", + "message_id": "", + "attachments": [ + { + "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", + "filename": "invoice.pdf", + "content_type": "application/pdf", + "content_id": null, + "disposition": "attachment", + "size": 48213 } + ], + "size": 61432, + "spam": false, + "verdicts": { + "spam": "pass", + "virus": "pass", + "spf": "pass", + "dkim": "pass", + "dmarc": "pass", + "dmarc_policy": null }, - "required": [ - "provider", - "required" + "sandbox": false, + "received_at": "2026-10-10T12:00:00.000Z" + } + ], + "has_more": false + } + ] + }, + "ReceivedEmailSummary": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "received_email" + ] + }, + "id": { + "type": "string", + "examples": [ + "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "status": { + "type": "string", + "enum": [ + "received", + "blocked" + ], + "description": "`received`, or `blocked` when the message carried a virus: then only its envelope and verdicts are kept, never its content." + }, + "blocked_reason": { + "anyOf": [ + { + "type": "string", + "enum": [ + "virus" ] }, { "type": "null" } ], - "description": "The captcha provider assessments verify tokens with (never the secret)." + "description": "Why it's `blocked`. `null`: it isn't." }, - "content_thresholds": { - "type": "object", - "properties": { - "spam": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "description": "The score at which `content.spam` fires. `null`: never." - }, - "phishing": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "description": "The score at which `content.phishing` fires. `null`: never." - }, - "malware": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "description": "The score at which `content.malware` fires. `null`: never." - }, - "fraud": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "description": "The score at which `content.fraud` fires. `null`: never." - } + "mail_from": { + "type": "string", + "description": "The envelope sender (`MAIL FROM`); empty for bounces." + }, + "recipients": { + "type": "array", + "items": { + "type": "string" }, - "required": [ - "spam", - "phishing", - "malware", - "fraud" + "description": "The envelope recipients this environment received the message for (Bcc'd addresses included, other organizations' never)." + }, + "from": { + "type": [ + "string", + "null" ], - "description": "Per content category, the score at which `content.` fires; `null`: never (the score is still in `signals.content` for your rules)." + "description": "The `From` header." }, - "reason_weights": { - "type": "object", - "properties": { - "email.invalid": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "email.disposable": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "email.undeliverable": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "email.role_account": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "email.typo": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "email.domain_new": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "ip.tor": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "ip.vpn_network": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "ip.hosting_provider": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "ip.vpn": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "ip.proxy": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "ip.relay": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "ip.private": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "velocity.signups_per_ip_1h": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "velocity.signups_per_ip_24h": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "velocity.signups_per_subnet_1h": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "velocity.signups_per_email_domain_1h": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "velocity.signups_per_mailbox_24h": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "velocity.signups_per_device_24h": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "velocity.checkouts_per_ip_1h": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "velocity.checkouts_per_user_1h": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "velocity.checkouts_per_card_24h": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "velocity.logins_per_ip_1h": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "velocity.logins_per_user_1h": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "velocity.events_per_ip_1h": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "velocity.events_per_user_1h": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "duplicate.canonical_mailbox": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "duplicate.device": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "history.fraud_mailbox": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "history.fraud_user": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "history.fraud_ip": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "history.fraud_subnet": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "history.fraud_card": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "history.fraud_device": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "checkout.card_country_mismatch": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "checkout.billing_country_mismatch": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "checkout.prepaid_card": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "device.automation": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "device.invalid": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "login.new_country": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "login.new_network": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "captcha.failed": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "captcha.missing": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "content.spam": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "content.phishing": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "content.malware": { - "type": "number", - "description": "This reason's weight in this environment." - }, - "content.fraud": { - "type": "number", - "description": "This reason's weight in this environment." - } + "to": { + "type": "array", + "items": { + "type": "string" }, - "description": "Reasons whose weight this environment changed (from calibration or by hand); the rest weigh what the reason catalog says." + "description": "The `To` header's addresses." }, - "updated_at": { + "cc": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The `Cc` header's addresses." + }, + "subject": { + "type": [ + "string", + "null" + ], + "description": "The `Subject` header. `null`: none." + }, + "message_id": { "type": [ "string", "null" ], + "description": "The `Message-ID` header." + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReceivedEmailAttachment" + }, + "description": "Its attachments, without their bytes." + }, + "size": { + "type": "integer", + "description": "Size of the raw message in bytes." + }, + "spam": { + "type": "boolean", + "description": "Whether the spam verdict is `fail`." + }, + "verdicts": { + "$ref": "#/components/schemas/ReceivedEmailVerdicts" + }, + "sandbox": { + "type": "boolean", + "description": "Whether it was received by a sandbox environment." + }, + "received_at": { + "type": "string", "format": "date-time", - "description": "`null` while on the defaults." + "description": "When it arrived." } }, "required": [ "object", - "review_threshold", - "block_threshold", - "disabled_reasons", - "captcha", - "content_thresholds", - "reason_weights", - "updated_at" + "id", + "status", + "blocked_reason", + "mail_from", + "recipients", + "from", + "to", + "cc", + "subject", + "message_id", + "attachments", + "size", + "spam", + "verdicts", + "sandbox", + "received_at" ], "examples": [ { - "object": "risk_settings", - "review_threshold": 0.5, - "block_threshold": 0.85, - "disabled_reasons": [ - "email.role_account" + "object": "received_email", + "id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", + "status": "received", + "blocked_reason": null, + "mail_from": "jane@example.com", + "recipients": [ + "support@inbound.acme.com" ], - "captcha": { - "provider": "turnstile", - "required": true - }, - "content_thresholds": { - "spam": null, - "phishing": 0.8, - "malware": 0.8, - "fraud": 0.9 - }, - "reason_weights": { - "ip.hosting_provider": 0.1 + "from": "Jane Doe ", + "to": [ + "support@inbound.acme.com" + ], + "cc": [], + "subject": "Re: Your order", + "message_id": "", + "attachments": [ + { + "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", + "filename": "invoice.pdf", + "content_type": "application/pdf", + "content_id": null, + "disposition": "attachment", + "size": 48213 + } + ], + "size": 61432, + "spam": false, + "verdicts": { + "spam": "pass", + "virus": "pass", + "spf": "pass", + "dkim": "pass", + "dmarc": "pass", + "dmarc_policy": null }, - "updated_at": "2026-01-15T09:30:00.000Z" + "sandbox": false, + "received_at": "2026-10-10T12:00:00.000Z" } ] }, - "UpdateRiskSettingsRequest": { + "ReceivedEmailAttachment": { "type": "object", "properties": { - "review_threshold": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1, - "description": "Scores at or above it are `review`." + "id": { + "type": "string" }, - "block_threshold": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1, - "description": "Scores at or above it are `block`." + "filename": { + "type": [ + "string", + "null" + ], + "description": "`null` when the part named none." }, - "disabled_reasons": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "email.invalid", - "email.disposable", - "email.undeliverable", - "email.role_account", - "email.typo", - "email.domain_new", - "ip.tor", - "ip.vpn_network", - "ip.hosting_provider", - "ip.vpn", - "ip.proxy", - "ip.relay", - "ip.private", - "velocity.signups_per_ip_1h", - "velocity.signups_per_ip_24h", - "velocity.signups_per_subnet_1h", - "velocity.signups_per_email_domain_1h", - "velocity.signups_per_mailbox_24h", - "velocity.signups_per_device_24h", - "velocity.checkouts_per_ip_1h", - "velocity.checkouts_per_user_1h", - "velocity.checkouts_per_card_24h", - "velocity.logins_per_ip_1h", - "velocity.logins_per_user_1h", - "velocity.events_per_ip_1h", - "velocity.events_per_user_1h", - "duplicate.canonical_mailbox", - "duplicate.device", - "history.fraud_mailbox", - "history.fraud_user", - "history.fraud_ip", - "history.fraud_subnet", - "history.fraud_card", - "history.fraud_device", - "checkout.card_country_mismatch", - "checkout.billing_country_mismatch", - "checkout.prepaid_card", - "device.automation", - "device.invalid", - "login.new_country", - "login.new_network", - "captcha.failed", - "captcha.missing" - ] - }, - "maxItems": 100, - "description": "Reasons that never fire in this environment. Replaces the list." + "content_type": { + "type": "string" }, - "captcha": { + "content_id": { + "type": [ + "string", + "null" + ], + "description": "Set for parts the HTML references (`cid:` URLs)." + }, + "disposition": { + "type": "string", + "enum": [ + "attachment", + "inline" + ] + }, + "size": { + "type": "integer", + "description": "Size in bytes." + } + }, + "required": [ + "id", + "filename", + "content_type", + "content_id", + "disposition", + "size" + ], + "examples": [ + { + "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", + "filename": "invoice.pdf", + "content_type": "application/pdf", + "content_id": null, + "disposition": "attachment", + "size": 48213 + } + ] + }, + "InboundAddress": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "inbound_address" + ] + }, + "environment_id": { + "type": "string" + }, + "domain": { + "type": "string", + "description": "Email sent to any address at this domain is received by the environment, with no DNS setup. It never changes." + } + }, + "required": [ + "object", + "environment_id", + "domain" + ], + "examples": [ + { + "object": "inbound_address", + "environment_id": "env_01k7c8m4q2f3e9v6t5r1n0p8yz", + "domain": "k3j9x2m7qp4l.inbound.coss.com" + } + ] + }, + "ReceivedEmail": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "received_email" + ] + }, + "id": { + "type": "string", + "examples": [ + "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "status": { + "type": "string", + "enum": [ + "received", + "blocked" + ], + "description": "`received`, or `blocked` when the message carried a virus: then only its envelope and verdicts are kept, never its content." + }, + "blocked_reason": { "anyOf": [ { - "type": "object", - "properties": { - "provider": { - "type": "string", - "enum": [ - "turnstile", - "hcaptcha" - ], - "description": "The captcha provider." - }, - "secret": { - "type": "string", - "minLength": 1, - "maxLength": 1024, - "description": "The provider's secret key. Required when setting a provider for the first time or changing it; stored encrypted and never returned." - }, - "required": { - "type": "boolean", - "description": "Assessments without a `captcha` token get `captcha.missing`. Default false." - } - }, - "required": [ - "provider" - ], - "additionalProperties": false + "type": "string", + "enum": [ + "virus" + ] }, { "type": "null" } ], - "description": "`null` turns captcha verification off and deletes the secret." + "description": "Why it's `blocked`. `null`: it isn't." }, - "content_thresholds": { - "type": "object", - "properties": { - "spam": { - "anyOf": [ - { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1 - }, - { - "type": "null" - } - ], - "description": "The score at which `content.spam` fires. `null` removes it." - }, - "phishing": { - "anyOf": [ - { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1 - }, - { - "type": "null" - } - ], - "description": "The score at which `content.phishing` fires. `null` removes it." - }, - "malware": { - "anyOf": [ - { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1 - }, - { - "type": "null" - } - ], - "description": "The score at which `content.malware` fires. `null` removes it." - }, - "fraud": { - "anyOf": [ - { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1 - }, - { - "type": "null" - } - ], - "description": "The score at which `content.fraud` fires. `null` removes it." - } + "mail_from": { + "type": "string", + "description": "The envelope sender (`MAIL FROM`); empty for bounces." + }, + "recipients": { + "type": "array", + "items": { + "type": "string" }, - "additionalProperties": false, - "description": "Changes the categories given: a score in (0, 1] sets the threshold, `null` removes it." + "description": "The envelope recipients this environment received the message for (Bcc'd addresses included, other organizations' never)." }, - "reason_weights": { - "type": "object", - "properties": { - "email.invalid": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "from": { + "type": [ + "string", + "null" + ], + "description": "The `From` header." + }, + "to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The `To` header's addresses." + }, + "cc": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The `Cc` header's addresses." + }, + "subject": { + "type": [ + "string", + "null" + ], + "description": "The `Subject` header. `null`: none." + }, + "message_id": { + "type": [ + "string", + "null" + ], + "description": "The `Message-ID` header." + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReceivedEmailAttachment" + }, + "description": "Its attachments, without their bytes." + }, + "size": { + "type": "integer", + "description": "Size of the raw message in bytes." + }, + "spam": { + "type": "boolean", + "description": "Whether the spam verdict is `fail`." + }, + "verdicts": { + "$ref": "#/components/schemas/ReceivedEmailVerdicts" + }, + "sandbox": { + "type": "boolean", + "description": "Whether it was received by a sandbox environment." + }, + "received_at": { + "type": "string", + "format": "date-time", + "description": "When it arrived." + }, + "reply_to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The `Reply-To` header's addresses." + }, + "in_reply_to": { + "type": [ + "string", + "null" + ], + "description": "The `In-Reply-To` header. `null`: none." + }, + "references": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Message ids of the `References` header." + }, + "sent_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "The `Date` header, when valid." + }, + "headers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The header's name." + }, + "value": { + "type": "string", + "description": "The header's value." + } }, - "email.disposable": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "required": [ + "name", + "value" + ] + }, + "description": "Every header in message order (duplicates included)." + }, + "text": { + "type": [ + "string", + "null" + ], + "description": "The plain text body. `null`: none, or it's `blocked`." + }, + "html": { + "type": [ + "string", + "null" + ], + "description": "The HTML body. `null`: none, or it's `blocked`." + }, + "truncated": { + "type": "boolean", + "description": "Whether `text`, `html` or `headers` were shortened (2 MB per body, 64 KB of headers). The raw message always has all of it." + } + }, + "required": [ + "object", + "id", + "status", + "blocked_reason", + "mail_from", + "recipients", + "from", + "to", + "cc", + "subject", + "message_id", + "attachments", + "size", + "spam", + "verdicts", + "sandbox", + "received_at", + "reply_to", + "in_reply_to", + "references", + "sent_at", + "headers", + "text", + "html", + "truncated" + ], + "examples": [ + { + "object": "received_email", + "id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", + "status": "received", + "blocked_reason": null, + "mail_from": "jane@example.com", + "recipients": [ + "support@inbound.acme.com" + ], + "from": "Jane Doe ", + "to": [ + "support@inbound.acme.com" + ], + "cc": [], + "subject": "Re: Your order", + "message_id": "", + "attachments": [ + { + "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", + "filename": "invoice.pdf", + "content_type": "application/pdf", + "content_id": null, + "disposition": "attachment", + "size": 48213 + } + ], + "size": 61432, + "spam": false, + "verdicts": { + "spam": "pass", + "virus": "pass", + "spf": "pass", + "dkim": "pass", + "dmarc": "pass", + "dmarc_policy": null + }, + "sandbox": false, + "received_at": "2026-10-10T12:00:00.000Z", + "reply_to": [], + "in_reply_to": "", + "references": [ + "" + ], + "sent_at": "2026-10-10T11:59:58.000Z", + "headers": [ + { + "name": "From", + "value": "Jane Doe " }, - "email.undeliverable": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + { + "name": "To", + "value": "support@inbound.acme.com" }, - "email.role_account": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, + { + "name": "Subject", + "value": "Re: Your order" + } + ], + "text": "Hi, the order arrived damaged.", + "html": "

Hi, the order arrived damaged.

", + "truncated": false + } + ] + }, + "DeletedReceivedEmail": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "received_email" + ] + }, + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "object", + "id", + "deleted" + ], + "examples": [ + { + "object": "received_email", + "id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", + "deleted": true + } + ] + }, + "Inbox": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "inbox" + ] + }, + "id": { + "type": "string" + }, + "address": { + "type": "string", + "description": "The inbox's email address (`username@domain`)." + }, + "username": { + "type": "string", + "description": "The part of the address before the `@`." + }, + "domain": { + "type": "string", + "description": "The part of the address after the `@`." + }, + "kind": { + "type": "string", + "enum": [ + "managed", + "domain" + ], + "description": "`managed`: at the environment's own domain under coss.com's inbound domain (no DNS setup). `domain`: at one of the environment's receiving domains." + }, + "display_name": { + "type": [ + "string", + "null" + ], + "description": "The name messages from the inbox are sent with." + }, + "client_id": { + "type": [ + "string", + "null" + ], + "description": "Your own id for the inbox, e.g. your agent's or user's (unique per environment)." + }, + "can_send": { + "type": "boolean", + "description": "Whether messages can be sent from the inbox. Inboxes at coss.com's inbound domain only receive in live environments: create the inbox at a receiving domain under one of your verified domains to send from it." + }, + "sandbox": { + "type": "boolean", + "description": "Whether it belongs to a sandbox environment." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "object", + "id", + "address", + "username", + "domain", + "kind", + "display_name", + "client_id", + "can_send", + "sandbox", + "created_at", + "updated_at" + ], + "examples": [ + { + "object": "inbox", + "id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", + "address": "support-agent@k3j9x2m7qp4l.inbound.coss.com", + "username": "support-agent", + "domain": "k3j9x2m7qp4l.inbound.coss.com", + "kind": "managed", + "display_name": "Acme Support", + "client_id": "agent-42", + "can_send": true, + "sandbox": true, + "created_at": "2026-10-10T12:00:00.000Z", + "updated_at": "2026-10-10T12:00:00.000Z" + } + ] + }, + "CreateInboxRequest": { + "type": "object", + "properties": { + "username": { + "type": "string", + "maxLength": 64, + "pattern": "^(?!.*\\.\\.)[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$", + "description": "The part before the @. Omit it for a random one. Mail to `username+anything@domain` reaches the inbox too.", + "examples": [ + "support-agent" + ] + }, + "domain": { + "type": "string", + "minLength": 1, + "maxLength": 253, + "description": "One of the environment's receiving domains (its hostname). Omit it for the environment's own domain under coss.com's inbound domain, which needs no DNS setup.", + "examples": [ + "agents.acme.com" + ] + }, + "display_name": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "The name messages from the inbox are sent with.", + "examples": [ + "Acme Support" + ] + }, + "client_id": { + "type": "string", + "pattern": "^[\\x21-\\x7e]{1,128}$", + "description": "Your own id for the inbox (unique per environment). Creating an inbox with a `client_id` that already has one returns that inbox instead (200), so retries and \"one inbox per user\" need no bookkeeping.", + "examples": [ + "agent-42" + ] + } + }, + "additionalProperties": false, + "examples": [ + { + "username": "support-agent", + "display_name": "Acme Support", + "client_id": "agent-42" + } + ] + }, + "InboxList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Inbox" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ + { + "object": "inbox", + "id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", + "address": "support-agent@k3j9x2m7qp4l.inbound.coss.com", + "username": "support-agent", + "domain": "k3j9x2m7qp4l.inbound.coss.com", + "kind": "managed", + "display_name": "Acme Support", + "client_id": "agent-42", + "can_send": true, + "sandbox": true, + "created_at": "2026-10-10T12:00:00.000Z", + "updated_at": "2026-10-10T12:00:00.000Z" + } + ], + "has_more": false + } + ] + }, + "UpdateInboxRequest": { + "type": "object", + "properties": { + "display_name": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + { + "type": "null" + } + ], + "description": "The name messages are sent with; `null` sends with the bare address." + } + }, + "additionalProperties": false, + "examples": [ + { + "display_name": "Acme Help" + } + ] + }, + "DeletedInbox": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "inbox" + ] + }, + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "object", + "id", + "deleted" + ], + "examples": [ + { + "object": "inbox", + "id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", + "deleted": true + } + ] + }, + "InboxThreadList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InboxThreadSummary" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ + { + "object": "inbox_thread", + "id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", + "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", + "subject": "My order arrived damaged", + "participants": [ + "jane@example.com" + ], + "message_count": 2, + "preview": "Sorry about that, Jane! A new one is on its way.", + "last_message_at": "2026-10-10T12:01:00.000Z", + "last_inbound_at": "2026-10-10T12:00:00.000Z", + "created_at": "2026-10-10T12:00:00.000Z" + } + ], + "has_more": false + } + ] + }, + "InboxThreadSummary": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "inbox_thread" + ] + }, + "id": { + "type": "string" + }, + "inbox_id": { + "type": "string", + "description": "The inbox's id." + }, + "subject": { + "type": [ + "string", + "null" + ], + "description": "The first message's subject." + }, + "participants": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Addresses of everyone in the conversation but the inbox (Bcc'd ones never)." + }, + "message_count": { + "type": "integer", + "description": "Messages in the thread." + }, + "preview": { + "type": [ + "string", + "null" + ], + "description": "The start of the latest message." + }, + "last_message_at": { + "type": "string", + "format": "date-time", + "description": "When its latest message was received or sent." + }, + "last_inbound_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When its latest inbound message arrived: later than `last_message_at`'s outbound one means the conversation awaits a reply." + }, + "created_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "object", + "id", + "inbox_id", + "subject", + "participants", + "message_count", + "preview", + "last_message_at", + "last_inbound_at", + "created_at" + ], + "examples": [ + { + "object": "inbox_thread", + "id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", + "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", + "subject": "My order arrived damaged", + "participants": [ + "jane@example.com" + ], + "message_count": 2, + "preview": "Sorry about that, Jane! A new one is on its way.", + "last_message_at": "2026-10-10T12:01:00.000Z", + "last_inbound_at": "2026-10-10T12:00:00.000Z", + "created_at": "2026-10-10T12:00:00.000Z" + } + ] + }, + "InboxThread": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "inbox_thread" + ] + }, + "id": { + "type": "string" + }, + "inbox_id": { + "type": "string", + "description": "The inbox's id." + }, + "subject": { + "type": [ + "string", + "null" + ], + "description": "The first message's subject." + }, + "participants": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Addresses of everyone in the conversation but the inbox (Bcc'd ones never)." + }, + "message_count": { + "type": "integer", + "description": "Messages in the thread." + }, + "preview": { + "type": [ + "string", + "null" + ], + "description": "The start of the latest message." + }, + "last_message_at": { + "type": "string", + "format": "date-time", + "description": "When its latest message was received or sent." + }, + "last_inbound_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When its latest inbound message arrived: later than `last_message_at`'s outbound one means the conversation awaits a reply." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InboxMessage" + }, + "description": "The thread's messages, oldest first: its latest 50 (see `message_count`; list the inbox's messages for older ones)." + } + }, + "required": [ + "object", + "id", + "inbox_id", + "subject", + "participants", + "message_count", + "preview", + "last_message_at", + "last_inbound_at", + "created_at", + "messages" + ], + "examples": [ + { + "object": "inbox_thread", + "id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", + "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", + "subject": "My order arrived damaged", + "participants": [ + "jane@example.com" + ], + "message_count": 2, + "preview": "Sorry about that, Jane! A new one is on its way.", + "last_message_at": "2026-10-10T12:01:00.000Z", + "last_inbound_at": "2026-10-10T12:00:00.000Z", + "created_at": "2026-10-10T12:00:00.000Z", + "messages": [ + { + "object": "inbox_message", + "id": "imsg_01k7c8m4s1d3c9x0z4e6g7h8j9", + "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", + "thread_id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", + "direction": "inbound", + "status": "received", + "from": "Jane Doe ", + "to": [ + "support-agent@k3j9x2m7qp4l.inbound.coss.com" + ], + "cc": [], + "subject": "My order arrived damaged", + "preview": "Hi, the order arrived damaged. Can you send a new one?", + "attachments": [ + { + "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", + "filename": "photo.jpg", + "content_type": "image/jpeg", + "content_id": null, + "disposition": "attachment", + "size": 48213 + } + ], + "spam": false, + "received_email_id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", + "email_id": null, + "created_at": "2026-10-10T12:00:00.000Z", + "reply_to": [], + "message_id": "", + "in_reply_to": null, + "references": [], + "text": "Hi, the order arrived damaged. Can you send a new one?\n\nJane", + "extracted_text": "Hi, the order arrived damaged. Can you send a new one?\n\nJane", + "html": null, + "truncated": false + }, + { + "object": "inbox_message", + "id": "imsg_01k7c8m5t2e4d0y1a5f7h8j9k0", + "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", + "thread_id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", + "direction": "outbound", + "status": "queued", + "from": "\"Acme Support\" ", + "to": [ + "jane@example.com" + ], + "cc": [], + "subject": "Re: My order arrived damaged", + "preview": "Sorry about that, Jane! A new one is on its way.", + "attachments": [], + "spam": false, + "received_email_id": null, + "email_id": "email_01k7c8m5t2e4d0y1a5f7h8j9k0", + "created_at": "2026-10-10T12:01:00.000Z", + "reply_to": [], + "message_id": null, + "in_reply_to": "", + "references": [ + "" + ], + "text": "Sorry about that, Jane! A new one is on its way.", + "extracted_text": "Sorry about that, Jane! A new one is on its way.", + "html": null, + "truncated": false + } + ] + } + ] + }, + "InboxMessage": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "inbox_message" + ] + }, + "id": { + "type": "string" + }, + "inbox_id": { + "type": "string" + }, + "thread_id": { + "type": "string" + }, + "direction": { + "type": "string", + "enum": [ + "inbound", + "outbound" + ], + "description": "`inbound`: received by the inbox. `outbound`: sent from it." + }, + "status": { + "type": "string", + "enum": [ + "received", + "queued", + "sent", + "failed", + "scheduled", + "canceled" + ], + "description": "`received` for inbound messages; an outbound message's email status (`queued`, `sent`, `failed`, …): its email has each recipient's delivery." + }, + "from": { + "type": [ + "string", + "null" + ], + "description": "The `From` header." + }, + "to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Addresses in the `To` header." + }, + "cc": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Addresses in the `Cc` header." + }, + "subject": { + "type": [ + "string", + "null" + ], + "description": "The subject. `null`: none." + }, + "preview": { + "type": [ + "string", + "null" + ], + "description": "The start of the message's own text (without the quoted conversation)." + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReceivedEmailAttachment" + }, + "description": "An inbound message's attachments (download them from the message)." + }, + "spam": { + "type": "boolean", + "description": "Whether the receiving server judged it spam." + }, + "received_email_id": { + "type": [ + "string", + "null" + ], + "description": "The received email of an inbound message." + }, + "email_id": { + "type": [ + "string", + "null" + ], + "description": "The email of an outbound message: its delivery events and webhooks name it." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When it was received or sent." + }, + "reply_to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Addresses in the `Reply-To` header." + }, + "message_id": { + "type": [ + "string", + "null" + ], + "description": "An inbound message's `Message-ID` header (outbound ones get theirs at sending)." + }, + "in_reply_to": { + "type": [ + "string", + "null" + ], + "description": "The message it answers, by `Message-ID`. `null`: none." + }, + "references": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The `Message-ID`s of the conversation so far." + }, + "text": { + "type": [ + "string", + "null" + ], + "description": "The plain text part. `null`: none." + }, + "extracted_text": { + "type": [ + "string", + "null" + ], + "description": "The message's own text: `text` without the quoted conversation and signature below it (a heuristic; when nothing is left, all of `text`). Made from `html` when there is no `text`." + }, + "html": { + "type": [ + "string", + "null" + ], + "description": "The sender's HTML: sanitize it before showing it to anyone." + }, + "truncated": { + "type": "boolean", + "description": "Whether `text` or `html` were shortened (2 MB each)." + } + }, + "required": [ + "object", + "id", + "inbox_id", + "thread_id", + "direction", + "status", + "from", + "to", + "cc", + "subject", + "preview", + "attachments", + "spam", + "received_email_id", + "email_id", + "created_at", + "reply_to", + "message_id", + "in_reply_to", + "references", + "text", + "extracted_text", + "html", + "truncated" + ], + "examples": [ + { + "object": "inbox_message", + "id": "imsg_01k7c8m4s1d3c9x0z4e6g7h8j9", + "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", + "thread_id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", + "direction": "inbound", + "status": "received", + "from": "Jane Doe ", + "to": [ + "support-agent@k3j9x2m7qp4l.inbound.coss.com" + ], + "cc": [], + "subject": "My order arrived damaged", + "preview": "Hi, the order arrived damaged. Can you send a new one?", + "attachments": [ + { + "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", + "filename": "photo.jpg", + "content_type": "image/jpeg", + "content_id": null, + "disposition": "attachment", + "size": 48213 + } + ], + "spam": false, + "received_email_id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", + "email_id": null, + "created_at": "2026-10-10T12:00:00.000Z", + "reply_to": [], + "message_id": "", + "in_reply_to": null, + "references": [], + "text": "Hi, the order arrived damaged. Can you send a new one?\n\nJane", + "extracted_text": "Hi, the order arrived damaged. Can you send a new one?\n\nJane", + "html": null, + "truncated": false + }, + { + "object": "inbox_message", + "id": "imsg_01k7c8m5t2e4d0y1a5f7h8j9k0", + "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", + "thread_id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", + "direction": "outbound", + "status": "queued", + "from": "\"Acme Support\" ", + "to": [ + "jane@example.com" + ], + "cc": [], + "subject": "Re: My order arrived damaged", + "preview": "Sorry about that, Jane! A new one is on its way.", + "attachments": [], + "spam": false, + "received_email_id": null, + "email_id": "email_01k7c8m5t2e4d0y1a5f7h8j9k0", + "created_at": "2026-10-10T12:01:00.000Z", + "reply_to": [], + "message_id": null, + "in_reply_to": "", + "references": [ + "" + ], + "text": "Sorry about that, Jane! A new one is on its way.", + "extracted_text": "Sorry about that, Jane! A new one is on its way.", + "html": null, + "truncated": false + } + ] + }, + "DeletedInboxThread": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "inbox_thread" + ] + }, + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "object", + "id", + "deleted" + ], + "examples": [ + { + "object": "inbox_thread", + "id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", + "deleted": true + } + ] + }, + "InboxMessageList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InboxMessageSummary" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ + { + "object": "inbox_message", + "id": "imsg_01k7c8m4s1d3c9x0z4e6g7h8j9", + "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", + "thread_id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", + "direction": "inbound", + "status": "received", + "from": "Jane Doe ", + "to": [ + "support-agent@k3j9x2m7qp4l.inbound.coss.com" + ], + "cc": [], + "subject": "My order arrived damaged", + "preview": "Hi, the order arrived damaged. Can you send a new one?", + "attachments": [ + { + "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", + "filename": "photo.jpg", + "content_type": "image/jpeg", + "content_id": null, + "disposition": "attachment", + "size": 48213 + } + ], + "spam": false, + "received_email_id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", + "email_id": null, + "created_at": "2026-10-10T12:00:00.000Z" + } + ], + "has_more": false + } + ] + }, + "InboxMessageSummary": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "inbox_message" + ] + }, + "id": { + "type": "string" + }, + "inbox_id": { + "type": "string" + }, + "thread_id": { + "type": "string" + }, + "direction": { + "type": "string", + "enum": [ + "inbound", + "outbound" + ], + "description": "`inbound`: received by the inbox. `outbound`: sent from it." + }, + "status": { + "type": "string", + "enum": [ + "received", + "queued", + "sent", + "failed", + "scheduled", + "canceled" + ], + "description": "`received` for inbound messages; an outbound message's email status (`queued`, `sent`, `failed`, …): its email has each recipient's delivery." + }, + "from": { + "type": [ + "string", + "null" + ], + "description": "The `From` header." + }, + "to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Addresses in the `To` header." + }, + "cc": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Addresses in the `Cc` header." + }, + "subject": { + "type": [ + "string", + "null" + ], + "description": "The subject. `null`: none." + }, + "preview": { + "type": [ + "string", + "null" + ], + "description": "The start of the message's own text (without the quoted conversation)." + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReceivedEmailAttachment" + }, + "description": "An inbound message's attachments (download them from the message)." + }, + "spam": { + "type": "boolean", + "description": "Whether the receiving server judged it spam." + }, + "received_email_id": { + "type": [ + "string", + "null" + ], + "description": "The received email of an inbound message." + }, + "email_id": { + "type": [ + "string", + "null" + ], + "description": "The email of an outbound message: its delivery events and webhooks name it." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When it was received or sent." + } + }, + "required": [ + "object", + "id", + "inbox_id", + "thread_id", + "direction", + "status", + "from", + "to", + "cc", + "subject", + "preview", + "attachments", + "spam", + "received_email_id", + "email_id", + "created_at" + ], + "examples": [ + { + "object": "inbox_message", + "id": "imsg_01k7c8m4s1d3c9x0z4e6g7h8j9", + "inbox_id": "inbox_01k7c8m4q2f3e9v6t5r1n0p8yz", + "thread_id": "thread_01k7c8m4r0c2b8w9y3d5f6g7h8", + "direction": "inbound", + "status": "received", + "from": "Jane Doe ", + "to": [ + "support-agent@k3j9x2m7qp4l.inbound.coss.com" + ], + "cc": [], + "subject": "My order arrived damaged", + "preview": "Hi, the order arrived damaged. Can you send a new one?", + "attachments": [ + { + "id": "rcvdatt_01k7c8m4r0c2b8w9y3d5f6g7h8", + "filename": "photo.jpg", + "content_type": "image/jpeg", + "content_id": null, + "disposition": "attachment", + "size": 48213 + } + ], + "spam": false, + "received_email_id": "rcvd_01k7c8m4q2f3e9v6t5r1n0p8yz", + "email_id": null, + "created_at": "2026-10-10T12:00:00.000Z" + } + ] + }, + "SendInboxMessageRequest": { + "type": "object", + "properties": { + "to": { + "anyOf": [ + { + "type": "string", + "description": "An email address, optionally with a display name: \"Acme \"." + }, + { + "type": "array", + "items": { + "type": "string", + "description": "An email address, optionally with a display name: \"Acme \"." + }, + "minItems": 1, + "maxItems": 50 + } + ], + "description": "Who it goes to." + }, + "cc": { + "anyOf": [ + { + "type": "string", + "description": "An email address, optionally with a display name: \"Acme \"." + }, + { + "type": "array", + "items": { + "type": "string", + "description": "An email address, optionally with a display name: \"Acme \"." + }, + "minItems": 0, + "maxItems": 50 + } + ], + "description": "Who it's copied to." + }, + "bcc": { + "anyOf": [ + { + "type": "string", + "description": "An email address, optionally with a display name: \"Acme \"." + }, + { + "type": "array", + "items": { + "type": "string", + "description": "An email address, optionally with a display name: \"Acme \"." + }, + "minItems": 0, + "maxItems": 50 + } + ], + "description": "Who it's blind copied to." + }, + "subject": { + "type": "string", + "minLength": 1, + "maxLength": 998, + "description": "The new thread's subject." + }, + "text": { + "type": "string", + "description": "Plain-text body. Without it, an `html` email goes out with a text part generated from the HTML." + }, + "html": { + "type": "string", + "description": "HTML body. Provide `html`, `text`, or both." + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SendEmailAttachment" + }, + "maxItems": 20, + "description": "Up to 20 files, at most 25 MB combined." + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The tag's name, unique on the email." + }, + "value": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The tag's value." + } + }, + "required": [ + "name", + "value" + ], + "additionalProperties": false + }, + "maxItems": 50, + "description": "Your tags on the email." + } + }, + "required": [ + "to", + "subject" + ], + "additionalProperties": false, + "description": "Starts a thread: the message is sent from the inbox's address.", + "examples": [ + { + "to": "jane@example.com", + "subject": "Your order", + "text": "Hi Jane, your order has shipped." + } + ] + }, + "ReplyInboxMessageRequest": { + "type": "object", + "properties": { + "reply_all": { + "type": "boolean", + "description": "Also reply to everyone else on the message (its `to` and `cc`, but the inbox). Default `false`: only the sender (its `Reply-To`, when it has one)." + }, + "cc": { + "anyOf": [ + { + "type": "string", + "description": "An email address, optionally with a display name: \"Acme \"." + }, + { + "type": "array", + "items": { + "type": "string", + "description": "An email address, optionally with a display name: \"Acme \"." + }, + "minItems": 0, + "maxItems": 50 + } + ], + "description": "Who it's copied to." + }, + "bcc": { + "anyOf": [ + { + "type": "string", + "description": "An email address, optionally with a display name: \"Acme \"." + }, + { + "type": "array", + "items": { + "type": "string", + "description": "An email address, optionally with a display name: \"Acme \"." + }, + "minItems": 0, + "maxItems": 50 + } + ], + "description": "Who it's blind copied to." + }, + "text": { + "type": "string", + "description": "Plain-text body. Without it, an `html` email goes out with a text part generated from the HTML." + }, + "html": { + "type": "string", + "description": "HTML body. Provide `html`, `text`, or both." + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SendEmailAttachment" + }, + "maxItems": 20, + "description": "Up to 20 files, at most 25 MB combined." + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The tag's name, unique on the email." + }, + "value": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The tag's value." + } + }, + "required": [ + "name", + "value" + ], + "additionalProperties": false + }, + "maxItems": 50, + "description": "Your tags on the email." + } + }, + "additionalProperties": false, + "description": "Replies in the message's thread, from the inbox's address, with `Re:` and the headers mail clients thread by (`In-Reply-To`, `References`).", + "examples": [ + { + "text": "Sorry about that, Jane! A new one is on its way." + } + ] + }, + "RiskAssessment": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_assessment" + ] + }, + "id": { + "type": "string", + "examples": [ + "rsk_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "event": { + "$ref": "#/components/schemas/RiskEvent" + }, + "event_name": { + "type": [ + "string", + "null" + ], + "description": "Your name for a `custom` event. `null`: none." + }, + "decision": { + "$ref": "#/components/schemas/RiskDecision" + }, + "score": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "0-1: how risky the signals are together. Lists can decide regardless of the score." + }, + "reasons": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RiskReason" + }, + "description": "Why, most significant first." + }, + "signals": { + "type": "object", + "properties": { + "email": { + "anyOf": [ + { + "$ref": "#/components/schemas/RiskEmailSignals" + }, + { + "type": "null" + } + ], + "description": "What the address says. `null`: no email was given." + }, + "ip": { + "anyOf": [ + { + "$ref": "#/components/schemas/RiskIpSignals" + }, + { + "type": "null" + } + ], + "description": "What the IP address says. `null`: none was given." + }, + "velocity": { + "anyOf": [ + { + "$ref": "#/components/schemas/RiskVelocitySignals" + }, + { + "type": "null" + } + ], + "description": "`null` when velocity was unavailable (see `degraded`)." + }, + "captcha": { + "anyOf": [ + { + "$ref": "#/components/schemas/RiskCaptchaSignals" + }, + { + "type": "null" + } + ], + "description": "How the captcha went. `null`: no captcha applied." + }, + "login": { + "anyOf": [ + { + "$ref": "#/components/schemas/RiskLoginSignals" + }, + { + "type": "null" + } + ], + "description": "Whether a login is new for this user. `null`: not a login of a user with history." + }, + "content": { + "anyOf": [ + { + "$ref": "#/components/schemas/RiskContentSignals" + }, + { + "type": "null" + } + ], + "description": "`null` when no content was given or it couldn't be classified." + }, + "checkout": { + "anyOf": [ + { + "$ref": "#/components/schemas/RiskCheckoutSignals" + }, + { + "type": "null" + } + ], + "description": "What you sent in `checkout`. `null`: not given." + }, + "device": { + "anyOf": [ + { + "$ref": "#/components/schemas/RiskDeviceSignals" + }, + { + "type": "null" + } + ], + "description": "What the device token said. `null`: none was given." + } + }, + "required": [ + "email", + "ip", + "velocity", + "captcha", + "login", + "content", + "checkout", + "device" + ], + "description": "What the assessment saw, by group. A group is `null` when it had nothing to go on." + }, + "user": { + "type": "object", + "properties": { + "id": { + "type": [ + "string", + "null" + ], + "description": "Your user id. `null`: none given." + }, + "email": { + "type": [ + "string", + "null" + ], + "description": "`null` once removed (90 days after the assessment) or when none was given." + } + }, + "required": [ + "id", + "email" + ], + "description": "Who it's about." + }, + "ip": { + "type": [ + "string", + "null" + ], + "description": "`null` once removed (90 days after)." + }, + "degraded": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email_dns", + "domain_age", + "ip_data", + "velocity", + "captcha", + "content", + "ip_privacy", + "usage_limit" + ] + }, + "description": "Signal groups that were unavailable for this assessment (it was made without them). `usage_limit` means the plan's included assessments are used up, or a billing hold or budget applies: the assessment was free and made without velocity, memory of earlier events, content or address-level IP data." + }, + "outcome": { + "anyOf": [ + { + "$ref": "#/components/schemas/RiskOutcome" + }, + { + "type": "null" + } + ], + "description": "The latest outcome you reported with feedback." + }, + "review_status": { + "anyOf": [ + { + "type": "string", + "enum": [ + "open", + "resolved" + ] + }, + { + "type": "null" + } + ], + "description": "`open` for `review` decisions until you send feedback, then `resolved`." + }, + "created_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "object", + "id", + "event", + "event_name", + "decision", + "score", + "reasons", + "signals", + "user", + "ip", + "degraded", + "outcome", + "review_status", + "created_at" + ], + "examples": [ + { + "object": "risk_assessment", + "id": "rsk_01k7c8m4q2f3e9v6t5r1n0p8yz", + "event": "signup", + "event_name": null, + "decision": "review", + "score": 0.62, + "reasons": [ + { + "code": "email.typo", + "message": "The domain looks like a typo of gmail.com.", + "suggestion": "gmail.com" + }, + { + "code": "velocity.signups_per_ip_1h", + "message": "7 sign-ups from this IP address in the last hour.", + "value": 7, + "threshold": 5, + "window": "1h" + } + ], + "signals": { + "email": { + "domain": "gmail.con", + "valid": true, + "disposable": false, + "deliverability": "deliverable", + "role_account": false, + "free_provider": false, + "suggestion": "gmail.com", + "domain_age_days": 9 + }, + "ip": { + "version": 4, + "private": false, + "tor": false, + "hosting": false, + "hosting_provider": null, + "hosting_kind": null, + "asn": 64500, + "as_name": "Example Broadband", + "country": "GB", + "privacy": null + }, + "velocity": { + "ip_1h": 7, + "ip_24h": 9, + "subnet_1h": 7, + "mailbox_24h": 1 + }, + "captcha": { + "provided": true, + "passed": true + }, + "login": null, + "content": null, + "checkout": null, + "device": null + }, + "user": { + "id": null, + "email": "ada@gmail.con" + }, + "ip": "203.0.113.7", + "degraded": [], + "outcome": null, + "review_status": "open", + "created_at": "2026-01-15T09:30:00.000Z" + } + ] + }, + "RiskEmailSignals": { + "type": "object", + "properties": { + "domain": { + "type": [ + "string", + "null" + ], + "description": "`null` once the address is removed (90 days after the assessment)." + }, + "valid": { + "type": "boolean", + "description": "Whether it's syntactically valid." + }, + "disposable": { + "type": "boolean", + "description": "Whether its domain is a known burner service." + }, + "deliverability": { + "type": "string", + "enum": [ + "deliverable", + "undeliverable", + "unknown" + ], + "description": "From DNS (MX, A, AAAA). `unknown` when the lookup failed or timed out." + }, + "role_account": { + "type": "boolean", + "description": "Whether it's a role mailbox, like `info@`." + }, + "free_provider": { + "type": "boolean", + "description": "Whether its domain is a free mailbox provider." + }, + "suggestion": { + "type": [ + "string", + "null" + ], + "description": "`email.typo`: the likely intended domain. `null` otherwise." + }, + "domain_age_days": { + "type": [ + "integer", + "null" + ], + "description": "Days since the domain was registered (RDAP); `null` when unknown or not looked up (free providers, disposable domains)." + } + }, + "required": [ + "domain", + "valid", + "disposable", + "deliverability", + "role_account", + "free_provider", + "suggestion", + "domain_age_days" + ], + "description": "What the address says. `null` on an assessment with no email." + }, + "RiskIpSignals": { + "type": "object", + "properties": { + "version": { + "anyOf": [ + { + "type": "number", + "enum": [ + 4 + ] + }, + { + "type": "number", + "enum": [ + 6 + ] + } + ], + "description": "IPv4 or IPv6." + }, + "private": { + "type": "boolean", + "description": "Whether it's a private or reserved address." + }, + "tor": { + "type": "boolean", + "description": "Whether it's a Tor exit." + }, + "hosting": { + "type": "boolean", + "description": "Whether it's in a cloud, hosting or VPN network." + }, + "hosting_provider": { + "type": [ + "string", + "null" + ], + "description": "That network's operator. `null` when it isn't one." + }, + "hosting_kind": { + "type": [ + "string", + "null" + ], + "enum": [ + "cloud", + "hosting", + "vpn", + null + ], + "description": "`cloud`, `hosting` or `vpn`. `null` when it isn't one." + }, + "asn": { + "type": [ + "integer", + "null" + ], + "description": "The network's autonomous system number. `null` when unknown." + }, + "as_name": { + "type": [ + "string", + "null" + ], + "description": "The network's name. `null` when unknown." + }, + "country": { + "type": [ + "string", + "null" + ], + "description": "ISO 3166-1 alpha-2." + }, + "privacy": { + "anyOf": [ + { + "$ref": "#/components/schemas/RiskIpPrivacy" + }, + { + "type": "null" + } + ], + "description": "From coss.com's IP privacy data provider (VPNs, proxies, relays). `null` when it isn't available or was not asked (private addresses)." + } + }, + "required": [ + "version", + "private", + "tor", + "hosting", + "hosting_provider", + "hosting_kind", + "asn", + "as_name", + "country", + "privacy" + ], + "description": "What the address says. `null` on an assessment with no IP." + }, + "RiskIpPrivacy": { + "type": "object", + "properties": { + "vpn": { + "type": "boolean", + "description": "Whether it's a VPN exit." + }, + "proxy": { + "type": "boolean", + "description": "Whether it's a proxy." + }, + "relay": { + "type": "boolean", + "description": "Whether it's a privacy relay." + }, + "service": { + "type": [ + "string", + "null" + ], + "description": "The VPN or proxy service." + } + }, + "required": [ + "vpn", + "proxy", + "relay", + "service" + ] + }, + "RiskVelocitySignals": { + "type": "object", + "additionalProperties": { + "type": "integer" + }, + "description": "Events of the same kind in this environment, this one included, keyed `_` (e.g. `ip_1h`, `mailbox_24h`)." + }, + "RiskCaptchaSignals": { + "type": "object", + "properties": { + "provided": { + "type": "boolean", + "description": "Whether the request carried a token." + }, + "passed": { + "type": [ + "boolean", + "null" + ], + "description": "`null` when it couldn't be checked (no captcha configured, or the provider was unreachable)." + } + }, + "required": [ + "provided", + "passed" + ], + "description": "How the captcha went. `null` when no captcha applies." + }, + "RiskLoginSignals": { + "type": "object", + "properties": { + "new_country": { + "type": "boolean", + "description": "Whether this user never logged in from this country in this environment." + }, + "new_network": { + "type": "boolean", + "description": "Whether this user never logged in from this network (ASN) in this environment." + } + }, + "required": [ + "new_country", + "new_network" + ], + "description": "Logins of a user with history: whether the country or network is new for them." + }, + "RiskContentSignals": { + "type": "object", + "properties": { + "spam": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "The model's probability (0-1) that it's spam." + }, + "phishing": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "The model's probability (0-1) that it's phishing." + }, + "malware": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "The model's probability (0-1) that it's malware." + }, + "fraud": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "The model's probability (0-1) that it's fraud." + } + }, + "required": [ + "spam", + "phishing", + "malware", + "fraud" + ], + "description": "Per category, the model's probability (0-1) that the content belongs to it. Scores are not calibrated: pick thresholds by looking at your own traffic." + }, + "RiskCheckoutSignals": { + "type": "object", + "properties": { + "amount": { + "type": "integer", + "description": "The amount you sent, in the currency's smallest unit." + }, + "currency": { + "type": "string", + "description": "The currency you sent." + }, + "card_country": { + "type": [ + "string", + "null" + ], + "description": "The card's country. `null` when not given." + }, + "card_funding": { + "type": [ + "string", + "null" + ], + "enum": [ + "credit", + "debit", + "prepaid", + "unknown", + null + ], + "description": "The card's funding type. `null` when not given." + }, + "billing_country": { + "type": [ + "string", + "null" + ], + "description": "The billing country. `null` when not given." + }, + "shipping_country": { + "type": [ + "string", + "null" + ], + "description": "The shipping country. `null` when not given." + } + }, + "required": [ + "amount", + "currency", + "card_country", + "card_funding", + "billing_country", + "shipping_country" + ], + "description": "What you sent in `checkout` (never the card)." + }, + "RiskDeviceSignals": { + "type": "object", + "properties": { + "valid": { + "type": "boolean", + "description": "`false`: the token was malformed or too old." + }, + "automation": { + "type": "boolean", + "description": "WebDriver control or a headless browser." + }, + "timezone": { + "type": [ + "string", + "null" + ], + "description": "IANA time zone (`Europe/London`)." + }, + "language": { + "type": [ + "string", + "null" + ], + "description": "The browser's first language." + }, + "platform": { + "type": [ + "string", + "null" + ], + "description": "The browser's reported platform. `null` when it reported none." + }, + "screen": { + "type": [ + "string", + "null" + ], + "description": "`x` in CSS pixels." + }, + "touch": { + "type": [ + "boolean", + "null" + ], + "description": "Whether the device has a touch screen. `null` when it didn't say." + } + }, + "required": [ + "valid", + "automation", + "timezone", + "language", + "platform", + "screen", + "touch" + ], + "description": "What the browser reported through `coss/device` (its device id is only kept as a keyed hash)." + }, + "CreateRiskAssessmentRequest": { + "type": "object", + "properties": { + "event": { + "$ref": "#/components/schemas/RiskEvent" + }, + "event_name": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$", + "description": "Required for `custom` events (e.g. `invite_sent`), not allowed otherwise." + }, + "user": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Your id for the user, when it exists (logins always; sign-ups once the account is created)." + }, + "email": { + "type": "string", + "minLength": 1, + "maxLength": 320, + "description": "The email address the user gave. Checked as given, even if invalid." + } + }, + "additionalProperties": false + }, + "ip": { + "anyOf": [ + { + "type": "string", + "format": "ip" + }, + { + "type": "string", + "format": "ip" + } + ], + "description": "The client's IP address (not your server's or proxy's)." + }, + "user_agent": { + "type": "string", + "maxLength": 1024, + "description": "The client's user agent." + }, + "captcha": { + "type": "object", + "properties": { + "token": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "description": "The token your captcha widget (Turnstile or hCaptcha, set in the environment's risk settings) gave the browser. Verified once: don't reuse it." + } + }, + "required": [ + "token" + ], + "additionalProperties": false + }, + "checkout": { + "$ref": "#/components/schemas/RiskCheckoutRequest" + }, + "device": { + "type": "object", + "properties": { + "token": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "description": "What `collectDevice()` from `coss/device` returned in the user's browser, passed through your server." + } + }, + "required": [ + "token" + ], + "additionalProperties": false, + "description": "The browser the event came from: recognizes it across accounts (`duplicate.device`, device velocity, fraud memory) and spots automation." + }, + "content": { + "type": "object", + "properties": { + "text": { + "type": "string", + "minLength": 1, + "maxLength": 50000, + "description": "Text the user wrote (a profile bio, a message, a listing). Classified by an AI model with zero data retention and never stored: only its scores are." + } + }, + "required": [ + "text" + ], + "additionalProperties": false, + "description": "Content to classify with the event. Its scores go in `signals.content` and rules can test them; `content.*` reasons fire only at thresholds you set." + } + }, + "required": [ + "event" + ], + "additionalProperties": false, + "examples": [ + { + "event": "signup", + "user": { + "email": "ada@gmail.con" + }, + "ip": "203.0.113.7", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 15_0) AppleWebKit/605.1.15" + } + ] + }, + "RiskCheckoutRequest": { + "type": "object", + "properties": { + "amount": { + "type": "integer", + "minimum": 0, + "maximum": 1000000000000, + "description": "The amount in the currency's minor unit (cents for USD: 1999 is $19.99)." + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$", + "description": "ISO 4217, upper-case (`USD`)." + }, + "card": { + "type": "object", + "properties": { + "fingerprint": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Your payment processor's fingerprint of the card number (Stripe: `card.fingerprint`). Counted and remembered as a keyed hash only; never send the card number." + }, + "country": { + "type": "string", + "pattern": "^[A-Z]{2}$", + "description": "Where the card was issued." + }, + "funding": { + "type": "string", + "enum": [ + "credit", + "debit", + "prepaid", + "unknown" + ] + } + }, + "additionalProperties": false, + "description": "What your payment processor tells you about the card." + }, + "billing_country": { + "type": "string", + "pattern": "^[A-Z]{2}$", + "description": "ISO 3166-1 alpha-2, upper-case (`GB`)." + }, + "shipping_country": { + "type": "string", + "pattern": "^[A-Z]{2}$", + "description": "ISO 3166-1 alpha-2, upper-case (`GB`)." + } + }, + "required": [ + "amount", + "currency" + ], + "additionalProperties": false, + "description": "Only with `checkout` events: what is being paid for, and with which card." + }, + "RiskAssessmentList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RiskAssessment" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ + { + "object": "risk_assessment", + "id": "rsk_01k7c8m4q2f3e9v6t5r1n0p8yz", + "event": "signup", + "event_name": null, + "decision": "review", + "score": 0.62, + "reasons": [ + { + "code": "email.typo", + "message": "The domain looks like a typo of gmail.com.", + "suggestion": "gmail.com" + }, + { + "code": "velocity.signups_per_ip_1h", + "message": "7 sign-ups from this IP address in the last hour.", + "value": 7, + "threshold": 5, + "window": "1h" + } + ], + "signals": { + "email": { + "domain": "gmail.con", + "valid": true, + "disposable": false, + "deliverability": "deliverable", + "role_account": false, + "free_provider": false, + "suggestion": "gmail.com", + "domain_age_days": 9 + }, + "ip": { + "version": 4, + "private": false, + "tor": false, + "hosting": false, + "hosting_provider": null, + "hosting_kind": null, + "asn": 64500, + "as_name": "Example Broadband", + "country": "GB", + "privacy": null + }, + "velocity": { + "ip_1h": 7, + "ip_24h": 9, + "subnet_1h": 7, + "mailbox_24h": 1 + }, + "captcha": { + "provided": true, + "passed": true + }, + "login": null, + "content": null, + "checkout": null, + "device": null + }, + "user": { + "id": null, + "email": "ada@gmail.con" + }, + "ip": "203.0.113.7", + "degraded": [], + "outcome": null, + "review_status": "open", + "created_at": "2026-01-15T09:30:00.000Z" + } + ], + "has_more": false + } + ] + }, + "CreateRiskFeedbackRequest": { + "type": "object", + "properties": { + "outcome": { + "$ref": "#/components/schemas/RiskOutcome" + }, + "note": { + "type": "string", + "maxLength": 500, + "description": "For your team; never shared." + } + }, + "required": [ + "outcome" + ], + "additionalProperties": false, + "examples": [ + { + "outcome": "fraud" + } + ] + }, + "RiskListEntry": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_list_entry" + ] + }, + "id": { + "type": "string" + }, + "list": { + "$ref": "#/components/schemas/RiskList" + }, + "type": { + "$ref": "#/components/schemas/RiskListEntryType" + }, + "value": { + "type": "string", + "description": "Normalized: emails as their mailbox, CIDR ranges canonical, ASNs as numbers." + }, + "note": { + "type": [ + "string", + "null" + ], + "description": "Your note about it. `null`: none." + }, + "expires_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When it stops matching. `null`: never." + }, + "created_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "object", + "id", + "list", + "type", + "value", + "note", + "expires_at", + "created_at" + ], + "examples": [ + { + "object": "risk_list_entry", + "id": "rskle_01k7c8m4q2f3e9v6t5r1n0p8yz", + "list": "block", + "type": "domain", + "value": "evil.example", + "note": "Card testing", + "expires_at": null, + "created_at": "2026-01-15T09:30:00.000Z" + } + ] + }, + "RiskList": { + "type": "string", + "enum": [ + "allow", + "block" + ], + "description": "Which list: `allow` or `block`." + }, + "RiskListEntryType": { + "type": "string", + "enum": [ + "email", + "domain", + "ip", + "asn", + "country", + "user" + ], + "description": "`email` (matched by mailbox, dots and `+tags` folded), `domain` (subdomains included), `ip` (an address or CIDR range), `asn`, `country` (ISO 3166-1 alpha-2), `user` (your user id)." + }, + "CreateRiskListEntryRequest": { + "type": "object", + "properties": { + "list": { + "$ref": "#/components/schemas/RiskList" + }, + "type": { + "$ref": "#/components/schemas/RiskListEntryType" + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 320, + "description": "What to match: the address, domain, IP, CIDR range, ASN or other value of its `type`." + }, + "note": { + "type": "string", + "maxLength": 500, + "description": "Your note about it." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "The entry stops matching at this time (it is kept until deleted)." + } + }, + "required": [ + "list", + "type", + "value" + ], + "additionalProperties": false, + "examples": [ + { + "list": "block", + "type": "domain", + "value": "evil.example", + "note": "Card testing" + } + ] + }, + "RiskListEntryList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RiskListEntry" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ + { + "object": "risk_list_entry", + "id": "rskle_01k7c8m4q2f3e9v6t5r1n0p8yz", + "list": "block", + "type": "domain", + "value": "evil.example", + "note": "Card testing", + "expires_at": null, + "created_at": "2026-01-15T09:30:00.000Z" + } + ], + "has_more": false + } + ] + }, + "DeletedRiskListEntry": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_list_entry" + ] + }, + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "object", + "id", + "deleted" + ] + }, + "ForgottenRiskUser": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_user" + ] + }, + "id": { + "type": "string", + "description": "Your user id." + }, + "deleted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "object", + "id", + "deleted" + ], + "examples": [ + { + "object": "risk_user", + "id": "u_42", + "deleted": true + } + ] + }, + "RiskSettings": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_settings" + ] + }, + "review_threshold": { + "type": "number", + "description": "Scores at or above it are `review`." + }, + "block_threshold": { + "type": "number", + "description": "Scores at or above it are `block`." + }, + "disabled_reasons": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email.invalid", + "email.disposable", + "email.undeliverable", + "email.role_account", + "email.typo", + "email.domain_new", + "ip.tor", + "ip.vpn_network", + "ip.hosting_provider", + "ip.vpn", + "ip.proxy", + "ip.relay", + "ip.private", + "velocity.signups_per_ip_1h", + "velocity.signups_per_ip_24h", + "velocity.signups_per_subnet_1h", + "velocity.signups_per_email_domain_1h", + "velocity.signups_per_mailbox_24h", + "velocity.signups_per_device_24h", + "velocity.checkouts_per_ip_1h", + "velocity.checkouts_per_user_1h", + "velocity.checkouts_per_card_24h", + "velocity.logins_per_ip_1h", + "velocity.logins_per_user_1h", + "velocity.events_per_ip_1h", + "velocity.events_per_user_1h", + "duplicate.canonical_mailbox", + "duplicate.device", + "history.fraud_mailbox", + "history.fraud_user", + "history.fraud_ip", + "history.fraud_subnet", + "history.fraud_card", + "history.fraud_device", + "checkout.card_country_mismatch", + "checkout.billing_country_mismatch", + "checkout.prepaid_card", + "device.automation", + "device.invalid", + "login.new_country", + "login.new_network", + "captcha.failed", + "captcha.missing" + ] + }, + "description": "Reasons that never fire in this environment (they don't count in the score)." + }, + "captcha": { + "anyOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "turnstile", + "hcaptcha" + ], + "description": "The captcha provider." + }, + "required": { + "type": "boolean", + "description": "Whether assessments without a token get `captcha.missing`." + } + }, + "required": [ + "provider", + "required" + ] + }, + { + "type": "null" + } + ], + "description": "The captcha provider assessments verify tokens with (never the secret)." + }, + "content_thresholds": { + "type": "object", + "properties": { + "spam": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "description": "The score at which `content.spam` fires. `null`: never." + }, + "phishing": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "description": "The score at which `content.phishing` fires. `null`: never." + }, + "malware": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "description": "The score at which `content.malware` fires. `null`: never." + }, + "fraud": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "description": "The score at which `content.fraud` fires. `null`: never." + } + }, + "required": [ + "spam", + "phishing", + "malware", + "fraud" + ], + "description": "Per content category, the score at which `content.` fires; `null`: never (the score is still in `signals.content` for your rules)." + }, + "reason_weights": { + "type": "object", + "properties": { + "email.invalid": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "email.disposable": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "email.undeliverable": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "email.role_account": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "email.typo": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "email.domain_new": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "ip.tor": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "ip.vpn_network": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "ip.hosting_provider": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "ip.vpn": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "ip.proxy": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "ip.relay": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "ip.private": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "velocity.signups_per_ip_1h": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "velocity.signups_per_ip_24h": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "velocity.signups_per_subnet_1h": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "velocity.signups_per_email_domain_1h": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "velocity.signups_per_mailbox_24h": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "velocity.signups_per_device_24h": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "velocity.checkouts_per_ip_1h": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "velocity.checkouts_per_user_1h": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "velocity.checkouts_per_card_24h": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "velocity.logins_per_ip_1h": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "velocity.logins_per_user_1h": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "velocity.events_per_ip_1h": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "velocity.events_per_user_1h": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "duplicate.canonical_mailbox": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "duplicate.device": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "history.fraud_mailbox": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "history.fraud_user": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "history.fraud_ip": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "history.fraud_subnet": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "history.fraud_card": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "history.fraud_device": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "checkout.card_country_mismatch": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "checkout.billing_country_mismatch": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "checkout.prepaid_card": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "device.automation": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "device.invalid": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "login.new_country": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "login.new_network": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "captcha.failed": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "captcha.missing": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "content.spam": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "content.phishing": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "content.malware": { + "type": "number", + "description": "This reason's weight in this environment." + }, + "content.fraud": { + "type": "number", + "description": "This reason's weight in this environment." + } + }, + "description": "Reasons whose weight this environment changed (from calibration or by hand); the rest weigh what the reason catalog says." + }, + "updated_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "`null` while on the defaults." + } + }, + "required": [ + "object", + "review_threshold", + "block_threshold", + "disabled_reasons", + "captcha", + "content_thresholds", + "reason_weights", + "updated_at" + ], + "examples": [ + { + "object": "risk_settings", + "review_threshold": 0.5, + "block_threshold": 0.85, + "disabled_reasons": [ + "email.role_account" + ], + "captcha": { + "provider": "turnstile", + "required": true + }, + "content_thresholds": { + "spam": null, + "phishing": 0.8, + "malware": 0.8, + "fraud": 0.9 + }, + "reason_weights": { + "ip.hosting_provider": 0.1 + }, + "updated_at": "2026-01-15T09:30:00.000Z" + } + ] + }, + "UpdateRiskSettingsRequest": { + "type": "object", + "properties": { + "review_threshold": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 1, + "description": "Scores at or above it are `review`." + }, + "block_threshold": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 1, + "description": "Scores at or above it are `block`." + }, + "disabled_reasons": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email.invalid", + "email.disposable", + "email.undeliverable", + "email.role_account", + "email.typo", + "email.domain_new", + "ip.tor", + "ip.vpn_network", + "ip.hosting_provider", + "ip.vpn", + "ip.proxy", + "ip.relay", + "ip.private", + "velocity.signups_per_ip_1h", + "velocity.signups_per_ip_24h", + "velocity.signups_per_subnet_1h", + "velocity.signups_per_email_domain_1h", + "velocity.signups_per_mailbox_24h", + "velocity.signups_per_device_24h", + "velocity.checkouts_per_ip_1h", + "velocity.checkouts_per_user_1h", + "velocity.checkouts_per_card_24h", + "velocity.logins_per_ip_1h", + "velocity.logins_per_user_1h", + "velocity.events_per_ip_1h", + "velocity.events_per_user_1h", + "duplicate.canonical_mailbox", + "duplicate.device", + "history.fraud_mailbox", + "history.fraud_user", + "history.fraud_ip", + "history.fraud_subnet", + "history.fraud_card", + "history.fraud_device", + "checkout.card_country_mismatch", + "checkout.billing_country_mismatch", + "checkout.prepaid_card", + "device.automation", + "device.invalid", + "login.new_country", + "login.new_network", + "captcha.failed", + "captcha.missing" + ] + }, + "maxItems": 100, + "description": "Reasons that never fire in this environment. Replaces the list." + }, + "captcha": { + "anyOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "turnstile", + "hcaptcha" + ], + "description": "The captcha provider." + }, + "secret": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "description": "The provider's secret key. Required when setting a provider for the first time or changing it; stored encrypted and never returned." + }, + "required": { + "type": "boolean", + "description": "Assessments without a `captcha` token get `captcha.missing`. Default false." + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ], + "description": "`null` turns captcha verification off and deletes the secret." + }, + "content_thresholds": { + "type": "object", + "properties": { + "spam": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ], + "description": "The score at which `content.spam` fires. `null` removes it." + }, + "phishing": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ], + "description": "The score at which `content.phishing` fires. `null` removes it." + }, + "malware": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ], + "description": "The score at which `content.malware` fires. `null` removes it." + }, + "fraud": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ], + "description": "The score at which `content.fraud` fires. `null` removes it." + } + }, + "additionalProperties": false, + "description": "Changes the categories given: a score in (0, 1] sets the threshold, `null` removes it." + }, + "reason_weights": { + "type": "object", + "properties": { + "email.invalid": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "email.disposable": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "email.undeliverable": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "email.role_account": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "email.typo": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "email.domain_new": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "ip.tor": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "ip.vpn_network": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "ip.hosting_provider": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "ip.vpn": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "ip.proxy": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "ip.relay": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "ip.private": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "velocity.signups_per_ip_1h": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "velocity.signups_per_ip_24h": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "velocity.signups_per_subnet_1h": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "velocity.signups_per_email_domain_1h": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "velocity.signups_per_mailbox_24h": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "velocity.signups_per_device_24h": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "velocity.checkouts_per_ip_1h": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "velocity.checkouts_per_user_1h": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "velocity.checkouts_per_card_24h": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "velocity.logins_per_ip_1h": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "velocity.logins_per_user_1h": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "velocity.events_per_ip_1h": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "velocity.events_per_user_1h": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "duplicate.canonical_mailbox": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "duplicate.device": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "history.fraud_mailbox": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "history.fraud_user": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "history.fraud_ip": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "history.fraud_subnet": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "history.fraud_card": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "history.fraud_device": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "checkout.card_country_mismatch": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "checkout.billing_country_mismatch": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, { "type": "null" } ], "description": "This reason's new weight. `null` restores its default." }, - "email.typo": { + "checkout.prepaid_card": { "anyOf": [ { "type": "number", @@ -35129,7 +38071,7 @@ ], "description": "This reason's new weight. `null` restores its default." }, - "email.domain_new": { + "device.automation": { "anyOf": [ { "type": "number", @@ -35142,7 +38084,7 @@ ], "description": "This reason's new weight. `null` restores its default." }, - "ip.tor": { + "device.invalid": { "anyOf": [ { "type": "number", @@ -35155,7 +38097,7 @@ ], "description": "This reason's new weight. `null` restores its default." }, - "ip.vpn_network": { + "login.new_country": { "anyOf": [ { "type": "number", @@ -35168,7 +38110,7 @@ ], "description": "This reason's new weight. `null` restores its default." }, - "ip.hosting_provider": { + "login.new_network": { "anyOf": [ { "type": "number", @@ -35181,7 +38123,7 @@ ], "description": "This reason's new weight. `null` restores its default." }, - "ip.vpn": { + "captcha.failed": { "anyOf": [ { "type": "number", @@ -35194,7 +38136,59 @@ ], "description": "This reason's new weight. `null` restores its default." }, - "ip.proxy": { + "captcha.missing": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "content.spam": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "content.phishing": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "content.malware": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 0.99 + }, + { + "type": "null" + } + ], + "description": "This reason's new weight. `null` restores its default." + }, + "content.fraud": { "anyOf": [ { "type": "number", @@ -35206,2038 +38200,7125 @@ } ], "description": "This reason's new weight. `null` restores its default." + } + }, + "description": "Changes the reasons given: a weight in [0, 0.99] replaces the default, `null` goes back to it." + } + }, + "additionalProperties": false, + "examples": [ + { + "block_threshold": 0.9, + "captcha": { + "provider": "turnstile", + "secret": "0x4AAA…", + "required": true + } + } + ] + }, + "RiskCalibration": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_calibration" + ] + }, + "window_days": { + "type": "integer", + "description": "How many days back it looks." + }, + "assessments": { + "type": "integer", + "description": "Assessments in the window." + }, + "labeled": { + "type": "integer", + "description": "Of those, with feedback." + }, + "reasons": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RiskCalibrationReason" + }, + "description": "How each reason did against your feedback." + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RiskCalibrationRule" + }, + "description": "How each rule did against your feedback." + } + }, + "required": [ + "object", + "window_days", + "assessments", + "labeled", + "reasons", + "rules" + ], + "description": "Feedback against the reasons and rules that fired, over the last 90 days. Feedback you give mostly on reviews and blocks over-represents those: read precision as a guide, not a measurement.", + "examples": [ + { + "object": "risk_calibration", + "window_days": 90, + "assessments": 1840, + "labeled": 212, + "reasons": [ + { + "code": "ip.hosting_provider", + "fired": 96, + "fraud": 4, + "legitimate": 38, + "precision": 0.095, + "default_weight": 0.25, + "weight": 0.25, + "suggested_weight": 0.16 + } + ], + "rules": [ + { + "rule": "rskrule_01k7c8m4q2f3e9v6t5r1n0p8yz", + "name": "Disposable email from a data center", + "matched": 31, + "fraud": 12, + "legitimate": 1, + "precision": 0.923 + } + ] + } + ] + }, + "RiskCalibrationReason": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "email.invalid", + "email.disposable", + "email.undeliverable", + "email.role_account", + "email.typo", + "email.domain_new", + "ip.tor", + "ip.vpn_network", + "ip.hosting_provider", + "ip.vpn", + "ip.proxy", + "ip.relay", + "ip.private", + "velocity.signups_per_ip_1h", + "velocity.signups_per_ip_24h", + "velocity.signups_per_subnet_1h", + "velocity.signups_per_email_domain_1h", + "velocity.signups_per_mailbox_24h", + "velocity.signups_per_device_24h", + "velocity.checkouts_per_ip_1h", + "velocity.checkouts_per_user_1h", + "velocity.checkouts_per_card_24h", + "velocity.logins_per_ip_1h", + "velocity.logins_per_user_1h", + "velocity.events_per_ip_1h", + "velocity.events_per_user_1h", + "duplicate.canonical_mailbox", + "duplicate.device", + "history.fraud_mailbox", + "history.fraud_user", + "history.fraud_ip", + "history.fraud_subnet", + "history.fraud_card", + "history.fraud_device", + "checkout.card_country_mismatch", + "checkout.billing_country_mismatch", + "checkout.prepaid_card", + "device.automation", + "device.invalid", + "login.new_country", + "login.new_network", + "captcha.failed", + "captcha.missing", + "content.spam", + "content.phishing", + "content.malware", + "content.fraud" + ], + "description": "The reason's code." + }, + "fired": { + "type": "integer", + "description": "Assessments it fired in." + }, + "fraud": { + "type": "integer", + "description": "With feedback `fraud` or `abuse`." + }, + "legitimate": { + "type": "integer", + "description": "With feedback `legitimate`." + }, + "precision": { + "type": [ + "number", + "null" + ], + "description": "`fraud / (fraud + legitimate)`: how often it was right when you told coss.com. `null` without feedback." + }, + "default_weight": { + "type": "number", + "description": "Its weight unless this environment changes it." + }, + "weight": { + "type": "number", + "description": "Its weight in this environment now." + }, + "suggested_weight": { + "type": [ + "number", + "null" + ], + "description": "The weight feedback supports: its precision, pulled toward the default while there is little feedback. `null` below 20 assessments with feedback." + } + }, + "required": [ + "code", + "fired", + "fraud", + "legitimate", + "precision", + "default_weight", + "weight", + "suggested_weight" + ] + }, + "RiskCalibrationRule": { + "type": "object", + "properties": { + "rule": { + "type": "string", + "description": "The rule's id." + }, + "name": { + "type": "string", + "description": "The rule's name." + }, + "matched": { + "type": "integer", + "description": "Assessments it decided." + }, + "fraud": { + "type": "integer", + "description": "With feedback `fraud` or `abuse`." + }, + "legitimate": { + "type": "integer", + "description": "With feedback `legitimate`." + }, + "precision": { + "type": [ + "number", + "null" + ], + "description": "`fraud / (fraud + legitimate)`: how often it was right when you told coss.com. `null` without feedback." + } + }, + "required": [ + "rule", + "name", + "matched", + "fraud", + "legitimate", + "precision" + ] + }, + "RiskRule": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_rule" + ] + }, + "id": { + "type": "string" + }, + "name": { + "type": "string", + "description": "Your name for the rule, returned with reasons when it decides." + }, + "enabled": { + "type": "boolean", + "description": "`false`: it's kept, and no assessment applies it." + }, + "position": { + "type": "integer", + "description": "Rules are tried in ascending order." + }, + "events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RiskEvent" + }, + "description": "Events it applies to; empty: every event." + }, + "match": { + "type": "string", + "enum": [ + "all", + "any" + ], + "description": "`all`: every condition must hold. `any`: one is enough." + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RiskRuleCondition" + }, + "description": "What must hold, by `match`." + }, + "action": { + "$ref": "#/components/schemas/RiskDecision" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "object", + "id", + "name", + "enabled", + "position", + "events", + "match", + "conditions", + "action", + "created_at", + "updated_at" + ], + "examples": [ + { + "object": "risk_rule", + "id": "rskrule_01k7c8m4q2f3e9v6t5r1n0p8yz", + "name": "Disposable email from a data center", + "enabled": true, + "position": 0, + "events": [ + "signup" + ], + "match": "all", + "conditions": [ + { + "field": "email.disposable", + "operator": "is", + "value": true + }, + { + "field": "ip.hosting", + "operator": "is", + "value": true + } + ], + "action": "block", + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z" + } + ] + }, + "RiskRuleCondition": { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": [ + "email.valid", + "email.disposable", + "email.undeliverable", + "email.role_account", + "email.free_provider", + "email.typo", + "email.domain_new", + "email.domain", + "email.domain_age_days", + "ip.tor", + "ip.hosting", + "ip.vpn", + "ip.proxy", + "ip.relay", + "ip.private", + "ip.country", + "ip.asn", + "ip.hosting_provider", + "captcha.passed", + "duplicate.canonical_mailbox", + "duplicate.device", + "history.fraud", + "login.new_country", + "login.new_network", + "content.spam", + "content.phishing", + "content.malware", + "content.fraud", + "checkout.amount", + "checkout.currency", + "checkout.card_country", + "checkout.card_funding", + "checkout.billing_country", + "checkout.shipping_country", + "device.automation", + "device.timezone", + "device.language", + "score", + "velocity.ip_1h", + "velocity.ip_24h", + "velocity.subnet_1h", + "velocity.subnet_24h", + "velocity.email_domain_1h", + "velocity.email_domain_24h", + "velocity.mailbox_1h", + "velocity.mailbox_24h", + "velocity.user_1h", + "velocity.user_24h", + "velocity.card_1h", + "velocity.card_24h", + "velocity.device_1h", + "velocity.device_24h" + ], + "description": "The signal to test, such as `email.disposable` or `ip.country`." + }, + "operator": { + "type": "string", + "enum": [ + "is", + "eq", + "neq", + "gt", + "gte", + "lt", + "lte", + "in", + "not_in" + ], + "description": "How to compare. Booleans use `is`, numbers `eq` to `lte`, and strings `eq`, `neq`, `in` and `not_in`." + }, + "value": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string", + "maxLength": 320 + }, + { + "type": "array", + "items": { + "type": "string", + "maxLength": 320 + }, + "minItems": 1, + "maxItems": 100 + } + ], + "description": "What to compare it with: its type follows the field, and `in` and `not_in` take a list of strings." + } + }, + "required": [ + "field", + "operator", + "value" + ], + "additionalProperties": false, + "description": "A test of one field. A field without a value (no IP, no email…) never matches, whatever the operator." + }, + "CreateRiskRuleRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Your name for the rule, returned with reasons when it decides." + }, + "enabled": { + "type": "boolean", + "description": "Default `true`." + }, + "position": { + "type": "integer", + "minimum": 0, + "maximum": 10000, + "description": "Default 0." + }, + "events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RiskEvent" + }, + "maxItems": 5, + "description": "Events it applies to. Default: every event." + }, + "match": { + "type": "string", + "enum": [ + "all", + "any" + ], + "description": "Default `all`." + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RiskRuleCondition" + }, + "minItems": 1, + "maxItems": 20, + "description": "What must hold, by `match`." + }, + "action": { + "$ref": "#/components/schemas/RiskDecision" + } + }, + "required": [ + "name", + "conditions", + "action" + ], + "additionalProperties": false, + "examples": [ + { + "name": "Disposable email from a data center", + "events": [ + "signup" + ], + "conditions": [ + { + "field": "email.disposable", + "operator": "is", + "value": true }, - "ip.relay": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } + { + "field": "ip.hosting", + "operator": "is", + "value": true + } + ], + "action": "block" + } + ] + }, + "RiskRuleList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RiskRule" + } + }, + "has_more": { + "type": "boolean", + "enum": [ + false + ] + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "description": "Every rule of the environment (at most 50), in evaluation order.", + "examples": [ + { + "object": "list", + "data": [ + { + "object": "risk_rule", + "id": "rskrule_01k7c8m4q2f3e9v6t5r1n0p8yz", + "name": "Disposable email from a data center", + "enabled": true, + "position": 0, + "events": [ + "signup" ], - "description": "This reason's new weight. `null` restores its default." - }, - "ip.private": { - "anyOf": [ + "match": "all", + "conditions": [ { - "type": "number", - "minimum": 0, - "maximum": 0.99 + "field": "email.disposable", + "operator": "is", + "value": true }, { - "type": "null" + "field": "ip.hosting", + "operator": "is", + "value": true } ], - "description": "This reason's new weight. `null` restores its default." + "action": "block", + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z" + } + ], + "has_more": false + } + ] + }, + "UpdateRiskRuleRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Your name for the rule." + }, + "enabled": { + "type": "boolean", + "description": "`false` keeps it, and stops applying it." + }, + "position": { + "type": "integer", + "minimum": 0, + "maximum": 10000, + "description": "Rules are tried in ascending order." + }, + "events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RiskEvent" + }, + "maxItems": 5, + "description": "Events it applies to. Empty: every event." + }, + "match": { + "type": "string", + "enum": [ + "all", + "any" + ], + "description": "`all`: every condition must hold. `any`: one is enough." + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RiskRuleCondition" + }, + "minItems": 1, + "maxItems": 20, + "description": "What must hold, by `match`." + }, + "action": { + "$ref": "#/components/schemas/RiskDecision" + } + }, + "additionalProperties": false, + "examples": [ + { + "enabled": false + } + ] + }, + "DeletedRiskRule": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_rule" + ] + }, + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "object", + "id", + "deleted" + ] + }, + "RiskEmailCheck": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_email_check" + ] + }, + "email": { + "type": "string", + "description": "The address checked." + }, + "valid": { + "type": "boolean", + "description": "Syntactically valid (the rest assumes it is)." + }, + "domain": { + "type": [ + "string", + "null" + ], + "description": "Its domain. `null` when it isn't valid." + }, + "canonical": { + "type": [ + "string", + "null" + ], + "description": "The mailbox with the provider's aliasing folded (dots and `+tags` at Gmail…): two addresses with the same canonical form reach one inbox." + }, + "disposable": { + "type": "boolean", + "description": "Whether its domain is a known burner service." + }, + "deliverability": { + "type": "string", + "enum": [ + "deliverable", + "undeliverable", + "unknown" + ], + "description": "From DNS (MX, A, AAAA). `unknown` when the lookup failed or timed out." + }, + "role_account": { + "type": "boolean", + "description": "Whether it's a role mailbox, like `info@`." + }, + "free_provider": { + "type": "boolean", + "description": "Whether its domain is a free mailbox provider." + }, + "suggestion": { + "type": [ + "string", + "null" + ], + "description": "The likely intended domain when this one looks like a typo of a popular one." + }, + "domain_age_days": { + "type": [ + "integer", + "null" + ], + "description": "Days since the domain was registered (RDAP); `null` when unknown or not looked up (free providers, disposable domains)." + }, + "degraded": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "email_dns", + "domain_age" + ] + }, + "description": "Lookups that failed or timed out (their fields are `unknown`/`null`)." + } + }, + "required": [ + "object", + "email", + "valid", + "domain", + "canonical", + "disposable", + "deliverability", + "role_account", + "free_provider", + "suggestion", + "domain_age_days", + "degraded" + ], + "examples": [ + { + "object": "risk_email_check", + "email": "ada@gnail.com", + "valid": true, + "domain": "gnail.com", + "canonical": "ada@gnail.com", + "disposable": false, + "deliverability": "deliverable", + "role_account": false, + "free_provider": false, + "suggestion": "gmail.com", + "domain_age_days": 7300, + "degraded": [] + } + ] + }, + "RiskEmailCheckRequest": { + "type": "object", + "properties": { + "email": { + "type": "string", + "minLength": 1, + "maxLength": 320, + "description": "The address to check, as given." + } + }, + "required": [ + "email" + ], + "additionalProperties": false, + "examples": [ + { + "email": "ada@gnail.com" + } + ] + }, + "RiskIpCheck": { + "type": "object", + "properties": { + "version": { + "anyOf": [ + { + "type": "number", + "enum": [ + 4 + ] }, - "velocity.signups_per_ip_1h": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + { + "type": "number", + "enum": [ + 6 + ] + } + ], + "description": "IPv4 or IPv6." + }, + "private": { + "type": "boolean", + "description": "Whether it's a private or reserved address." + }, + "tor": { + "type": "boolean", + "description": "Whether it's a Tor exit." + }, + "hosting": { + "type": "boolean", + "description": "Whether it's in a cloud, hosting or VPN network." + }, + "hosting_provider": { + "type": [ + "string", + "null" + ], + "description": "That network's operator. `null` when it isn't one." + }, + "hosting_kind": { + "type": [ + "string", + "null" + ], + "enum": [ + "cloud", + "hosting", + "vpn", + null + ], + "description": "`cloud`, `hosting` or `vpn`. `null` when it isn't one." + }, + "asn": { + "type": [ + "integer", + "null" + ], + "description": "The network's autonomous system number. `null` when unknown." + }, + "as_name": { + "type": [ + "string", + "null" + ], + "description": "The network's name. `null` when unknown." + }, + "country": { + "type": [ + "string", + "null" + ], + "description": "ISO 3166-1 alpha-2." + }, + "privacy": { + "anyOf": [ + { + "$ref": "#/components/schemas/RiskIpPrivacy" }, - "velocity.signups_per_ip_24h": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + { + "type": "null" + } + ], + "description": "From coss.com's IP privacy data provider (VPNs, proxies, relays). `null` when it isn't available or was not asked (private addresses)." + }, + "object": { + "type": "string", + "enum": [ + "risk_ip_check" + ] + }, + "ip": { + "type": "string", + "description": "The address checked." + }, + "degraded": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ip_data", + "ip_privacy" + ] + }, + "description": "`ip_data` when some IP data (Tor exits, cloud ranges, networks and countries) is unavailable at the moment; `ip_privacy` when the privacy data provider failed or timed out." + } + }, + "required": [ + "version", + "private", + "tor", + "hosting", + "hosting_provider", + "hosting_kind", + "asn", + "as_name", + "country", + "privacy", + "object", + "ip", + "degraded" + ], + "examples": [ + { + "object": "risk_ip_check", + "ip": "185.220.101.1", + "version": 4, + "private": false, + "tor": true, + "hosting": false, + "hosting_provider": null, + "hosting_kind": null, + "asn": 60729, + "as_name": "Stiftung Erneuerbare Freiheit", + "country": "DE", + "privacy": null, + "degraded": [] + } + ] + }, + "RiskIpCheckRequest": { + "type": "object", + "properties": { + "ip": { + "anyOf": [ + { + "type": "string", + "format": "ip" }, - "velocity.signups_per_subnet_1h": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + { + "type": "string", + "format": "ip" + } + ], + "description": "The address to check." + } + }, + "required": [ + "ip" + ], + "additionalProperties": false, + "examples": [ + { + "ip": "185.220.101.1" + } + ] + }, + "RiskPhoneCheck": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_phone_check" + ] + }, + "phone": { + "type": "string", + "description": "The number in E.164, or as given when it isn't." + }, + "valid": { + "type": "boolean", + "description": "A possible number in its country's numbering plan. When the lookup failed (`degraded`), only the E.164 syntax was checked." + }, + "country": { + "type": [ + "string", + "null" + ], + "description": "ISO 3166-1 alpha-2." + }, + "line_type": { + "type": [ + "string", + "null" + ], + "enum": [ + "mobile", + "landline", + "fixed_voip", + "non_fixed_voip", + "toll_free", + "premium", + "shared_cost", + "personal", + "uan", + "voicemail", + "pager", + "unknown", + null + ], + "description": "`fixed_voip` numbers belong to an address (cable, business lines); `non_fixed_voip` ones can be had online in minutes (Google Voice, app numbers), the kind used for throwaway accounts. `null` when it couldn't be looked up." + }, + "carrier": { + "anyOf": [ + { + "$ref": "#/components/schemas/RiskPhoneCarrier" }, - "velocity.signups_per_email_domain_1h": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + { + "type": "null" + } + ], + "description": "The network the number is on now (after any port). `null` when unknown, and for toll-free, premium and personal numbers." + }, + "sim_swap": { + "anyOf": [ + { + "$ref": "#/components/schemas/RiskPhoneSimSwap" }, - "velocity.signups_per_mailbox_24h": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + { + "type": "null" + } + ], + "description": "The mobile number's last SIM change. `null` where coss.com doesn't have that data (not every country and carrier)." + }, + "degraded": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "phone_data", + "sim_swap" + ] + }, + "description": "`phone_data` when the lookup failed or timed out (or had no line type for the number); `sim_swap` when the carrier didn't answer about the SIM." + } + }, + "required": [ + "object", + "phone", + "valid", + "country", + "line_type", + "carrier", + "sim_swap", + "degraded" + ], + "examples": [ + { + "object": "risk_phone_check", + "phone": "+14155550123", + "valid": true, + "country": "US", + "line_type": "mobile", + "carrier": { + "name": "T-Mobile USA, Inc.", + "mobile_country_code": "310", + "mobile_network_code": "260" + }, + "sim_swap": null, + "degraded": [] + } + ] + }, + "RiskPhoneCarrier": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ] + }, + "mobile_country_code": { + "type": [ + "string", + "null" + ] + }, + "mobile_network_code": { + "type": [ + "string", + "null" + ], + "description": "Only for mobile numbers." + } + }, + "required": [ + "name", + "mobile_country_code", + "mobile_network_code" + ] + }, + "RiskPhoneSimSwap": { + "type": "object", + "properties": { + "recent": { + "type": "boolean", + "description": "The SIM changed within `period`: someone may have taken the number over." + }, + "swapped_at": { + "type": [ + "string", + "null" + ], + "description": "The last SIM change, where the carrier tells the date." + }, + "period": { + "type": [ + "string", + "null" + ], + "description": "What `recent` means, as an ISO 8601 duration (`PT48H`)." + } + }, + "required": [ + "recent", + "swapped_at", + "period" + ] + }, + "RiskPhoneCheckRequest": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "description": "The number in E.164 (`+14155550123`); spaces, dashes, dots and brackets are ignored. Anything else is `valid: false`." + } + }, + "required": [ + "phone" + ], + "additionalProperties": false, + "examples": [ + { + "phone": "+14155550123" + } + ] + }, + "RiskContentClassification": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_content_classification" + ] + }, + "scores": { + "$ref": "#/components/schemas/RiskContentSignals" + }, + "truncated": { + "type": "boolean", + "description": "The text was longer than the model reads: only its beginning was classified." + } + }, + "required": [ + "object", + "scores", + "truncated" + ], + "description": "Scores only: coss.com never decides what they mean for you. Neither the text nor the scores are stored.", + "examples": [ + { + "object": "risk_content_classification", + "scores": { + "spam": 0.91, + "phishing": 0.04, + "malware": 0.01, + "fraud": 0.86 + }, + "truncated": false + } + ] + }, + "RiskContentClassifyRequest": { + "type": "object", + "properties": { + "text": { + "type": "string", + "minLength": 1, + "maxLength": 50000, + "description": "The text to classify (a profile bio, a comment, a listing). The model reads the first 8,000 characters." + } + }, + "required": [ + "text" + ], + "additionalProperties": false, + "examples": [ + { + "text": "Earn $5,000 a week from home! DM me on Telegram: t.me/fast-cash" + } + ] + }, + "RiskLimit": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_limit" + ] + }, + "bucket": { + "type": "string", + "description": "The bucket's name." + }, + "allowed": { + "type": "boolean", + "description": "Whether to let the request through. Denied requests take no tokens." + }, + "limit": { + "type": "integer", + "description": "The bucket's size (`burst`)." + }, + "remaining": { + "type": "integer", + "description": "Tokens left for this key." + }, + "reset_ms": { + "type": "integer", + "description": "Milliseconds until the key's bucket is full again." + }, + "retry_after_ms": { + "type": [ + "integer", + "null" + ], + "description": "When denied: milliseconds until the same request would be allowed; `null` when it never would (`cost` above the bucket's size). 0 when allowed." + }, + "degraded": { + "type": "boolean", + "description": "coss.com couldn't count (its store was unavailable): `allowed` is the bucket's `on_failure`." + } + }, + "required": [ + "object", + "bucket", + "allowed", + "limit", + "remaining", + "reset_ms", + "retry_after_ms", + "degraded" + ], + "examples": [ + { + "object": "risk_limit", + "bucket": "password_reset", + "allowed": true, + "limit": 5, + "remaining": 4, + "reset_ms": 720000, + "retry_after_ms": 0, + "degraded": false + } + ] + }, + "CheckRiskLimitRequest": { + "type": "object", + "properties": { + "bucket": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$", + "description": "The bucket's name." + }, + "key": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "description": "Who or what is limited: a user id, an email address, an IP address… coss.com keeps only a keyed hash of it." + }, + "cost": { + "type": "integer", + "minimum": 1, + "maximum": 1000000, + "description": "Tokens this request takes. Default 1." + } + }, + "required": [ + "bucket", + "key" + ], + "additionalProperties": false, + "examples": [ + { + "bucket": "password_reset", + "key": "ada@example.com" + } + ] + }, + "RiskLimitBucket": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_limit_bucket" + ] + }, + "id": { + "type": "string", + "examples": [ + "rsklb_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "name": { + "type": "string", + "description": "What `POST /v1/risk/limits` names it by." + }, + "limit": { + "type": "integer", + "description": "Tokens added per `period_seconds`." + }, + "period_seconds": { + "type": "integer", + "description": "How many seconds `limit` tokens take to be added." + }, + "burst": { + "type": "integer", + "description": "The bucket's size: how many requests a key can make at once." + }, + "on_failure": { + "type": "string", + "enum": [ + "allow", + "deny" + ], + "description": "The answer while coss.com can't count (its store is unavailable): `allow` (fail open) or `deny`." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "object", + "id", + "name", + "limit", + "period_seconds", + "burst", + "on_failure", + "created_at", + "updated_at" + ], + "examples": [ + { + "object": "risk_limit_bucket", + "id": "rsklb_01k7c8m4q2f3e9v6t5r1n0p8yz", + "name": "password_reset", + "limit": 5, + "period_seconds": 3600, + "burst": 5, + "on_failure": "allow", + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z" + } + ] + }, + "CreateRiskLimitBucketRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$", + "description": "Unique in the environment; can't be changed." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000, + "description": "Tokens added per `period_seconds`." + }, + "period_seconds": { + "type": "integer", + "minimum": 1, + "maximum": 86400, + "description": "1 second to 1 day." + }, + "burst": { + "type": "integer", + "minimum": 1, + "maximum": 1000000, + "description": "How many requests a key can make at once. Defaults to `limit`." + }, + "on_failure": { + "type": "string", + "enum": [ + "allow", + "deny" + ], + "description": "The answer while coss.com can't count. Defaults to `allow`." + } + }, + "required": [ + "name", + "limit", + "period_seconds" + ], + "additionalProperties": false, + "examples": [ + { + "name": "password_reset", + "limit": 5, + "period_seconds": 3600 + } + ] + }, + "RiskLimitBucketList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RiskLimitBucket" + } + }, + "has_more": { + "type": "boolean", + "enum": [ + false + ] + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "description": "Every bucket of the environment (at most 100), by name.", + "examples": [ + { + "object": "list", + "data": [ + { + "object": "risk_limit_bucket", + "id": "rsklb_01k7c8m4q2f3e9v6t5r1n0p8yz", + "name": "password_reset", + "limit": 5, + "period_seconds": 3600, + "burst": 5, + "on_failure": "allow", + "created_at": "2026-01-15T09:30:00.000Z", + "updated_at": "2026-01-15T09:30:00.000Z" + } + ], + "has_more": false + } + ] + }, + "UpdateRiskLimitBucketRequest": { + "type": "object", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000, + "description": "Tokens added per `period_seconds`." + }, + "period_seconds": { + "type": "integer", + "minimum": 1, + "maximum": 86400, + "description": "How many seconds `limit` tokens take to be added." + }, + "burst": { + "type": "integer", + "minimum": 1, + "maximum": 1000000, + "description": "The bucket's size: how many requests a key can make at once." + }, + "on_failure": { + "type": "string", + "enum": [ + "allow", + "deny" + ], + "description": "The answer while coss.com can't count: `allow` or `deny`." + } + }, + "additionalProperties": false, + "examples": [ + { + "limit": 10, + "burst": 20 + } + ] + }, + "DeletedRiskLimitBucket": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "risk_limit_bucket" + ] + }, + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "object", + "id", + "deleted" + ] + } + }, + "parameters": { + "CossVersion": { + "in": "header", + "name": "Coss-Version", + "required": false, + "description": "The API version (a date) to use for this request. Defaults to your organization's version, pinned to the latest one when it first used the API and upgraded from the dashboard. The SDK always sends the version it was built for.", + "schema": { + "type": "string", + "enum": [ + "2026-10-10" + ], + "examples": [ + "2026-10-10" + ] + } + } + } + }, + "paths": { + "/v1/environment": { + "get": { + "operationId": "getEnvironment", + "tags": [ + "Environment" + ], + "summary": "Retrieve the API key's environment", + "description": "Which environment, project and organization the API key belongs to, and the key itself. Any valid key can read its own environment (no permission needed): use it to check a key, show where it sends and what it may do.", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "The API key's environment.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "velocity.signups_per_device_24h": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Environment" + } + } + } + }, + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "velocity.checkouts_per_ip_1h": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + } + }, + "/v1/api_key": { + "delete": { + "operationId": "revokeApiKey", + "tags": [ + "Environment" + ], + "summary": "Revoke the API key", + "description": "Revokes the API key making this request, at once: every later request with it fails with `invalid_api_key`. `coss logout --revoke` uses it. Other keys are revoked in the dashboard.", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "The key was revoked.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "velocity.checkouts_per_user_1h": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedApiKey" + } + } + } + }, + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "velocity.checkouts_per_card_24h": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + } + }, + "/v1/emails": { + "post": { + "operationId": "sendEmail", + "tags": [ + "Emails" + ], + "summary": "Send an email", + "description": "Queues an email from the API key's environment and returns it with `status: \"queued\"`; it is handed to the delivery provider moments later. Follow it with `GET /v1/emails/{id}` or the `email.sent` / `email.failed` webhooks. Live (`coss_live_…`) keys must send from a verified domain of the organization. Sandbox (`coss_test_…`) keys accept any `from` and never deliver anything; the response has `sandbox: true`. Recipients on the environment's suppression list (for every email, or for the email's `category`) are skipped and get the status `suppressed`. With a future `scheduled_at` the email is stored with the status `scheduled` and sent at that time; the domain and the suppression list are checked again then. Attachments are base64-encoded; only metadata is kept after sending. Recipients count against the organization's send quota (per second and per day; sandbox environments have their own daily quota). With open or click tracking on (the environment's settings, or `tracking`), the HTML body is delivered with a tracking pixel and/or links rewritten to coss.com redirects; the stored `html` stays as sent.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Makes retries safe: a repeated request with the same key and body returns the original email (with its current status) instead of sending again, for 24 hours. 1-256 characters.", + "examples": [ + "welcome-user-123" + ] + }, + "required": false, + "description": "Makes retries safe: a repeated request with the same key and body returns the original email (with its current status) instead of sending again, for 24 hours. 1-256 characters.", + "name": "Idempotency-Key", + "in": "header" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendEmailRequest" + } + } + } + }, + "responses": { + "202": { + "description": "The email, accepted for delivery (`queued` or `scheduled`). A replay returns the original email with its current status.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "velocity.logins_per_ip_1h": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Idempotent-Replayed": { + "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", + "schema": { + "type": "string", + "enum": [ + "true" + ] + } }, - "velocity.logins_per_user_1h": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Email" + } + } + } + }, + "400": { + "description": "The body is not valid JSON (`invalid_json`) or the `Idempotency-Key` header is invalid (`invalid_idempotency_key`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "402": { + "description": "Live sending is paused for an overdue invoice or the spend limit (`billing_hold`), or the send would go past the plan's monthly allowance (`usage_limit_exceeded`) (`billing_error`). Never retried: it needs a plan change or a payment.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`insufficient_permissions`), its organization is suspended (`organization_suspended`), live sending is suspended (`account_suspended`), or sending is paused for the organization because of its sending reputation (`sending_paused`), or the `subaccount` is turned off (`subaccount_disabled`) or paused (`subaccount_paused`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Requests with this `Idempotency-Key` are racing for an expired key (`request_in_progress`). Retry after `Retry-After` seconds.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "velocity.events_per_ip_1h": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "The request body is larger than 40 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The body failed validation (`validation_error`, see `issues`), the `from` domain is not a verified domain of the organization (`domain_not_verified`, live keys only), the `Idempotency-Key` was already used with a different body (`idempotency_key_reused`), or the template can't be sent: it doesn't exist (`template_not_found`), has no published version (`template_not_published`) or no such version (`template_version_not_found`), the variables don't match it (`invalid_template_variables`, see `issues`), or it renders an invalid subject or empty body (`template_render_failed`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or the email's recipients exceed the send quota (`quota_exceeded`). Retry after `Retry-After` seconds; without `Retry-After` the email has more recipients than the quota allows at once.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "velocity.events_per_user_1h": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "get": { + "operationId": "listEmails", + "tags": [ + "Emails" + ], + "summary": "List emails", + "description": "Emails sent from the API key's environment, newest first, without their content (retrieve an email for `html`, `text`, `headers` and `attachments`). Filter by recipient (`to`), by the email's or a recipient's status, by category, batch or creation time. Pass the last `id` as `after` to get the next page while `has_more` is true.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 320, + "description": "Only emails with this recipient (in `to`, `cc` or `bcc`; the bare address, any case).", + "examples": [ + "user@example.com" + ] + }, + "required": false, + "description": "Only emails with this recipient (in `to`, `cc` or `bcc`; the bare address, any case).", + "name": "to", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Only emails with a recipient in one of these statuses, comma-separated: `bounced,complained`. With `to`, that recipient's status." + }, + "required": false, + "description": "Only emails with a recipient in one of these statuses, comma-separated: `bounced,complained`. With `to`, that recipient's status.", + "name": "recipient_status", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Only emails in these statuses, comma-separated: `failed`." + }, + "required": false, + "description": "Only emails in these statuses, comma-separated: `failed`.", + "name": "status", + "in": "query" + }, + { + "schema": { + "type": "string", + "maxLength": 256, + "description": "Free text, matched against the subject and the `to` addresses." + }, + "required": false, + "description": "Free text, matched against the subject and the `to` addresses.", + "name": "q", + "in": "query" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "Only emails of this category." + }, + "required": false, + "description": "Only emails of this category.", + "name": "category", + "in": "query" + }, + { + "schema": { + "type": "string", + "maxLength": 64, + "description": "Only the emails of this batch." + }, + "required": false, + "description": "Only the emails of this batch.", + "name": "batch_id", + "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Only the emails sent for this sub-account (its id or your uid)." + }, + "required": false, + "description": "Only the emails sent for this sub-account (its id or your uid).", + "name": "subaccount", + "in": "query" + }, + { + "schema": { + "type": "string", + "format": "date-time", + "description": "Only emails created at or after this time." + }, + "required": false, + "description": "Only emails created at or after this time.", + "name": "created_after", + "in": "query" + }, + { + "schema": { + "type": "string", + "format": "date-time", + "description": "Only emails created before this time." + }, + "required": false, + "description": "Only emails created before this time.", + "name": "created_before", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "1-100, default 20." + }, + "required": false, + "description": "1-100, default 20.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "maxLength": 64, + "description": "Cursor: the `id` of the last email of the previous page." + }, + "required": false, + "description": "Cursor: the `id` of the last email of the previous page.", + "name": "after", + "in": "query" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "A page of emails.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "duplicate.canonical_mailbox": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailSummaryList" + } + } + } + }, + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "duplicate.device": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/emails/batch": { + "post": { + "operationId": "sendEmailBatch", + "tags": [ + "Emails" + ], + "summary": "Send a batch of emails", + "description": "Queues up to 100 emails in one request. Each email is handled exactly as by `POST /v1/emails` (sender checks, suppressions, scheduling, attachments, tracking), but the batch is all or nothing: if any email is invalid, or any `from` is not a verified domain (live keys), nothing is stored and the error names the email (e.g. `emails.3.to`). One `Idempotency-Key` covers the whole batch. The emails' recipients are charged against the send quota together, so a batch with more recipients than the quota allows at once is refused (`quota_exceeded` without `Retry-After`): split it. The request body shares the 40 MB limit of a single send.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Makes retries safe: a repeated request with the same key and body returns the original email (with its current status) instead of sending again, for 24 hours. 1-256 characters.", + "examples": [ + "welcome-user-123" + ] + }, + "required": false, + "description": "Makes retries safe: a repeated request with the same key and body returns the original email (with its current status) instead of sending again, for 24 hours. 1-256 characters.", + "name": "Idempotency-Key", + "in": "header" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendEmailBatchRequest" + } + } + } + }, + "responses": { + "202": { + "description": "The batch, accepted for delivery, with its emails in request order. A replay returns the original batch with the emails' current statuses.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "history.fraud_mailbox": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Idempotent-Replayed": { + "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", + "schema": { + "type": "string", + "enum": [ + "true" + ] + } }, - "history.fraud_user": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailBatch" + } + } + } + }, + "400": { + "description": "The body is not valid JSON (`invalid_json`) or the `Idempotency-Key` header is invalid (`invalid_idempotency_key`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "402": { + "description": "Live sending is paused for an overdue invoice or the spend limit (`billing_hold`), or the send would go past the plan's monthly allowance (`usage_limit_exceeded`) (`billing_error`). Never retried: it needs a plan change or a payment.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`insufficient_permissions`), its organization is suspended (`organization_suspended`), live sending is suspended (`account_suspended`), sending is paused for the organization because of its sending reputation (`sending_paused`), or a `subaccount` is turned off (`subaccount_disabled`) or paused (`subaccount_paused`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Requests with this `Idempotency-Key` are racing for an expired key (`request_in_progress`). Retry after `Retry-After` seconds.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "history.fraud_ip": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "The request body is larger than 40 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The body failed validation (`validation_error`, see `issues`), a `from` domain is not a verified domain of the organization (`domain_not_verified`, live keys only; `param` names the email), the `Idempotency-Key` was already used with a different body (`idempotency_key_reused`), or an email's template can't be sent (the `template_…` codes of `POST /v1/emails`; `param` names the email).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or the batch's recipients exceed the send quota (`quota_exceeded`). Retry after `Retry-After` seconds; without `Retry-After` the batch has more recipients than the quota allows at once.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "history.fraud_subnet": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/emails/batch/{id}": { + "get": { + "operationId": "getEmailBatch", + "tags": [ + "Emails" + ], + "summary": "Retrieve a batch of emails", + "description": "A batch sent with `POST /v1/emails/batch`, with its emails' current statuses in the order of the request. Emails past the data retention period are left out. Only batches sent from the API key's own environment can be retrieved.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The batch's id (`batch_id` of its emails).", + "examples": [ + "batch_01k8d0n6x5f3w2e1r9t7y4u6i8" + ] + }, + "required": true, + "description": "The batch's id (`batch_id` of its emails).", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "The batch.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "history.fraud_card": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailBatch" + } + } + } + }, + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "No batch with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "history.fraud_device": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/emails/{id}": { + "get": { + "operationId": "getEmail", + "tags": [ + "Emails" + ], + "summary": "Retrieve an email", + "description": "Only emails sent from the API key's own environment can be retrieved.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The email's id.", + "examples": [ + "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The email's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "The email.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "checkout.card_country_mismatch": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Email" + } + } + } + }, + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "No email with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "checkout.billing_country_mismatch": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/emails/{id}/cancel": { + "post": { + "operationId": "cancelEmail", + "tags": [ + "Emails" + ], + "summary": "Cancel a scheduled email", + "description": "Cancels an email sent with a future `scheduled_at` before it is sent. Canceling an already canceled email returns it unchanged. To send it at another time, reschedule it instead.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The email's id.", + "examples": [ + "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The email's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "The canceled email.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "checkout.prepaid_card": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Email" + } + } + } + }, + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "No email with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The email is not scheduled (it is being sent, was sent or failed): `email_not_cancelable`.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "device.automation": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/emails/{id}/reschedule": { + "post": { + "operationId": "rescheduleEmail", + "tags": [ + "Emails" + ], + "summary": "Reschedule a scheduled email", + "description": "Moves an email sent with a future `scheduled_at` to a new time, at most 30 days from now, and records an `email.rescheduled` event. A time that is not in the future sends it within seconds. Rescheduling to the time it already has returns it unchanged. The domain and the suppression list are still checked when it is sent.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The email's id.", + "examples": [ + "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The email's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RescheduleEmailRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The rescheduled email.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "device.invalid": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Email" + } + } + } + }, + "400": { + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "No email with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The body failed validation (`validation_error`, see `issues`), or the email is not scheduled (it is being sent, was sent, failed or was canceled): `email_not_reschedulable`.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "login.new_country": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/emails/{id}/events": { + "get": { + "operationId": "listEmailEvents", + "tags": [ + "Emails" + ], + "summary": "List an email's delivery events", + "description": "Every delivery event of the email, oldest first: `email.scheduled`, `email.rescheduled`, `email.canceled`, `email.sent` and `email.failed` for the whole email, and `email.delivered`, `email.delivery_delayed`, `email.bounced`, `email.complained` and `email.suppressed` (skipped because of the suppression list) per recipient, plus `email.opened` and `email.clicked` when tracking is on (attributed to a recipient only when the email had a single one) and `email.unsubscribed` after a one-click unsubscribe.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The email's id.", + "examples": [ + "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The email's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "The email's events.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "login.new_network": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailEventList" + } + } + } + }, + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "No email with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "captcha.failed": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/emails/{id}/links": { + "get": { + "operationId": "listEmailLinks", + "tags": [ + "Emails" + ], + "summary": "List an email's tracked links", + "description": "The links coss.com rewrote for click tracking, in the order they appear in the HTML, with their click counts. Empty unless the email was sent with click tracking.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The email's id.", + "examples": [ + "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The email's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "The email's tracked links.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "captcha.missing": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailLinkList" + } + } + } + }, + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "No email with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "content.spam": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/email_templates": { + "post": { + "operationId": "createEmailTemplate", + "tags": [ + "Email templates" + ], + "summary": "Create an email template", + "description": "Creates a template with a draft holding its variables and a subject, HTML and text per locale. Sends use it once its draft is published. Not idempotent: retrying after a lost response can create a second template (unless it has an `alias`). Templates belong to the API key's own environment: a `coss_test_…` key only ever sees sandbox ones. Give a template the same `alias` in every environment to send it by that name.", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEmailTemplateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The template.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "content.phishing": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailTemplate" + } + } + } + }, + "400": { + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The body failed validation (`validation_error`), another template has the alias (`template_alias_taken`), or the environment has 500 templates (`template_limit_reached`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "content.malware": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, + "get": { + "operationId": "listEmailTemplates", + "tags": [ + "Email templates" + ], + "summary": "List email templates", + "description": "The environment's templates, newest first. Pass the last `id` as `after` to get the next page while `has_more` is true.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "1-100, default 50." + }, + "required": false, + "description": "1-100, default 50.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Cursor: the `id` of the last template of the previous page." + }, + "required": false, + "description": "Cursor: the `id` of the last template of the previous page.", + "name": "after", + "in": "query" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "A page of templates.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - "content.fraud": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 0.99 - }, - { - "type": "null" - } - ], - "description": "This reason's new weight. `null` restores its default." + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailTemplateList" + } + } + } + }, + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } } - }, - "description": "Changes the reasons given: a weight in [0, 0.99] replaces the default, `null` goes back to it." - } - }, - "additionalProperties": false, - "examples": [ - { - "block_threshold": 0.9, - "captcha": { - "provider": "turnstile", - "secret": "0x4AAA…", - "required": true } - } - ] - }, - "RiskCalibration": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "risk_calibration" - ] - }, - "window_days": { - "type": "integer", - "description": "How many days back it looks." }, - "assessments": { - "type": "integer", - "description": "Assessments in the window." + "422": { + "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "labeled": { - "type": "integer", - "description": "Of those, with feedback." + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "reasons": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RiskCalibrationReason" + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } }, - "description": "How each reason did against your feedback." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "rules": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RiskCalibrationRule" + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } }, - "description": "How each rule did against your feedback." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } - }, - "required": [ - "object", - "window_days", - "assessments", - "labeled", - "reasons", - "rules" + } + } + }, + "/v1/email_templates/{id}": { + "get": { + "operationId": "getEmailTemplate", + "tags": [ + "Email templates" ], - "description": "Feedback against the reasons and rules that fired, over the last 90 days. Feedback you give mostly on reviews and blocks over-represents those: read precision as a guide, not a measurement.", - "examples": [ + "summary": "Retrieve an email template", + "description": "A template by id or alias. Its content is in its draft and versions.", + "security": [ { - "object": "risk_calibration", - "window_days": 90, - "assessments": 1840, - "labeled": 212, - "reasons": [ - { - "code": "ip.hosting_provider", - "fired": 96, - "fraud": 4, - "legitimate": 38, - "precision": 0.095, - "default_weight": 0.25, - "weight": 0.25, - "suggested_weight": 0.16 + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The template's id or alias.", + "examples": [ + "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The template's id or alias.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "The template.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } } - ], - "rules": [ - { - "rule": "rskrule_01k7c8m4q2f3e9v6t5r1n0p8yz", - "name": "Disposable email from a data center", - "matched": 31, - "fraud": 12, - "legitimate": 1, - "precision": 0.923 + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailTemplate" + } } - ] - } - ] - }, - "RiskCalibrationReason": { - "type": "object", - "properties": { - "code": { - "type": "string", - "enum": [ - "email.invalid", - "email.disposable", - "email.undeliverable", - "email.role_account", - "email.typo", - "email.domain_new", - "ip.tor", - "ip.vpn_network", - "ip.hosting_provider", - "ip.vpn", - "ip.proxy", - "ip.relay", - "ip.private", - "velocity.signups_per_ip_1h", - "velocity.signups_per_ip_24h", - "velocity.signups_per_subnet_1h", - "velocity.signups_per_email_domain_1h", - "velocity.signups_per_mailbox_24h", - "velocity.signups_per_device_24h", - "velocity.checkouts_per_ip_1h", - "velocity.checkouts_per_user_1h", - "velocity.checkouts_per_card_24h", - "velocity.logins_per_ip_1h", - "velocity.logins_per_user_1h", - "velocity.events_per_ip_1h", - "velocity.events_per_user_1h", - "duplicate.canonical_mailbox", - "duplicate.device", - "history.fraud_mailbox", - "history.fraud_user", - "history.fraud_ip", - "history.fraud_subnet", - "history.fraud_card", - "history.fraud_device", - "checkout.card_country_mismatch", - "checkout.billing_country_mismatch", - "checkout.prepaid_card", - "device.automation", - "device.invalid", - "login.new_country", - "login.new_network", - "captcha.failed", - "captcha.missing", - "content.spam", - "content.phishing", - "content.malware", - "content.fraud" - ], - "description": "The reason's code." + } }, - "fired": { - "type": "integer", - "description": "Assessments it fired in." + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "fraud": { - "type": "integer", - "description": "With feedback `fraud` or `abuse`." + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "legitimate": { - "type": "integer", - "description": "With feedback `legitimate`." + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "precision": { - "type": [ - "number", - "null" - ], - "description": "`fraud / (fraud + legitimate)`: how often it was right when you told coss.com. `null` without feedback." + "404": { + "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "default_weight": { - "type": "number", - "description": "Its weight unless this environment changes it." + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "weight": { - "type": "number", - "description": "Its weight in this environment now." + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "suggested_weight": { - "type": [ - "number", - "null" - ], - "description": "The weight feedback supports: its precision, pulled toward the default while there is little feedback. `null` below 20 assessments with feedback." + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } - }, - "required": [ - "code", - "fired", - "fraud", - "legitimate", - "precision", - "default_weight", - "weight", - "suggested_weight" - ] + } }, - "RiskCalibrationRule": { - "type": "object", - "properties": { - "rule": { - "type": "string", - "description": "The rule's id." - }, - "name": { - "type": "string", - "description": "The rule's name." - }, - "matched": { - "type": "integer", - "description": "Assessments it decided." - }, - "fraud": { - "type": "integer", - "description": "With feedback `fraud` or `abuse`." - }, - "legitimate": { - "type": "integer", - "description": "With feedback `legitimate`." + "patch": { + "operationId": "updateEmailTemplate", + "tags": [ + "Email templates" + ], + "summary": "Update an email template", + "description": "Changes the template's name, alias or description. Changing the alias changes what sends must use. Content changes go to the draft (`PUT /v1/email_templates/{id}/draft`).", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The template's id or alias.", + "examples": [ + "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The template's id or alias.", + "name": "id", + "in": "path" }, - "precision": { - "type": [ - "number", - "null" - ], - "description": "`fraud / (fraud + legitimate)`: how often it was right when you told coss.com. `null` without feedback." + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEmailTemplateRequest" + } + } } }, - "required": [ - "rule", - "name", - "matched", - "fraud", - "legitimate", - "precision" - ] - }, - "RiskRule": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "risk_rule" - ] + "responses": { + "200": { + "description": "The template.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailTemplate" + } + } + } }, - "id": { - "type": "string" + "400": { + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "name": { - "type": "string", - "description": "Your name for the rule, returned with reasons when it decides." + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "enabled": { - "type": "boolean", - "description": "`false`: it's kept, and no assessment applies it." + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "position": { - "type": "integer", - "description": "Rules are tried in ascending order." + "404": { + "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "events": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RiskEvent" + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } }, - "description": "Events it applies to; empty: every event." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "match": { - "type": "string", - "enum": [ - "all", - "any" - ], - "description": "`all`: every condition must hold. `any`: one is enough." + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "conditions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RiskRuleCondition" + "422": { + "description": "The body failed validation (`validation_error`), or another template has the alias (`template_alias_taken`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } }, - "description": "What must hold, by `match`." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "action": { - "$ref": "#/components/schemas/RiskDecision" + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "created_at": { - "type": "string", - "format": "date-time" + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "updated_at": { - "type": "string", - "format": "date-time" + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } - }, - "required": [ - "object", - "id", - "name", - "enabled", - "position", - "events", - "match", - "conditions", - "action", - "created_at", - "updated_at" + } + }, + "delete": { + "operationId": "deleteEmailTemplate", + "tags": [ + "Email templates" ], - "examples": [ + "summary": "Delete an email template", + "description": "Deletes the template with its draft and every version. Emails already sent or scheduled from it keep their content; new sends naming it fail with `template_not_found`.", + "security": [ { - "object": "risk_rule", - "id": "rskrule_01k7c8m4q2f3e9v6t5r1n0p8yz", - "name": "Disposable email from a data center", - "enabled": true, - "position": 0, - "events": [ - "signup" - ], - "match": "all", - "conditions": [ - { - "field": "email.disposable", - "operator": "is", - "value": true - }, - { - "field": "ip.hosting", - "operator": "is", - "value": true - } - ], - "action": "block", - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z" + "bearerAuth": [] } - ] - }, - "RiskRuleCondition": { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "email.valid", - "email.disposable", - "email.undeliverable", - "email.role_account", - "email.free_provider", - "email.typo", - "email.domain_new", - "email.domain", - "email.domain_age_days", - "ip.tor", - "ip.hosting", - "ip.vpn", - "ip.proxy", - "ip.relay", - "ip.private", - "ip.country", - "ip.asn", - "ip.hosting_provider", - "captcha.passed", - "duplicate.canonical_mailbox", - "duplicate.device", - "history.fraud", - "login.new_country", - "login.new_network", - "content.spam", - "content.phishing", - "content.malware", - "content.fraud", - "checkout.amount", - "checkout.currency", - "checkout.card_country", - "checkout.card_funding", - "checkout.billing_country", - "checkout.shipping_country", - "device.automation", - "device.timezone", - "device.language", - "score", - "velocity.ip_1h", - "velocity.ip_24h", - "velocity.subnet_1h", - "velocity.subnet_24h", - "velocity.email_domain_1h", - "velocity.email_domain_24h", - "velocity.mailbox_1h", - "velocity.mailbox_24h", - "velocity.user_1h", - "velocity.user_24h", - "velocity.card_1h", - "velocity.card_24h", - "velocity.device_1h", - "velocity.device_24h" - ], - "description": "The signal to test, such as `email.disposable` or `ip.country`." - }, - "operator": { - "type": "string", - "enum": [ - "is", - "eq", - "neq", - "gt", - "gte", - "lt", - "lte", - "in", - "not_in" - ], - "description": "How to compare. Booleans use `is`, numbers `eq` to `lte`, and strings `eq`, `neq`, `in` and `not_in`." + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The template's id or alias.", + "examples": [ + "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The template's id or alias.", + "name": "id", + "in": "path" }, - "value": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "number" - }, - { - "type": "string", - "maxLength": 320 + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "The template was deleted.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - { - "type": "array", - "items": { + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { "type": "string", - "maxLength": 320 - }, - "minItems": 1, - "maxItems": 100 + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedEmailTemplate" + } + } + } + }, + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } } - ], - "description": "What to compare it with: its type follows the field, and `in` and `not_in` take a list of strings." - } - }, - "required": [ - "field", - "operator", - "value" - ], - "additionalProperties": false, - "description": "A test of one field. A field without a value (no IP, no email…) never matches, whatever the operator." - }, - "CreateRiskRuleRequest": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100, - "description": "Your name for the rule, returned with reasons when it decides." + } }, - "enabled": { - "type": "boolean", - "description": "Default `true`." + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "position": { - "type": "integer", - "minimum": 0, - "maximum": 10000, - "description": "Default 0." + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "events": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RiskEvent" + "404": { + "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } }, - "maxItems": 5, - "description": "Events it applies to. Default: every event." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "match": { - "type": "string", - "enum": [ - "all", - "any" - ], - "description": "Default `all`." + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "conditions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RiskRuleCondition" + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } }, - "minItems": 1, - "maxItems": 20, - "description": "What must hold, by `match`." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "action": { - "$ref": "#/components/schemas/RiskDecision" + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } - }, - "required": [ - "name", - "conditions", - "action" + } + } + }, + "/v1/email_templates/{id}/draft": { + "get": { + "operationId": "getEmailTemplateDraft", + "tags": [ + "Email templates" ], - "additionalProperties": false, - "examples": [ + "summary": "Retrieve a template's draft", + "description": "The draft (`version: null`): what the next publish makes sends use. `problems` lists syntax errors and undeclared variables, which keep it from being published.", + "security": [ { - "name": "Disposable email from a data center", - "events": [ - "signup" - ], - "conditions": [ - { - "field": "email.disposable", - "operator": "is", - "value": true + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The template's id or alias.", + "examples": [ + "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The template's id or alias.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "The draft.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - { - "field": "ip.hosting", - "operator": "is", - "value": true + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } } - ], - "action": "block" - } - ] - }, - "RiskRuleList": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "list" - ] + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailTemplateVersion" + } + } + } }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RiskRule" + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } } }, - "has_more": { - "type": "boolean", - "enum": [ - false - ] - } - }, - "required": [ - "object", - "data", - "has_more" - ], - "description": "Every rule of the environment (at most 50), in evaluation order.", - "examples": [ - { - "object": "list", - "data": [ - { - "object": "risk_rule", - "id": "rskrule_01k7c8m4q2f3e9v6t5r1n0p8yz", - "name": "Disposable email from a data center", - "enabled": true, - "position": 0, - "events": [ - "signup" - ], - "match": "all", - "conditions": [ - { - "field": "email.disposable", - "operator": "is", - "value": true - }, - { - "field": "ip.hosting", - "operator": "is", - "value": true - } - ], - "action": "block", - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z" + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } } - ], - "has_more": false - } - ] - }, - "UpdateRiskRuleRequest": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100, - "description": "Your name for the rule." + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "enabled": { - "type": "boolean", - "description": "`false` keeps it, and stops applying it." + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "position": { - "type": "integer", - "minimum": 0, - "maximum": 10000, - "description": "Rules are tried in ascending order." + "404": { + "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "events": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RiskEvent" + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } }, - "maxItems": 5, - "description": "Events it applies to. Empty: every event." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "match": { - "type": "string", - "enum": [ - "all", - "any" - ], - "description": "`all`: every condition must hold. `any`: one is enough." + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "conditions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RiskRuleCondition" + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "put": { + "operationId": "updateEmailTemplateDraft", + "tags": [ + "Email templates" + ], + "summary": "Update a template's draft", + "description": "Replaces the draft's variables and content. Sends keep using the published version until the draft is published. Pass the `revision` you read to refuse overwriting someone else's changes. A draft with problems is saved (and reported in `problems`).", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The template's id or alias.", + "examples": [ + "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "minItems": 1, - "maxItems": 20, - "description": "What must hold, by `match`." + "required": true, + "description": "The template's id or alias.", + "name": "id", + "in": "path" }, - "action": { - "$ref": "#/components/schemas/RiskDecision" - } - }, - "additionalProperties": false, - "examples": [ { - "enabled": false + "$ref": "#/components/parameters/CossVersion" } - ] - }, - "DeletedRiskRule": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "risk_rule" - ] - }, - "id": { - "type": "string" - }, - "deleted": { - "type": "boolean", - "enum": [ - true - ] + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEmailTemplateDraftRequest" + } + } } }, - "required": [ - "object", - "id", - "deleted" - ] - }, - "RiskEmailCheck": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "risk_email_check" - ] - }, - "email": { - "type": "string", - "description": "The address checked." + "responses": { + "200": { + "description": "The saved draft.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailTemplateVersion" + } + } + } }, - "valid": { - "type": "boolean", - "description": "Syntactically valid (the rest assumes it is)." + "400": { + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "domain": { - "type": [ - "string", - "null" - ], - "description": "Its domain. `null` when it isn't valid." + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "canonical": { - "type": [ - "string", - "null" - ], - "description": "The mailbox with the provider's aliasing folded (dots and `+tags` at Gmail…): two addresses with the same canonical form reach one inbox." + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "disposable": { - "type": "boolean", - "description": "Whether its domain is a known burner service." + "404": { + "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "deliverability": { - "type": "string", - "enum": [ - "deliverable", - "undeliverable", - "unknown" - ], - "description": "From DNS (MX, A, AAAA). `unknown` when the lookup failed or timed out." + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "role_account": { - "type": "boolean", - "description": "Whether it's a role mailbox, like `info@`." + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "free_provider": { - "type": "boolean", - "description": "Whether its domain is a free mailbox provider." + "422": { + "description": "The body failed validation (`validation_error`), or the draft changed since `revision` (`template_revision_mismatch`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "suggestion": { - "type": [ - "string", - "null" - ], - "description": "The likely intended domain when this one looks like a typo of a popular one." + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "domain_age_days": { - "type": [ - "integer", - "null" - ], - "description": "Days since the domain was registered (RDAP); `null` when unknown or not looked up (free providers, disposable domains)." + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "degraded": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "email_dns", - "domain_age" - ] + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } }, - "description": "Lookups that failed or timed out (their fields are `unknown`/`null`)." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } - }, - "required": [ - "object", - "email", - "valid", - "domain", - "canonical", - "disposable", - "deliverability", - "role_account", - "free_provider", - "suggestion", - "domain_age_days", - "degraded" + } + } + }, + "/v1/email_templates/{id}/publish": { + "post": { + "operationId": "publishEmailTemplate", + "tags": [ + "Email templates" ], - "examples": [ + "summary": "Publish a template's draft", + "description": "Copies the draft into the next version, which sends use from then on. Emails already accepted keep the content they were rendered with. A draft without changes since the last publish returns that version, so retrying with the `revision` you reviewed is safe (without it, a retry could publish edits saved in between).", + "security": [ { - "object": "risk_email_check", - "email": "ada@gnail.com", - "valid": true, - "domain": "gnail.com", - "canonical": "ada@gnail.com", - "disposable": false, - "deliverability": "deliverable", - "role_account": false, - "free_provider": false, - "suggestion": "gmail.com", - "domain_age_days": 7300, - "degraded": [] - } - ] - }, - "RiskEmailCheckRequest": { - "type": "object", - "properties": { - "email": { - "type": "string", - "minLength": 1, - "maxLength": 320, - "description": "The address to check, as given." + "bearerAuth": [] } - }, - "required": [ - "email" ], - "additionalProperties": false, - "examples": [ + "parameters": [ { - "email": "ada@gnail.com" + "schema": { + "type": "string", + "description": "The template's id or alias.", + "examples": [ + "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The template's id or alias.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" } - ] - }, - "RiskIpCheck": { - "type": "object", - "properties": { - "version": { - "anyOf": [ - { - "type": "number", - "enum": [ - 4 - ] + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublishEmailTemplateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The published version.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - { - "type": "number", - "enum": [ - 6 - ] + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } } - ], - "description": "IPv4 or IPv6." - }, - "private": { - "type": "boolean", - "description": "Whether it's a private or reserved address." + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailTemplateVersion" + } + } + } }, - "tor": { - "type": "boolean", - "description": "Whether it's a Tor exit." + "400": { + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "hosting": { - "type": "boolean", - "description": "Whether it's in a cloud, hosting or VPN network." + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "hosting_provider": { - "type": [ - "string", - "null" - ], - "description": "That network's operator. `null` when it isn't one." + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "hosting_kind": { - "type": [ - "string", - "null" - ], - "enum": [ - "cloud", - "hosting", - "vpn", - null - ], - "description": "`cloud`, `hosting` or `vpn`. `null` when it isn't one." + "404": { + "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "asn": { - "type": [ - "integer", - "null" - ], - "description": "The network's autonomous system number. `null` when unknown." + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "as_name": { - "type": [ - "string", - "null" - ], - "description": "The network's name. `null` when unknown." + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "country": { - "type": [ - "string", - "null" - ], - "description": "ISO 3166-1 alpha-2." + "422": { + "description": "The draft has problems or no content for its default locale (`template_invalid`, see `issues`), or changed since `revision` (`template_revision_mismatch`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "privacy": { - "anyOf": [ - { - "$ref": "#/components/schemas/RiskIpPrivacy" + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - { - "type": "null" + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } } - ], - "description": "From coss.com's IP privacy data provider (VPNs, proxies, relays). `null` when it isn't available or was not asked (private addresses)." - }, - "object": { - "type": "string", - "enum": [ - "risk_ip_check" - ] + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "ip": { - "type": "string", - "description": "The address checked." + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "degraded": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "ip_data", - "ip_privacy" - ] + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } }, - "description": "`ip_data` when some IP data (Tor exits, cloud ranges, networks and countries) is unavailable at the moment; `ip_privacy` when the privacy data provider failed or timed out." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } - }, - "required": [ - "version", - "private", - "tor", - "hosting", - "hosting_provider", - "hosting_kind", - "asn", - "as_name", - "country", - "privacy", - "object", - "ip", - "degraded" + } + } + }, + "/v1/email_templates/{id}/render": { + "post": { + "operationId": "renderEmailTemplate", + "tags": [ + "Email templates" ], - "examples": [ + "summary": "Render a template", + "description": "Renders the draft (or a published `version`) with `variables`, exactly as a send would, and returns the subject, HTML and text: a preview, nothing is sent or stored. Without `variables` it renders the version's `sample_variables`.", + "security": [ { - "object": "risk_ip_check", - "ip": "185.220.101.1", - "version": 4, - "private": false, - "tor": true, - "hosting": false, - "hosting_provider": null, - "hosting_kind": null, - "asn": 60729, - "as_name": "Stiftung Erneuerbare Freiheit", - "country": "DE", - "privacy": null, - "degraded": [] - } - ] - }, - "RiskIpCheckRequest": { - "type": "object", - "properties": { - "ip": { - "anyOf": [ - { - "type": "string", - "format": "ip" - }, - { - "type": "string", - "format": "ip" - } - ], - "description": "The address to check." + "bearerAuth": [] } - }, - "required": [ - "ip" ], - "additionalProperties": false, - "examples": [ + "parameters": [ { - "ip": "185.220.101.1" + "schema": { + "type": "string", + "description": "The template's id or alias.", + "examples": [ + "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The template's id or alias.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" } - ] - }, - "RiskPhoneCheck": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "risk_phone_check" - ] + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RenderEmailTemplateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The rendered template.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RenderedEmailTemplate" + } + } + } }, - "phone": { - "type": "string", - "description": "The number in E.164, or as given when it isn't." + "400": { + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "valid": { - "type": "boolean", - "description": "A possible number in its country's numbering plan. When the lookup failed (`degraded`), only the E.164 syntax was checked." + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "country": { - "type": [ - "string", - "null" - ], - "description": "ISO 3166-1 alpha-2." + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "line_type": { - "type": [ - "string", - "null" - ], - "enum": [ - "mobile", - "landline", - "fixed_voip", - "non_fixed_voip", - "toll_free", - "premium", - "shared_cost", - "personal", - "uan", - "voicemail", - "pager", - "unknown", - null - ], - "description": "`fixed_voip` numbers belong to an address (cable, business lines); `non_fixed_voip` ones can be had online in minutes (Google Voice, app numbers), the kind used for throwaway accounts. `null` when it couldn't be looked up." + "422": { + "description": "The body failed validation (`validation_error`), the draft has problems (`template_invalid`, see `issues`), the variables don't match its declarations (`invalid_template_variables`), it has no such version (`template_version_not_found`), or it doesn't render (`template_render_failed`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "carrier": { - "anyOf": [ - { - "$ref": "#/components/schemas/RiskPhoneCarrier" + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } }, - { - "type": "null" + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } } - ], - "description": "The network the number is on now (after any port). `null` when unknown, and for toll-free, premium and personal numbers." + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "sim_swap": { - "anyOf": [ - { - "$ref": "#/components/schemas/RiskPhoneSimSwap" - }, - { - "type": "null" + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } } - ], - "description": "The mobile number's last SIM change. `null` where coss.com doesn't have that data (not every country and carrier)." + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "degraded": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "phone_data", - "sim_swap" - ] + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } }, - "description": "`phone_data` when the lookup failed or timed out (or had no line type for the number); `sim_swap` when the carrier didn't answer about the SIM." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } - }, - "required": [ - "object", - "phone", - "valid", - "country", - "line_type", - "carrier", - "sim_swap", - "degraded" + } + } + }, + "/v1/email_templates/{id}/versions": { + "get": { + "operationId": "listEmailTemplateVersions", + "tags": [ + "Email templates" ], - "examples": [ + "summary": "List a template's versions", + "description": "Published versions, newest first. Pass the last `version` as `after` to get the next page while `has_more` is true.", + "security": [ { - "object": "risk_phone_check", - "phone": "+14155550123", - "valid": true, - "country": "US", - "line_type": "mobile", - "carrier": { - "name": "T-Mobile USA, Inc.", - "mobile_country_code": "310", - "mobile_network_code": "260" - }, - "sim_swap": null, - "degraded": [] - } - ] - }, - "RiskPhoneCarrier": { - "type": "object", - "properties": { - "name": { - "type": [ - "string", - "null" - ] - }, - "mobile_country_code": { - "type": [ - "string", - "null" - ] - }, - "mobile_network_code": { - "type": [ - "string", - "null" - ], - "description": "Only for mobile numbers." - } - }, - "required": [ - "name", - "mobile_country_code", - "mobile_network_code" - ] - }, - "RiskPhoneSimSwap": { - "type": "object", - "properties": { - "recent": { - "type": "boolean", - "description": "The SIM changed within `period`: someone may have taken the number over." - }, - "swapped_at": { - "type": [ - "string", - "null" - ], - "description": "The last SIM change, where the carrier tells the date." - }, - "period": { - "type": [ - "string", - "null" - ], - "description": "What `recent` means, as an ISO 8601 duration (`PT48H`)." - } - }, - "required": [ - "recent", - "swapped_at", - "period" - ] - }, - "RiskPhoneCheckRequest": { - "type": "object", - "properties": { - "phone": { - "type": "string", - "minLength": 1, - "maxLength": 32, - "description": "The number in E.164 (`+14155550123`); spaces, dashes, dots and brackets are ignored. Anything else is `valid: false`." + "bearerAuth": [] } - }, - "required": [ - "phone" ], - "additionalProperties": false, - "examples": [ + "parameters": [ { - "phone": "+14155550123" - } - ] - }, - "RiskContentClassification": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "risk_content_classification" - ] - }, - "scores": { - "$ref": "#/components/schemas/RiskContentSignals" + "schema": { + "type": "string", + "description": "The template's id or alias.", + "examples": [ + "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The template's id or alias.", + "name": "id", + "in": "path" }, - "truncated": { - "type": "boolean", - "description": "The text was longer than the model reads: only its beginning was classified." - } - }, - "required": [ - "object", - "scores", - "truncated" - ], - "description": "Scores only: coss.com never decides what they mean for you. Neither the text nor the scores are stored.", - "examples": [ { - "object": "risk_content_classification", - "scores": { - "spam": 0.91, - "phishing": 0.04, - "malware": 0.01, - "fraud": 0.86 + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 20, + "description": "1-20, default 10." }, - "truncated": false - } - ] - }, - "RiskContentClassifyRequest": { - "type": "object", - "properties": { - "text": { - "type": "string", - "minLength": 1, - "maxLength": 50000, - "description": "The text to classify (a profile bio, a comment, a listing). The model reads the first 8,000 characters." - } - }, - "required": [ - "text" - ], - "additionalProperties": false, - "examples": [ - { - "text": "Earn $5,000 a week from home! DM me on Telegram: t.me/fast-cash" - } - ] - }, - "RiskLimit": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "risk_limit" - ] - }, - "bucket": { - "type": "string", - "description": "The bucket's name." - }, - "allowed": { - "type": "boolean", - "description": "Whether to let the request through. Denied requests take no tokens." - }, - "limit": { - "type": "integer", - "description": "The bucket's size (`burst`)." - }, - "remaining": { - "type": "integer", - "description": "Tokens left for this key." - }, - "reset_ms": { - "type": "integer", - "description": "Milliseconds until the key's bucket is full again." - }, - "retry_after_ms": { - "type": [ - "integer", - "null" - ], - "description": "When denied: milliseconds until the same request would be allowed; `null` when it never would (`cost` above the bucket's size). 0 when allowed." + "required": false, + "description": "1-20, default 10.", + "name": "limit", + "in": "query" }, - "degraded": { - "type": "boolean", - "description": "coss.com couldn't count (its store was unavailable): `allowed` is the bucket's `on_failure`." - } - }, - "required": [ - "object", - "bucket", - "allowed", - "limit", - "remaining", - "reset_ms", - "retry_after_ms", - "degraded" - ], - "examples": [ { - "object": "risk_limit", - "bucket": "password_reset", - "allowed": true, - "limit": 5, - "remaining": 4, - "reset_ms": 720000, - "retry_after_ms": 0, - "degraded": false - } - ] - }, - "CheckRiskLimitRequest": { - "type": "object", - "properties": { - "bucket": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$", - "description": "The bucket's name." - }, - "key": { - "type": "string", - "minLength": 1, - "maxLength": 512, - "description": "Who or what is limited: a user id, an email address, an IP address… coss.com keeps only a keyed hash of it." + "schema": { + "type": "integer", + "minimum": 1, + "description": "Cursor: the `version` of the last version of the previous page." + }, + "required": false, + "description": "Cursor: the `version` of the last version of the previous page.", + "name": "after", + "in": "query" }, - "cost": { - "type": "integer", - "minimum": 1, - "maximum": 1000000, - "description": "Tokens this request takes. Default 1." - } - }, - "required": [ - "bucket", - "key" - ], - "additionalProperties": false, - "examples": [ { - "bucket": "password_reset", - "key": "ada@example.com" + "$ref": "#/components/parameters/CossVersion" } - ] - }, - "RiskLimitBucket": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "risk_limit_bucket" - ] + ], + "responses": { + "200": { + "description": "A page of versions.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailTemplateVersionList" + } + } + } }, - "id": { - "type": "string", - "examples": [ - "rsklb_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "name": { - "type": "string", - "description": "What `POST /v1/risk/limits` names it by." + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "limit": { - "type": "integer", - "description": "Tokens added per `period_seconds`." + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "period_seconds": { - "type": "integer", - "description": "How many seconds `limit` tokens take to be added." + "404": { + "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "burst": { - "type": "integer", - "description": "The bucket's size: how many requests a key can make at once." + "422": { + "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "on_failure": { - "type": "string", - "enum": [ - "allow", - "deny" - ], - "description": "The answer while coss.com can't count (its store is unavailable): `allow` (fail open) or `deny`." + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "created_at": { - "type": "string", - "format": "date-time" + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "updated_at": { - "type": "string", - "format": "date-time" + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } - }, - "required": [ - "object", - "id", - "name", - "limit", - "period_seconds", - "burst", - "on_failure", - "created_at", - "updated_at" + } + } + }, + "/v1/email_templates/{id}/versions/{version}": { + "get": { + "operationId": "getEmailTemplateVersion", + "tags": [ + "Email templates" ], - "examples": [ + "summary": "Retrieve a template version", + "description": "A published version, which never changes.", + "security": [ { - "object": "risk_limit_bucket", - "id": "rsklb_01k7c8m4q2f3e9v6t5r1n0p8yz", - "name": "password_reset", - "limit": 5, - "period_seconds": 3600, - "burst": 5, - "on_failure": "allow", - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z" + "bearerAuth": [] } - ] - }, - "CreateRiskLimitBucketRequest": { - "type": "object", - "properties": { - "name": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$", - "description": "Unique in the environment; can't be changed." - }, - "limit": { - "type": "integer", - "minimum": 1, - "maximum": 1000000, - "description": "Tokens added per `period_seconds`." - }, - "period_seconds": { - "type": "integer", - "minimum": 1, - "maximum": 86400, - "description": "1 second to 1 day." + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The template's id or alias.", + "examples": [ + "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The template's id or alias.", + "name": "id", + "in": "path" }, - "burst": { - "type": "integer", - "minimum": 1, - "maximum": 1000000, - "description": "How many requests a key can make at once. Defaults to `limit`." + { + "schema": { + "type": "integer", + "minimum": 1, + "description": "A published version number.", + "examples": [ + 1 + ] + }, + "required": true, + "description": "A published version number.", + "name": "version", + "in": "path" }, - "on_failure": { - "type": "string", - "enum": [ - "allow", - "deny" - ], - "description": "The answer while coss.com can't count. Defaults to `allow`." - } - }, - "required": [ - "name", - "limit", - "period_seconds" - ], - "additionalProperties": false, - "examples": [ { - "name": "password_reset", - "limit": 5, - "period_seconds": 3600 + "$ref": "#/components/parameters/CossVersion" } - ] - }, - "RiskLimitBucketList": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "list" - ] + ], + "responses": { + "200": { + "description": "The version.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailTemplateVersion" + } + } + } }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RiskLimitBucket" + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } } }, - "has_more": { - "type": "boolean", - "enum": [ - false - ] - } - }, - "required": [ - "object", - "data", - "has_more" - ], - "description": "Every bucket of the environment (at most 100), by name.", - "examples": [ - { - "object": "list", - "data": [ - { - "object": "risk_limit_bucket", - "id": "rsklb_01k7c8m4q2f3e9v6t5r1n0p8yz", - "name": "password_reset", - "limit": 5, - "period_seconds": 3600, - "burst": 5, - "on_failure": "allow", - "created_at": "2026-01-15T09:30:00.000Z", - "updated_at": "2026-01-15T09:30:00.000Z" + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } } - ], - "has_more": false - } - ] - }, - "UpdateRiskLimitBucketRequest": { - "type": "object", - "properties": { - "limit": { - "type": "integer", - "minimum": 1, - "maximum": 1000000, - "description": "Tokens added per `period_seconds`." + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "period_seconds": { - "type": "integer", - "minimum": 1, - "maximum": 86400, - "description": "How many seconds `limit` tokens take to be added." + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "burst": { - "type": "integer", - "minimum": 1, - "maximum": 1000000, - "description": "The bucket's size: how many requests a key can make at once." + "404": { + "description": "No template with this id or alias exists in the API key's environment, or it has no such version (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "on_failure": { - "type": "string", - "enum": [ - "allow", - "deny" - ], - "description": "The answer while coss.com can't count: `allow` or `deny`." - } - }, - "additionalProperties": false, - "examples": [ - { - "limit": 10, - "burst": 20 - } - ] - }, - "DeletedRiskLimitBucket": { - "type": "object", - "properties": { - "object": { - "type": "string", - "enum": [ - "risk_limit_bucket" - ] + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "id": { - "type": "string" + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } }, - "deleted": { - "type": "boolean", - "enum": [ - true - ] + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } } - }, - "required": [ - "object", - "id", - "deleted" - ] - } - }, - "parameters": { - "CossVersion": { - "in": "header", - "name": "Coss-Version", - "required": false, - "description": "The API version (a date) to use for this request. Defaults to your organization's version, pinned to the latest one when it first used the API and upgraded from the dashboard. The SDK always sends the version it was built for.", - "schema": { - "type": "string", - "enum": [ - "2026-10-10" - ], - "examples": [ - "2026-10-10" - ] } } - } - }, - "paths": { - "/v1/environment": { - "get": { - "operationId": "getEnvironment", + }, + "/v1/email_templates/{id}/versions/{version}/restore": { + "post": { + "operationId": "restoreEmailTemplateVersion", "tags": [ - "Environment" + "Email templates" ], - "summary": "Retrieve the API key's environment", - "description": "Which environment, project and organization the API key belongs to, and the key itself. Any valid key can read its own environment (no permission needed): use it to check a key, show where it sends and what it may do.", + "summary": "Restore a template version", + "description": "Replaces the draft with a published version's variables and content. Publish the draft to make sends use it again.", "security": [ { "bearerAuth": [] } ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The template's id or alias.", + "examples": [ + "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The template's id or alias.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "description": "A published version number.", + "examples": [ + 1 + ] + }, + "required": true, + "description": "A published version number.", + "name": "version", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "responses": { "200": { - "description": "The API key's environment.", + "description": "The draft.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37261,7 +45342,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Environment" + "$ref": "#/components/schemas/EmailTemplateVersion" } } } @@ -37329,6 +45410,48 @@ } } }, + "404": { + "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The template has no such version (`template_version_not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -37398,30 +45521,35 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/api_key": { - "delete": { - "operationId": "revokeApiKey", + "/v1/domains": { + "post": { + "operationId": "createDomain", "tags": [ - "Environment" + "Domains" ], - "summary": "Revoke the API key", - "description": "Revokes the API key making this request, at once: every later request with it fails with `invalid_api_key`. `coss logout --revoke` uses it. Other keys are revoked in the dashboard.", + "summary": "Add a sending domain", + "description": "Claims the domain for the organization and returns the DNS records to publish. Domains belong to the organization, not to one environment, so every API key of the organization sees the same domains. The records are checked repeatedly in the background and verification gives up after 72 hours; until the domain is `verified`, live keys cannot send from it.", "security": [ { "bearerAuth": [] } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateDomainRequest" + } + } + } + }, "responses": { "200": { - "description": "The key was revoked.", + "description": "The domain, with the DNS records to publish.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37445,13 +45573,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedApiKey" + "$ref": "#/components/schemas/Domain" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37492,8 +45620,8 @@ } } }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "402": { + "description": "The organization's plan allows no more domains (`plan_limit_reached`) (`billing_error`). Never retried: it needs a plan change or a payment.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37513,8 +45641,8 @@ } } }, - "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37524,12 +45652,6 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Retry-After": { - "description": "Seconds to wait before retrying.", - "schema": { - "type": "integer" - } } }, "content": { @@ -37540,8 +45662,8 @@ } } }, - "500": { - "description": "Unexpected server error (`api_error` / `internal_error`).", + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37561,8 +45683,8 @@ } } }, - "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37581,61 +45703,9 @@ } } } - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - } - }, - "/v1/emails": { - "post": { - "operationId": "sendEmail", - "tags": [ - "Emails" - ], - "summary": "Send an email", - "description": "Queues an email from the API key's environment and returns it with `status: \"queued\"`; it is handed to the delivery provider moments later. Follow it with `GET /v1/emails/{id}` or the `email.sent` / `email.failed` webhooks. Live (`coss_live_…`) keys must send from a verified domain of the organization. Sandbox (`coss_test_…`) keys accept any `from` and never deliver anything; the response has `sandbox: true`. Recipients on the environment's suppression list (for every email, or for the email's `category`) are skipped and get the status `suppressed`. With a future `scheduled_at` the email is stored with the status `scheduled` and sent at that time; the domain and the suppression list are checked again then. Attachments are base64-encoded; only metadata is kept after sending. Recipients count against the organization's send quota (per second and per day; sandbox environments have their own daily quota). With open or click tracking on (the environment's settings, or `tracking`), the HTML body is delivered with a tracking pixel and/or links rewritten to coss.com redirects; the stored `html` stays as sent.", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Makes retries safe: a repeated request with the same key and body returns the original email (with its current status) instead of sending again, for 24 hours. 1-256 characters.", - "examples": [ - "welcome-user-123" - ] - }, - "required": false, - "description": "Makes retries safe: a repeated request with the same key and body returns the original email (with its current status) instead of sending again, for 24 hours. 1-256 characters.", - "name": "Idempotency-Key", - "in": "header" }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SendEmailRequest" - } - } - } - }, - "responses": { - "202": { - "description": "The email, accepted for delivery (`queued` or `scheduled`). A replay returns the original email with its current status.", + "422": { + "description": "The body failed validation (`validation_error`), the name is not a usable domain (`invalid_domain`), it is already in use here or by another account (`domain_taken`), or The organization already has 5 domains awaiting verification (`too_many_pending_domains`). For a sub-account: it doesn't exist in the API key's environment (`subaccount_not_found`), isn't live (`subaccount_not_live`), or has 10 domains already (`too_many_subaccount_domains`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37645,36 +45715,45 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Idempotent-Replayed": { - "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", + } + }, + "content": { + "application/json": { "schema": { - "type": "string", - "enum": [ - "true" - ] + "$ref": "#/components/schemas/Error" } - }, - "Coss-Version": { - "description": "The API version this response is rendered in.", + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", "schema": { "type": "string", "examples": [ - "2026-10-10" + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } } }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Email" + "$ref": "#/components/schemas/Error" } } } }, - "400": { - "description": "The body is not valid JSON (`invalid_json`) or the `Idempotency-Key` header is invalid (`invalid_idempotency_key`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37694,8 +45773,8 @@ } } }, - "401": { - "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "502": { + "description": "The delivery provider could not set the domain up; retry (`provider_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37715,8 +45794,8 @@ } } }, - "402": { - "description": "Live sending is paused for an overdue invoice or the spend limit (`billing_hold`), or the send would go past the plan's monthly allowance (`usage_limit_exceeded`) (`billing_error`). Never retried: it needs a plan change or a payment.", + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37735,9 +45814,46 @@ } } } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, + "get": { + "operationId": "listDomains", + "tags": [ + "Domains" + ], + "summary": "List sending domains", + "description": "Every domain of the organization, ordered by name. Domains belong to the organization, not to one environment, so every API key of the organization sees the same domains.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Only this sub-account's domains (its id or your uid)." + }, + "required": false, + "description": "Only this sub-account's domains (its id or your uid).", + "name": "subaccount", + "in": "query" }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`insufficient_permissions`), its organization is suspended (`organization_suspended`), live sending is suspended (`account_suspended`), or sending is paused for the organization because of its sending reputation (`sending_paused`), or the `subaccount` is turned off (`subaccount_disabled`) or paused (`subaccount_paused`).", + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "The organization's domains.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37747,18 +45863,27 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } } }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/DomainList" } } } }, - "409": { - "description": "Requests with this `Idempotency-Key` are racing for an expired key (`request_in_progress`). Retry after `Retry-After` seconds.", + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37768,12 +45893,6 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Retry-After": { - "description": "Seconds to wait.", - "schema": { - "type": "integer" - } } }, "content": { @@ -37784,8 +45903,8 @@ } } }, - "413": { - "description": "The request body is larger than 40 MB (`payload_too_large`).", + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37805,8 +45924,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37827,7 +45946,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`, see `issues`), the `from` domain is not a verified domain of the organization (`domain_not_verified`, live keys only), the `Idempotency-Key` was already used with a different body (`idempotency_key_reused`), or the template can't be sent: it doesn't exist (`template_not_found`), has no published version (`template_not_published`) or no such version (`template_version_not_found`), the variables don't match it (`invalid_template_variables`, see `issues`), or it renders an invalid subject or empty body (`template_render_failed`).", + "description": "`subaccount` names no sub-account of the API key's environment (`subaccount_not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37848,7 +45967,7 @@ } }, "429": { - "description": "Too many requests with this API key (`rate_limited`), or the email's recipients exceed the send quota (`quota_exceeded`). Retry after `Retry-After` seconds; without `Retry-After` the email has more recipients than the quota allows at once.", + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -37860,7 +45979,7 @@ } }, "Retry-After": { - "description": "Seconds to wait.", + "description": "Seconds to wait before retrying.", "schema": { "type": "integer" } @@ -37917,14 +46036,16 @@ } } } - }, - "get": { - "operationId": "listEmails", + } + }, + "/v1/domains/{id}": { + "patch": { + "operationId": "updateDomain", "tags": [ - "Emails" + "Domains" ], - "summary": "List emails", - "description": "Emails sent from the API key's environment, newest first, without their content (retrieve an email for `html`, `text`, `headers` and `attachments`). Filter by recipient (`to`), by the email's or a recipient's status, by category, batch or creation time. Pass the last `id` as `after` to get the next page while `has_more` is true.", + "summary": "Update a sending domain", + "description": "Shares one of the organization's own domains with its sub-accounts (they may then send from it, through the organization's reputation), or stops sharing it.", "security": [ { "bearerAuth": [] @@ -37934,135 +46055,33 @@ { "schema": { "type": "string", - "minLength": 1, - "maxLength": 320, - "description": "Only emails with this recipient (in `to`, `cc` or `bcc`; the bare address, any case).", + "description": "The domain's id.", "examples": [ - "user@example.com" + "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, - "required": false, - "description": "Only emails with this recipient (in `to`, `cc` or `bcc`; the bare address, any case).", - "name": "to", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "Only emails with a recipient in one of these statuses, comma-separated: `bounced,complained`. With `to`, that recipient's status." - }, - "required": false, - "description": "Only emails with a recipient in one of these statuses, comma-separated: `bounced,complained`. With `to`, that recipient's status.", - "name": "recipient_status", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "Only emails in these statuses, comma-separated: `failed`." - }, - "required": false, - "description": "Only emails in these statuses, comma-separated: `failed`.", - "name": "status", - "in": "query" - }, - { - "schema": { - "type": "string", - "maxLength": 256, - "description": "Free text, matched against the subject and the `to` addresses." - }, - "required": false, - "description": "Free text, matched against the subject and the `to` addresses.", - "name": "q", - "in": "query" - }, - { - "schema": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "Only emails of this category." - }, - "required": false, - "description": "Only emails of this category.", - "name": "category", - "in": "query" - }, - { - "schema": { - "type": "string", - "maxLength": 64, - "description": "Only the emails of this batch." - }, - "required": false, - "description": "Only the emails of this batch.", - "name": "batch_id", - "in": "query" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Only the emails sent for this sub-account (its id or your uid)." - }, - "required": false, - "description": "Only the emails sent for this sub-account (its id or your uid).", - "name": "subaccount", - "in": "query" - }, - { - "schema": { - "type": "string", - "format": "date-time", - "description": "Only emails created at or after this time." - }, - "required": false, - "description": "Only emails created at or after this time.", - "name": "created_after", - "in": "query" - }, - { - "schema": { - "type": "string", - "format": "date-time", - "description": "Only emails created before this time." - }, - "required": false, - "description": "Only emails created before this time.", - "name": "created_before", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 20." - }, - "required": false, - "description": "1-100, default 20.", - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "maxLength": 64, - "description": "Cursor: the `id` of the last email of the previous page." - }, - "required": false, - "description": "Cursor: the `id` of the last email of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The domain's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateDomainRequest" + } + } + } + }, "responses": { "200": { - "description": "A page of emails.", + "description": "The domain.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38086,13 +46105,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailSummaryList" + "$ref": "#/components/schemas/Domain" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38154,8 +46173,8 @@ } } }, - "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "404": { + "description": "No domain with this id exists in this organization (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38175,8 +46194,8 @@ } } }, - "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38186,12 +46205,6 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Retry-After": { - "description": "Seconds to wait before retrying.", - "schema": { - "type": "integer" - } } }, "content": { @@ -38202,8 +46215,8 @@ } } }, - "500": { - "description": "Unexpected server error (`api_error` / `internal_error`).", + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38223,8 +46236,8 @@ } } }, - "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "422": { + "description": "The body failed validation (`validation_error`), or the domain is a sub-account's (`not_shareable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38243,56 +46256,9 @@ } } } - } - } - } - }, - "/v1/emails/batch": { - "post": { - "operationId": "sendEmailBatch", - "tags": [ - "Emails" - ], - "summary": "Send a batch of emails", - "description": "Queues up to 100 emails in one request. Each email is handled exactly as by `POST /v1/emails` (sender checks, suppressions, scheduling, attachments, tracking), but the batch is all or nothing: if any email is invalid, or any `from` is not a verified domain (live keys), nothing is stored and the error names the email (e.g. `emails.3.to`). One `Idempotency-Key` covers the whole batch. The emails' recipients are charged against the send quota together, so a batch with more recipients than the quota allows at once is refused (`quota_exceeded` without `Retry-After`): split it. The request body shares the 40 MB limit of a single send.", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Makes retries safe: a repeated request with the same key and body returns the original email (with its current status) instead of sending again, for 24 hours. 1-256 characters.", - "examples": [ - "welcome-user-123" - ] - }, - "required": false, - "description": "Makes retries safe: a repeated request with the same key and body returns the original email (with its current status) instead of sending again, for 24 hours. 1-256 characters.", - "name": "Idempotency-Key", - "in": "header" }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SendEmailBatchRequest" - } - } - } - }, - "responses": { - "202": { - "description": "The batch, accepted for delivery, with its emails in request order. A replay returns the original batch with the emails' current statuses.", + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38303,35 +46269,23 @@ ] } }, - "Idempotent-Replayed": { - "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", - "schema": { - "type": "string", - "enum": [ - "true" - ] - } - }, - "Coss-Version": { - "description": "The API version this response is rendered in.", + "Retry-After": { + "description": "Seconds to wait before retrying.", "schema": { - "type": "string", - "examples": [ - "2026-10-10" - ] + "type": "integer" } } }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailBatch" + "$ref": "#/components/schemas/Error" } } } }, - "400": { - "description": "The body is not valid JSON (`invalid_json`) or the `Idempotency-Key` header is invalid (`invalid_idempotency_key`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38351,8 +46305,8 @@ } } }, - "401": { - "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38371,9 +46325,42 @@ } } } + } + } + }, + "get": { + "operationId": "getDomain", + "tags": [ + "Domains" + ], + "summary": "Retrieve a sending domain", + "description": "The domain with the result of the last DNS check, so you can show which records are still missing or mismatched.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The domain's id.", + "examples": [ + "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The domain's id.", + "name": "id", + "in": "path" }, - "402": { - "description": "Live sending is paused for an overdue invoice or the spend limit (`billing_hold`), or the send would go past the plan's monthly allowance (`usage_limit_exceeded`) (`billing_error`). Never retried: it needs a plan change or a payment.", + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "The domain.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38383,25 +46370,13 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`insufficient_permissions`), its organization is suspended (`organization_suspended`), live sending is suspended (`account_suspended`), sending is paused for the organization because of its sending reputation (`sending_paused`), or a `subaccount` is turned off (`subaccount_disabled`) or paused (`subaccount_paused`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", "schema": { "type": "string", "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + "2026-10-10" ] } } @@ -38409,13 +46384,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Domain" } } } }, - "409": { - "description": "Requests with this `Idempotency-Key` are racing for an expired key (`request_in_progress`). Retry after `Retry-After` seconds.", + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38425,12 +46400,6 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Retry-After": { - "description": "Seconds to wait.", - "schema": { - "type": "integer" - } } }, "content": { @@ -38441,8 +46410,8 @@ } } }, - "413": { - "description": "The request body is larger than 40 MB (`payload_too_large`).", + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38462,8 +46431,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38483,8 +46452,8 @@ } } }, - "422": { - "description": "The body failed validation (`validation_error`, see `issues`), a `from` domain is not a verified domain of the organization (`domain_not_verified`, live keys only; `param` names the email), the `Idempotency-Key` was already used with a different body (`idempotency_key_reused`), or an email's template can't be sent (the `template_…` codes of `POST /v1/emails`; `param` names the email).", + "404": { + "description": "No domain with this id exists in this organization (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38505,7 +46474,7 @@ } }, "429": { - "description": "Too many requests with this API key (`rate_limited`), or the batch's recipients exceed the send quota (`quota_exceeded`). Retry after `Retry-After` seconds; without `Retry-After` the batch has more recipients than the quota allows at once.", + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38517,7 +46486,7 @@ } }, "Retry-After": { - "description": "Seconds to wait.", + "description": "Seconds to wait before retrying.", "schema": { "type": "integer" } @@ -38574,16 +46543,14 @@ } } } - } - }, - "/v1/emails/batch/{id}": { - "get": { - "operationId": "getEmailBatch", + }, + "delete": { + "operationId": "deleteDomain", "tags": [ - "Emails" + "Domains" ], - "summary": "Retrieve a batch of emails", - "description": "A batch sent with `POST /v1/emails/batch`, with its emails' current statuses in the order of the request. Emails past the data retention period are left out. Only batches sent from the API key's own environment can be retrieved.", + "summary": "Remove a sending domain", + "description": "Removes the domain and its provider identity. Emails already sent are unaffected, but live keys can no longer send from it and the name becomes available again.", "security": [ { "bearerAuth": [] @@ -38593,13 +46560,13 @@ { "schema": { "type": "string", - "description": "The batch's id (`batch_id` of its emails).", + "description": "The domain's id.", "examples": [ - "batch_01k8d0n6x5f3w2e1r9t7y4u6i8" + "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The batch's id (`batch_id` of its emails).", + "description": "The domain's id.", "name": "id", "in": "path" }, @@ -38609,7 +46576,7 @@ ], "responses": { "200": { - "description": "The batch.", + "description": "The domain was removed.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38633,7 +46600,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailBatch" + "$ref": "#/components/schemas/DeletedDomain" } } } @@ -38702,7 +46669,28 @@ } }, "404": { - "description": "No batch with this id exists in the API key's environment (`not_found`).", + "description": "No domain with this id exists in this organization (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The organization's single sign-on uses the domain (`domain_in_use_by_sso`): remove it from the SSO provider in the dashboard first.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38770,6 +46758,27 @@ } } }, + "502": { + "description": "The delivery provider could not remove the domain; retry (`provider_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "503": { "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { @@ -38794,14 +46803,14 @@ } } }, - "/v1/emails/{id}": { - "get": { - "operationId": "getEmail", + "/v1/domains/{id}/verify": { + "post": { + "operationId": "verifyDomain", "tags": [ - "Emails" + "Domains" ], - "summary": "Retrieve an email", - "description": "Only emails sent from the API key's own environment can be retrieved.", + "summary": "Check a domain's DNS records now", + "description": "Asks the delivery provider and DNS about the domain immediately instead of waiting for the next background check, and returns the updated domain. A `failed` domain starts verification over with fresh DKIM records, which have to be published again. Its tracking domain, if any, is checked too (and set up again if it `failed`).", "security": [ { "bearerAuth": [] @@ -38811,13 +46820,13 @@ { "schema": { "type": "string", - "description": "The email's id.", + "description": "The domain's id.", "examples": [ - "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The email's id.", + "description": "The domain's id.", "name": "id", "in": "path" }, @@ -38827,7 +46836,7 @@ ], "responses": { "200": { - "description": "The email.", + "description": "The domain, as the check left it.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38851,7 +46860,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Email" + "$ref": "#/components/schemas/Domain" } } } @@ -38920,7 +46929,28 @@ } }, "404": { - "description": "No email with this id exists in the API key's environment (`not_found`).", + "description": "No domain with this id exists in this organization (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The organization already has 5 domains awaiting verification, so a `failed` one cannot restart (`too_many_pending_domains`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38941,7 +46971,7 @@ } }, "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "description": "The domain was checked moments ago (`domain_recently_checked`), or too many requests with this API key (`rate_limited`). Retry after `Retry-After` seconds, or just read the domain.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -38953,7 +46983,7 @@ } }, "Retry-After": { - "description": "Seconds to wait before retrying.", + "description": "Seconds to wait.", "schema": { "type": "integer" } @@ -38988,6 +47018,27 @@ } } }, + "502": { + "description": "The delivery provider could not be reached; retry (`provider_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "503": { "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { @@ -39012,14 +47063,14 @@ } } }, - "/v1/emails/{id}/cancel": { + "/v1/domains/{id}/tracking": { "post": { - "operationId": "cancelEmail", + "operationId": "createTrackingDomain", "tags": [ - "Emails" + "Domains" ], - "summary": "Cancel a scheduled email", - "description": "Cancels an email sent with a future `scheduled_at` before it is sent. Canceling an already canceled email returns it unchanged. To send it at another time, reschedule it instead.", + "summary": "Add a custom tracking domain", + "description": "Sets up a subdomain of the domain (e.g. `links.acme.com`) to serve open and click tracking. Publish the CNAME record in `tracking.record`; coss.com then issues its certificate and checks it in the background, giving up after 72 hours. Open pixels and click redirects of emails sent from the domain (or its subdomains without a domain of their own) use the tracking domain while it is `active`, and the default tracking host otherwise. A domain has at most one tracking domain: remove it to use another hostname.", "security": [ { "bearerAuth": [] @@ -39029,13 +47080,13 @@ { "schema": { "type": "string", - "description": "The email's id.", + "description": "The domain's id.", "examples": [ - "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The email's id.", + "description": "The domain's id.", "name": "id", "in": "path" }, @@ -39043,9 +47094,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTrackingDomainRequest" + } + } + } + }, "responses": { "200": { - "description": "The canceled email.", + "description": "The domain, with its tracking domain and the CNAME to publish.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39069,13 +47130,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Email" + "$ref": "#/components/schemas/Domain" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39116,6 +47177,27 @@ } } }, + "402": { + "description": "The organization's plan doesn't include custom tracking domains (`plan_feature_unavailable`) (`billing_error`). Never retried: it needs a plan change or a payment.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -39138,7 +47220,49 @@ } }, "404": { - "description": "No email with this id exists in the API key's environment (`not_found`).", + "description": "No domain with this id exists in this organization (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39159,7 +47283,7 @@ } }, "422": { - "description": "The email is not scheduled (it is being sent, was sent or failed): `email_not_cancelable`.", + "description": "The body failed validation (`validation_error`), the hostname is not a subdomain of the domain it can use (`invalid_tracking_hostname`), the domain already has a tracking domain (`tracking_domain_exists`), or the hostname is already in use (`tracking_domain_taken`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39227,6 +47351,27 @@ } } }, + "502": { + "description": "The certificate provider could not set the hostname up; retry (`provider_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "503": { "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { @@ -39249,16 +47394,14 @@ } } } - } - }, - "/v1/emails/{id}/reschedule": { - "post": { - "operationId": "rescheduleEmail", + }, + "delete": { + "operationId": "deleteTrackingDomain", "tags": [ - "Emails" + "Domains" ], - "summary": "Reschedule a scheduled email", - "description": "Moves an email sent with a future `scheduled_at` to a new time, at most 30 days from now, and records an `email.rescheduled` event. A time that is not in the future sends it within seconds. Rescheduling to the time it already has returns it unchanged. The domain and the suppression list are still checked when it is sent.", + "summary": "Remove a custom tracking domain", + "description": "Removes the domain's tracking domain. Later emails use the default tracking host, but links and open pixels in emails already sent through the tracking domain stop working.", "security": [ { "bearerAuth": [] @@ -39268,13 +47411,13 @@ { "schema": { "type": "string", - "description": "The email's id.", + "description": "The domain's id.", "examples": [ - "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The email's id.", + "description": "The domain's id.", "name": "id", "in": "path" }, @@ -39282,19 +47425,9 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RescheduleEmailRequest" - } - } - } - }, "responses": { "200": { - "description": "The rescheduled email.", + "description": "The domain, without a tracking domain.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39318,13 +47451,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Email" + "$ref": "#/components/schemas/Domain" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39387,7 +47520,7 @@ } }, "404": { - "description": "No email with this id exists in the API key's environment (`not_found`).", + "description": "No domain with this id exists in this organization, or it has no tracking domain (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39407,8 +47540,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39418,26 +47551,11 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The body failed validation (`validation_error`, see `issues`), or the email is not scheduled (it is being sent, was sent, failed or was canceled): `email_not_reschedulable`.", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "type": "integer" } } }, @@ -39449,8 +47567,8 @@ } } }, - "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39460,12 +47578,6 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Retry-After": { - "description": "Seconds to wait before retrying.", - "schema": { - "type": "integer" - } } }, "content": { @@ -39476,8 +47588,8 @@ } } }, - "500": { - "description": "Unexpected server error (`api_error` / `internal_error`).", + "502": { + "description": "The certificate provider could not remove the hostname; retry (`provider_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39521,14 +47633,14 @@ } } }, - "/v1/emails/{id}/events": { - "get": { - "operationId": "listEmailEvents", + "/v1/domains/{id}/receiving": { + "post": { + "operationId": "createReceivingDomain", "tags": [ - "Emails" + "Domains" ], - "summary": "List an email's delivery events", - "description": "Every delivery event of the email, oldest first: `email.scheduled`, `email.rescheduled`, `email.canceled`, `email.sent` and `email.failed` for the whole email, and `email.delivered`, `email.delivery_delayed`, `email.bounced`, `email.complained` and `email.suppressed` (skipped because of the suppression list) per recipient, plus `email.opened` and `email.clicked` when tracking is on (attributed to a recipient only when the email had a single one) and `email.unsubscribed` after a one-click unsubscribe.", + "summary": "Receive email at a domain", + "description": "Starts receiving email at the domain itself or a subdomain (e.g. `inbound.acme.com`): email sent to any address at that hostname becomes a received email of the API key's environment (an `email.received` webhook, `GET /v1/received_emails`). Publish the MX record in `receiving.record`; coss.com checks it in the background. Using the domain itself moves all of its mail to coss.com. A domain has at most one receiving hostname: remove it to use another one.", "security": [ { "bearerAuth": [] @@ -39538,13 +47650,13 @@ { "schema": { "type": "string", - "description": "The email's id.", + "description": "The domain's id.", "examples": [ - "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The email's id.", + "description": "The domain's id.", "name": "id", "in": "path" }, @@ -39552,9 +47664,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReceivingDomainRequest" + } + } + } + }, "responses": { "200": { - "description": "The email's events.", + "description": "The domain, with its receiving domain and the MX record to publish.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39578,13 +47700,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailEventList" + "$ref": "#/components/schemas/Domain" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39647,7 +47769,70 @@ } }, "404": { - "description": "No email with this id exists in the API key's environment (`not_found`).", + "description": "No domain with this id exists in this organization (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The body failed validation (`validation_error`), the hostname is not one the domain can receive at (`invalid_receiving_hostname`), the domain already receives email (`receiving_domain_exists`), the hostname is already in use (`receiving_domain_taken`), or this deployment does not receive email (`receiving_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39737,16 +47922,14 @@ } } } - } - }, - "/v1/emails/{id}/links": { - "get": { - "operationId": "listEmailLinks", + }, + "delete": { + "operationId": "deleteReceivingDomain", "tags": [ - "Emails" + "Domains" ], - "summary": "List an email's tracked links", - "description": "The links coss.com rewrote for click tracking, in the order they appear in the HTML, with their click counts. Empty unless the email was sent with click tracking.", + "summary": "Stop receiving email at a domain", + "description": "Stops receiving at the domain's receiving hostname: later mail to it is dropped (remove the MX record too). Emails already received stay until their retention ends.", "security": [ { "bearerAuth": [] @@ -39756,13 +47939,13 @@ { "schema": { "type": "string", - "description": "The email's id.", + "description": "The domain's id.", "examples": [ - "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The email's id.", + "description": "The domain's id.", "name": "id", "in": "path" }, @@ -39772,7 +47955,7 @@ ], "responses": { "200": { - "description": "The email's tracked links.", + "description": "The domain, without a receiving domain.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39796,7 +47979,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailLinkList" + "$ref": "#/components/schemas/Domain" } } } @@ -39865,7 +48048,7 @@ } }, "404": { - "description": "No email with this id exists in the API key's environment (`not_found`).", + "description": "No domain with this id exists in this organization, or it does not receive email (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -39957,32 +48140,40 @@ } } }, - "/v1/email_templates": { - "post": { - "operationId": "createEmailTemplate", + "/v1/domains/{id}/dmarc": { + "get": { + "operationId": "getDomainDmarc", "tags": [ - "Email templates" + "Domains" ], - "summary": "Create an email template", - "description": "Creates a template with a draft holding its variables and a subject, HTML and text per locale. Sends use it once its draft is published. Not idempotent: retrying after a lost response can create a second template (unless it has an `alias`). Templates belong to the API key's own environment: a `coss_test_…` key only ever sees sandbox ones. Give a template the same `alias` in every environment to send it by that name.", + "summary": "Retrieve a domain's DMARC status", + "description": "The domain's published DMARC record, where it is on the path from `p=none` to `p=reject`, the record to publish next and whether the last 14 days of aggregate reports say that is safe (`ready`, with the `checks` behind it), and a summary of those reports. Mailbox providers send DMARC aggregate reports to the domain's `report_address` once its `_dmarc` record names it (`rua=mailto:…`), usually once a day each.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateEmailTemplateRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The domain's id.", + "examples": [ + "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The domain's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { "200": { - "description": "The template.", + "description": "The domain's DMARC status.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40006,13 +48197,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplate" + "$ref": "#/components/schemas/DomainDmarc" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40074,50 +48265,8 @@ } } }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The body failed validation (`validation_error`), another template has the alias (`template_alias_taken`), or the environment has 500 templates (`template_limit_reached`).", + "404": { + "description": "No domain with this id exists in this organization (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40206,45 +48355,56 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, + } + } + }, + "/v1/domains/{id}/dmarc/reports": { "get": { - "operationId": "listEmailTemplates", + "operationId": "listDomainDmarcReports", "tags": [ - "Email templates" + "Domains" ], - "summary": "List email templates", - "description": "The environment's templates, newest first. Pass the last `id` as `after` to get the next page while `has_more` is true.", + "summary": "List a domain's DMARC reports", + "description": "The aggregate reports received about the domain, newest first, without their rows (retrieve one for those). Pass the last `id` as `after` to get the next page while `has_more` is true. Mailbox providers send DMARC aggregate reports to the domain's `report_address` once its `_dmarc` record names it (`rua=mailto:…`), usually once a day each.", "security": [ { "bearerAuth": [] } ], "parameters": [ + { + "schema": { + "type": "string", + "description": "The domain's id.", + "examples": [ + "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The domain's id.", + "name": "id", + "in": "path" + }, { "schema": { "type": "integer", "minimum": 1, "maximum": 100, - "description": "1-100, default 50." + "description": "1-100, default 20." }, "required": false, - "description": "1-100, default 50.", + "description": "1-100, default 20.", "name": "limit", "in": "query" }, { "schema": { "type": "string", - "description": "Cursor: the `id` of the last template of the previous page." + "maxLength": 64, + "description": "Cursor: the `id` of the last report of the previous page." }, "required": false, - "description": "Cursor: the `id` of the last template of the previous page.", + "description": "Cursor: the `id` of the last report of the previous page.", "name": "after", "in": "query" }, @@ -40254,7 +48414,7 @@ ], "responses": { "200": { - "description": "A page of templates.", + "description": "A page of DMARC reports.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40278,7 +48438,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplateList" + "$ref": "#/components/schemas/DmarcReportList" } } } @@ -40346,8 +48506,29 @@ } } }, + "404": { + "description": "No domain with this id exists in this organization (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "description": "A query parameter failed validation (`validation_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40439,14 +48620,14 @@ } } }, - "/v1/email_templates/{id}": { + "/v1/domains/{id}/dmarc/reports/{report_id}": { "get": { - "operationId": "getEmailTemplate", + "operationId": "getDomainDmarcReport", "tags": [ - "Email templates" + "Domains" ], - "summary": "Retrieve an email template", - "description": "A template by id or alias. Its content is in its draft and versions.", + "summary": "Retrieve a DMARC report", + "description": "One aggregate report with its rows: each source IP, how many messages it sent, whether they passed DMARC (aligned DKIM or SPF) and what the receiver did with them.", "security": [ { "bearerAuth": [] @@ -40456,23 +48637,36 @@ { "schema": { "type": "string", - "description": "The template's id or alias.", + "description": "The domain's id.", "examples": [ - "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The template's id or alias.", + "description": "The domain's id.", "name": "id", "in": "path" }, + { + "schema": { + "type": "string", + "description": "The report's id.", + "examples": [ + "dmarcrep_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The report's id.", + "name": "report_id", + "in": "path" + }, { "$ref": "#/components/parameters/CossVersion" } ], "responses": { "200": { - "description": "The template.", + "description": "The report.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40496,7 +48690,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplate" + "$ref": "#/components/schemas/DmarcReport" } } } @@ -40565,7 +48759,7 @@ } }, "404": { - "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "description": "No such domain in this organization, or no such report of it (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40655,50 +48849,24 @@ } } } - }, - "patch": { - "operationId": "updateEmailTemplate", + } + }, + "/v1/dedicated_ips": { + "get": { + "operationId": "getDedicatedIpPool", "tags": [ - "Email templates" + "Dedicated IPs" ], - "summary": "Update an email template", - "description": "Changes the template's name, alias or description. Changing the alias changes what sends must use. Content changes go to the draft (`PUT /v1/email_templates/{id}/draft`).", + "summary": "Retrieve the organization's dedicated IPs", + "description": "The organization's dedicated IP pool (the Dedicated IPs add-on, bought on the dashboard's billing page): its status, its IPs and how much of the last week's mail they delivered. Organization-wide, like domains: every environment's key sees the same pool; only live emails go through it. Without the add-on the status is `not_enabled`.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The template's id or alias.", - "examples": [ - "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The template's id or alias.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateEmailTemplateRequest" - } - } - } - }, "responses": { "200": { - "description": "The template.", + "description": "The organization's dedicated IPs.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40722,13 +48890,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplate" + "$ref": "#/components/schemas/DedicatedIpPool" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40790,8 +48958,8 @@ } } }, - "404": { - "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40801,26 +48969,11 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "type": "integer" } } }, @@ -40832,8 +48985,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40853,8 +49006,8 @@ } } }, - "422": { - "description": "The body failed validation (`validation_error`), or another template has the alias (`template_alias_taken`).", + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40873,9 +49026,41 @@ } } } - }, - "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + } + }, + "/v1/webhook_endpoints": { + "post": { + "operationId": "createWebhookEndpoint", + "tags": [ + "Webhook endpoints" + ], + "summary": "Create a webhook endpoint", + "description": "Registers a URL that receives signed event notifications. Webhook endpoints belong to the API key's own environment: a `coss_test_…` key only ever sees sandbox endpoints, and a `coss_live_…` key only production ones. The response carries the signing secret, which is returned only here and when it is rolled: store it, and verify every delivery with it (`verifyWebhook` in the SDK).", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookEndpointRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The endpoint and its signing secret.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40886,30 +49071,12 @@ ] } }, - "Retry-After": { - "description": "Seconds to wait before retrying.", - "schema": { - "type": "integer" - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "500": { - "description": "Unexpected server error (`api_error` / `internal_error`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", + "Coss-Version": { + "description": "The API version this response is rendered in.", "schema": { "type": "string", "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + "2026-10-10" ] } } @@ -40917,13 +49084,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/WebhookEndpointWithSecret" } } } }, - "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "400": { + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40942,42 +49109,9 @@ } } } - } - } - }, - "delete": { - "operationId": "deleteEmailTemplate", - "tags": [ - "Email templates" - ], - "summary": "Delete an email template", - "description": "Deletes the template with its draft and every version. Emails already sent or scheduled from it keep their content; new sends naming it fail with `template_not_found`.", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The template's id or alias.", - "examples": [ - "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The template's id or alias.", - "name": "id", - "in": "path" }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], - "responses": { - "200": { - "description": "The template was deleted.", + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40987,27 +49121,18 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Coss-Version": { - "description": "The API version this response is rendered in.", - "schema": { - "type": "string", - "examples": [ - "2026-10-10" - ] - } } }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedEmailTemplate" + "$ref": "#/components/schemas/Error" } } } }, - "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41027,8 +49152,8 @@ } } }, - "401": { - "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41048,8 +49173,8 @@ } } }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41069,8 +49194,8 @@ } } }, - "404": { - "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "422": { + "description": "The body failed validation (`validation_error`, including an unknown `api_version`), the URL is not a usable https:// URL (`invalid_url`), or the environment already has 10 endpoints (`too_many_endpoints`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41159,43 +49284,28 @@ } } } - } - } - }, - "/v1/email_templates/{id}/draft": { + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, "get": { - "operationId": "getEmailTemplateDraft", + "operationId": "listWebhookEndpoints", "tags": [ - "Email templates" + "Webhook endpoints" ], - "summary": "Retrieve a template's draft", - "description": "The draft (`version: null`): what the next publish makes sends use. `problems` lists syntax errors and undeclared variables, which keep it from being published.", + "summary": "List webhook endpoints", + "description": "Every endpoint of the API key's environment, oldest first. Webhook endpoints belong to the API key's own environment: a `coss_test_…` key only ever sees sandbox endpoints, and a `coss_live_…` key only production ones.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The template's id or alias.", - "examples": [ - "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The template's id or alias.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], "responses": { "200": { - "description": "The draft.", + "description": "The environment's webhook endpoints.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41219,7 +49329,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplateVersion" + "$ref": "#/components/schemas/WebhookEndpointList" } } } @@ -41287,27 +49397,6 @@ } } }, - "404": { - "description": "No template with this id or alias exists in the API key's environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -41377,15 +49466,22 @@ } } } - } - }, - "put": { - "operationId": "updateEmailTemplateDraft", + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + } + }, + "/v1/webhook_endpoints/{id}": { + "get": { + "operationId": "getWebhookEndpoint", "tags": [ - "Email templates" + "Webhook endpoints" ], - "summary": "Update a template's draft", - "description": "Replaces the draft's variables and content. Sends keep using the published version until the draft is published. Pass the `revision` you read to refuse overwriting someone else's changes. A draft with problems is saved (and reported in `problems`).", + "summary": "Retrieve a webhook endpoint", + "description": "The endpoint without its signing secret, which is only returned when created or rolled.", "security": [ { "bearerAuth": [] @@ -41395,13 +49491,13 @@ { "schema": { "type": "string", - "description": "The template's id or alias.", + "description": "The webhook endpoint's id.", "examples": [ - "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The template's id or alias.", + "description": "The webhook endpoint's id.", "name": "id", "in": "path" }, @@ -41409,19 +49505,9 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateEmailTemplateDraftRequest" - } - } - } - }, "responses": { "200": { - "description": "The saved draft.", + "description": "The webhook endpoint.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41445,13 +49531,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplateVersion" + "$ref": "#/components/schemas/WebhookEndpoint" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41514,70 +49600,7 @@ } }, "404": { - "description": "No template with this id or alias exists in the API key's environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The body failed validation (`validation_error`), or the draft changed since `revision` (`template_revision_mismatch`).", + "description": "No webhook endpoint with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41667,16 +49690,14 @@ } } } - } - }, - "/v1/email_templates/{id}/publish": { - "post": { - "operationId": "publishEmailTemplate", + }, + "patch": { + "operationId": "updateWebhookEndpoint", "tags": [ - "Email templates" + "Webhook endpoints" ], - "summary": "Publish a template's draft", - "description": "Copies the draft into the next version, which sends use from then on. Emails already accepted keep the content they were rendered with. A draft without changes since the last publish returns that version, so retrying with the `revision` you reviewed is safe (without it, a retry could publish edits saved in between).", + "summary": "Update a webhook endpoint", + "description": "Changes the URL, description, subscribed event types, API version or `enabled` flag. Omitted fields keep their current value, and the signing secret stays the same (see Roll a signing secret), so receivers keep verifying deliveries. Disabling an endpoint stops deliveries without losing its configuration.", "security": [ { "bearerAuth": [] @@ -41686,13 +49707,13 @@ { "schema": { "type": "string", - "description": "The template's id or alias.", + "description": "The webhook endpoint's id.", "examples": [ - "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The template's id or alias.", + "description": "The webhook endpoint's id.", "name": "id", "in": "path" }, @@ -41705,14 +49726,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublishEmailTemplateRequest" + "$ref": "#/components/schemas/UpdateWebhookEndpointRequest" } } } }, "responses": { "200": { - "description": "The published version.", + "description": "The updated endpoint.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41736,7 +49757,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplateVersion" + "$ref": "#/components/schemas/WebhookEndpoint" } } } @@ -41805,7 +49826,7 @@ } }, "404": { - "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "description": "No webhook endpoint with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41868,7 +49889,7 @@ } }, "422": { - "description": "The draft has problems or no content for its default locale (`template_invalid`, see `issues`), or changed since `revision` (`template_revision_mismatch`).", + "description": "The body failed validation (`validation_error`) or the URL is not a usable https:// URL (`invalid_url`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41958,16 +49979,14 @@ } } } - } - }, - "/v1/email_templates/{id}/render": { - "post": { - "operationId": "renderEmailTemplate", + }, + "delete": { + "operationId": "deleteWebhookEndpoint", "tags": [ - "Email templates" + "Webhook endpoints" ], - "summary": "Render a template", - "description": "Renders the draft (or a published `version`) with `variables`, exactly as a send would, and returns the subject, HTML and text: a preview, nothing is sent or stored. Without `variables` it renders the version's `sample_variables`.", + "summary": "Delete a webhook endpoint", + "description": "Removes the endpoint and its delivery log. Deliveries already queued for it are dropped. To stop deliveries temporarily, set `enabled` to `false` instead.", "security": [ { "bearerAuth": [] @@ -41977,13 +49996,13 @@ { "schema": { "type": "string", - "description": "The template's id or alias.", + "description": "The webhook endpoint's id.", "examples": [ - "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The template's id or alias.", + "description": "The webhook endpoint's id.", "name": "id", "in": "path" }, @@ -41991,19 +50010,9 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RenderEmailTemplateRequest" - } - } - } - }, "responses": { "200": { - "description": "The rendered template.", + "description": "The endpoint was deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42027,13 +50036,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RenderedEmailTemplate" + "$ref": "#/components/schemas/DeletedWebhookEndpoint" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42096,70 +50105,7 @@ } }, "404": { - "description": "No template with this id or alias exists in the API key's environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The body failed validation (`validation_error`), the draft has problems (`template_invalid`, see `issues`), the variables don't match its declarations (`invalid_template_variables`), it has no such version (`template_version_not_found`), or it doesn't render (`template_render_failed`).", + "description": "No webhook endpoint with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42251,14 +50197,14 @@ } } }, - "/v1/email_templates/{id}/versions": { - "get": { - "operationId": "listEmailTemplateVersions", + "/v1/webhook_endpoints/{id}/roll_secret": { + "post": { + "operationId": "rollWebhookEndpointSecret", "tags": [ - "Email templates" + "Webhook endpoints" ], - "summary": "List a template's versions", - "description": "Published versions, newest first. Pass the last `version` as `after` to get the next page while `has_more` is true.", + "summary": "Roll a signing secret", + "description": "Replaces the endpoint's signing secret with a new one, returned in the response (the only time it is). Until `previous_secret_expires_at`, every delivery is signed with both secrets (`webhook-signature` lists two signatures), so receivers verifying with either one accept it: deploy the new secret, then let the old one expire. Rolling again in that time stops the older secret at once. Not idempotent: after a lost response, roll again.", "security": [ { "bearerAuth": [] @@ -42268,46 +50214,33 @@ { "schema": { "type": "string", - "description": "The template's id or alias.", + "description": "The webhook endpoint's id.", "examples": [ - "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The template's id or alias.", + "description": "The webhook endpoint's id.", "name": "id", "in": "path" }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 20, - "description": "1-20, default 10." - }, - "required": false, - "description": "1-20, default 10.", - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "description": "Cursor: the `version` of the last version of the previous page." - }, - "required": false, - "description": "Cursor: the `version` of the last version of the previous page.", - "name": "after", - "in": "query" - }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RollWebhookEndpointSecretRequest" + } + } + } + }, "responses": { "200": { - "description": "A page of versions.", + "description": "The endpoint and its new signing secret.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42331,13 +50264,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplateVersionList" + "$ref": "#/components/schemas/WebhookEndpointWithSecret" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42400,7 +50333,49 @@ } }, "404": { - "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "description": "No webhook endpoint with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42421,7 +50396,7 @@ } }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "description": "The body failed validation (`validation_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42513,14 +50488,14 @@ } } }, - "/v1/email_templates/{id}/versions/{version}": { - "get": { - "operationId": "getEmailTemplateVersion", + "/v1/webhook_endpoints/{id}/recover": { + "post": { + "operationId": "recoverWebhookEndpoint", "tags": [ - "Email templates" + "Webhook endpoints" ], - "summary": "Retrieve a template version", - "description": "A published version, which never changes.", + "summary": "Recover failed deliveries", + "description": "Queues the endpoint's deliveries that `failed` since `since` for one more attempt each, oldest first, spread over the next minute, e.g. after fixing a receiver that was down. At most 1000 per call: while `has_more` is `true`, call again with the same `since`. The endpoint must be enabled.", "security": [ { "bearerAuth": [] @@ -42530,37 +50505,33 @@ { "schema": { "type": "string", - "description": "The template's id or alias.", + "description": "The webhook endpoint's id.", "examples": [ - "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The template's id or alias.", + "description": "The webhook endpoint's id.", "name": "id", "in": "path" }, - { - "schema": { - "type": "integer", - "minimum": 1, - "description": "A published version number.", - "examples": [ - 1 - ] - }, - "required": true, - "description": "A published version number.", - "name": "version", - "in": "path" - }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecoverWebhookEndpointRequest" + } + } + } + }, "responses": { "200": { - "description": "The version.", + "description": "How many deliveries were queued.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42584,13 +50555,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplateVersion" + "$ref": "#/components/schemas/WebhookEndpointRecovery" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42653,7 +50624,70 @@ } }, "404": { - "description": "No template with this id or alias exists in the API key's environment, or it has no such version (`not_found`).", + "description": "No webhook endpoint with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The body failed validation (`validation_error`), or the endpoint is disabled (`endpoint_disabled`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42745,14 +50779,14 @@ } } }, - "/v1/email_templates/{id}/versions/{version}/restore": { - "post": { - "operationId": "restoreEmailTemplateVersion", + "/v1/webhook_endpoints/{id}/deliveries": { + "get": { + "operationId": "listWebhookDeliveries", "tags": [ - "Email templates" + "Webhook endpoints" ], - "summary": "Restore a template version", - "description": "Replaces the draft with a published version's variables and content. Publish the draft to make sends use it again.", + "summary": "List an endpoint's deliveries", + "description": "The endpoint's delivery log, newest first: one delivery per event sent to it, with every HTTP attempt and the payload as sent. Pass the last `id` as `after` to get the next page while `has_more` is true. Deleting the endpoint deletes its log.", "security": [ { "bearerAuth": [] @@ -42762,29 +50796,62 @@ { "schema": { "type": "string", - "description": "The template's id or alias.", + "description": "The webhook endpoint's id.", "examples": [ - "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The template's id or alias.", + "description": "The webhook endpoint's id.", "name": "id", "in": "path" }, + { + "schema": { + "type": "string", + "enum": [ + "pending", + "succeeded", + "failed" + ], + "description": "Only deliveries with this status." + }, + "required": false, + "description": "Only deliveries with this status.", + "name": "status", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Only the delivery of this event." + }, + "required": false, + "description": "Only the delivery of this event.", + "name": "event_id", + "in": "query" + }, { "schema": { "type": "integer", "minimum": 1, - "description": "A published version number.", - "examples": [ - 1 - ] + "maximum": 100, + "description": "1-100, default 50." }, - "required": true, - "description": "A published version number.", - "name": "version", - "in": "path" + "required": false, + "description": "1-100, default 50.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Cursor: the `id` of the last delivery of the previous page." + }, + "required": false, + "description": "Cursor: the `id` of the last delivery of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -42792,7 +50859,7 @@ ], "responses": { "200": { - "description": "The draft.", + "description": "A page of deliveries.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42816,7 +50883,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplateVersion" + "$ref": "#/components/schemas/WebhookDeliveryList" } } } @@ -42885,7 +50952,7 @@ } }, "404": { - "description": "No template with this id or alias exists in the API key's environment (`not_found`).", + "description": "No webhook endpoint with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42906,7 +50973,7 @@ } }, "422": { - "description": "The template has no such version (`template_version_not_found`).", + "description": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42998,32 +51065,53 @@ } } }, - "/v1/domains": { - "post": { - "operationId": "createDomain", + "/v1/webhook_endpoints/{id}/deliveries/{delivery_id}": { + "get": { + "operationId": "getWebhookDelivery", "tags": [ - "Domains" + "Webhook endpoints" ], - "summary": "Add a sending domain", - "description": "Claims the domain for the organization and returns the DNS records to publish. Domains belong to the organization, not to one environment, so every API key of the organization sees the same domains. The records are checked repeatedly in the background and verification gives up after 72 hours; until the domain is `verified`, live keys cannot send from it.", + "summary": "Retrieve a delivery", + "description": "One delivery of the endpoint, with every HTTP attempt and the payload as sent.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateDomainRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The webhook endpoint's id.", + "examples": [ + "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The webhook endpoint's id.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "The delivery's id.", + "examples": [ + "whdel_01k7c8n2b5d7f9h1k3m5p7r9tv" + ] + }, + "required": true, + "description": "The delivery's id.", + "name": "delivery_id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { "200": { - "description": "The domain, with the DNS records to publish.", + "description": "The delivery.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43047,13 +51135,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/WebhookDelivery" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43094,27 +51182,6 @@ } } }, - "402": { - "description": "The organization's plan allows no more domains (`plan_limit_reached`) (`billing_error`). Never retried: it needs a plan change or a payment.", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -43136,50 +51203,8 @@ } } }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The body failed validation (`validation_error`), the name is not a usable domain (`invalid_domain`), it is already in use here or by another account (`domain_taken`), or The organization already has 5 domains awaiting verification (`too_many_pending_domains`). For a sub-account: it doesn't exist in the API key's environment (`subaccount_not_found`), isn't live (`subaccount_not_live`), or has 10 domains already (`too_many_subaccount_domains`).", + "404": { + "description": "No webhook endpoint with this id exists in the API key's environment, or the endpoint has no delivery with this id (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43247,27 +51272,6 @@ } } }, - "502": { - "description": "The delivery provider could not set the domain up; retry (`provider_error`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "503": { "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { @@ -43289,20 +51293,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, - "get": { - "operationId": "listDomains", + } + } + }, + "/v1/webhook_endpoints/{id}/deliveries/{delivery_id}/retry": { + "post": { + "operationId": "retryWebhookDelivery", "tags": [ - "Domains" + "Webhook endpoints" ], - "summary": "List sending domains", - "description": "Every domain of the organization, ordered by name. Domains belong to the organization, not to one environment, so every API key of the organization sees the same domains.", + "summary": "Retry a delivery", + "description": "Queues a finished delivery (`succeeded` or `failed`) for one more attempt, made within seconds, signed with the endpoint's current secret and sent to its current URL. A delivery that is still `pending` is returned unchanged: it is already scheduled for `next_attempt_at` (automatic retries stop after 8 attempts).", "security": [ { "bearerAuth": [] @@ -43312,14 +51313,28 @@ { "schema": { "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Only this sub-account's domains (its id or your uid)." + "description": "The webhook endpoint's id.", + "examples": [ + "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "required": false, - "description": "Only this sub-account's domains (its id or your uid).", - "name": "subaccount", - "in": "query" + "required": true, + "description": "The webhook endpoint's id.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "The delivery's id.", + "examples": [ + "whdel_01k7c8n2b5d7f9h1k3m5p7r9tv" + ] + }, + "required": true, + "description": "The delivery's id.", + "name": "delivery_id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -43327,7 +51342,7 @@ ], "responses": { "200": { - "description": "The organization's domains.", + "description": "The delivery, now `pending`.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43351,7 +51366,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DomainList" + "$ref": "#/components/schemas/WebhookDelivery" } } } @@ -43419,8 +51434,8 @@ } } }, - "422": { - "description": "`subaccount` names no sub-account of the API key's environment (`subaccount_not_found`).", + "404": { + "description": "No webhook endpoint with this id exists in the API key's environment, or the endpoint has no delivery with this id (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43512,50 +51527,32 @@ } } }, - "/v1/domains/{id}": { - "patch": { - "operationId": "updateDomain", + "/v1/webhook_listeners": { + "post": { + "operationId": "createWebhookListener", "tags": [ - "Domains" + "Webhook endpoints" ], - "summary": "Update a sending domain", - "description": "Shares one of the organization's own domains with its sub-accounts (they may then send from it, through the organization's reputation), or stops sharing it.", + "summary": "Open a webhook listener", + "description": "Listeners are how `coss listen` forwards events to a local server: coss.com can't reach `localhost`, so the CLI pulls them. They belong to the API key's environment, receive events created after they open, and don't count toward the plan's webhook endpoints. A listener closes when it is deleted or after 10 minutes without a poll; an environment has at most 5 open at once. Events reach it unsigned: the CLI signs what it forwards with its own secret.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The domain's id.", - "examples": [ - "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The domain's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateDomainRequest" + "$ref": "#/components/schemas/CreateWebhookListenerRequest" } } } }, "responses": { "200": { - "description": "The domain.", + "description": "The listener.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43579,7 +51576,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/WebhookListener" } } } @@ -43647,27 +51644,6 @@ } } }, - "404": { - "description": "No domain with this id exists in this organization (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "413": { "description": "The request body is larger than 5 MB (`payload_too_large`).", "headers": { @@ -43711,7 +51687,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`), or the domain is a sub-account's (`not_shareable`).", + "description": "The body failed validation (`validation_error`, including an unknown `api_version`), or the environment already has 5 open listeners (`too_many_listeners`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43800,15 +51776,22 @@ } } } - } - }, - "get": { - "operationId": "getDomain", + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + } + }, + "/v1/webhook_listeners/{id}/poll": { + "post": { + "operationId": "pollWebhookListener", "tags": [ - "Domains" + "Webhook endpoints" ], - "summary": "Retrieve a sending domain", - "description": "The domain with the result of the last DNS check, so you can show which records are still missing or mismatched.", + "summary": "Poll a webhook listener", + "description": "Acknowledges deliveries handed out earlier, keeps the listener open for another 10 minutes, and waits up to `wait_ms` (at most 25 s) for events. Deliveries come oldest first; one not acknowledged within two minutes is handed out again (with the same `event_id`). The answer may come early, with no deliveries, when the API restarts: poll again. Polls are not request-logged; each acknowledgement is logged as a `webhook.forward` line.", "security": [ { "bearerAuth": [] @@ -43818,13 +51801,13 @@ { "schema": { "type": "string", - "description": "The domain's id.", + "description": "The webhook listener's id.", "examples": [ - "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" + "whlis_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The domain's id.", + "description": "The webhook listener's id.", "name": "id", "in": "path" }, @@ -43832,9 +51815,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PollWebhookListenerRequest" + } + } + } + }, "responses": { "200": { - "description": "The domain.", + "description": "Deliveries to forward (possibly none).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43858,13 +51851,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/WebhookListenerPoll" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43927,7 +51920,70 @@ } }, "404": { - "description": "No domain with this id exists in this organization (`not_found`).", + "description": "No open webhook listener with this id exists in the API key's environment: it was closed, or expired after 10 minutes without a poll (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The body failed validation (`validation_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44017,14 +52073,16 @@ } } } - }, + } + }, + "/v1/webhook_listeners/{id}": { "delete": { - "operationId": "deleteDomain", + "operationId": "deleteWebhookListener", "tags": [ - "Domains" + "Webhook endpoints" ], - "summary": "Remove a sending domain", - "description": "Removes the domain and its provider identity. Emails already sent are unaffected, but live keys can no longer send from it and the name becomes available again.", + "summary": "Close a webhook listener", + "description": "Closes the listener: it receives nothing more, and deliveries it was still holding are dropped. `coss listen` closes its listener when it exits.", "security": [ { "bearerAuth": [] @@ -44034,13 +52092,13 @@ { "schema": { "type": "string", - "description": "The domain's id.", + "description": "The webhook listener's id.", "examples": [ - "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" + "whlis_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The domain's id.", + "description": "The webhook listener's id.", "name": "id", "in": "path" }, @@ -44050,7 +52108,7 @@ ], "responses": { "200": { - "description": "The domain was removed.", + "description": "The listener was closed.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44074,7 +52132,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedDomain" + "$ref": "#/components/schemas/DeletedWebhookListener" } } } @@ -44143,28 +52201,7 @@ } }, "404": { - "description": "No domain with this id exists in this organization (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The organization's single sign-on uses the domain (`domain_in_use_by_sso`): remove it from the SSO provider in the dashboard first.", + "description": "No open webhook listener with this id exists in the API key's environment: it was closed, or expired after 10 minutes without a poll (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44232,27 +52269,6 @@ } } }, - "502": { - "description": "The delivery provider could not remove the domain; retry (`provider_error`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "503": { "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { @@ -44277,40 +52293,32 @@ } } }, - "/v1/domains/{id}/verify": { + "/v1/webhook_events/test": { "post": { - "operationId": "verifyDomain", + "operationId": "createTestWebhookEvent", "tags": [ - "Domains" + "Webhook endpoints" ], - "summary": "Check a domain's DNS records now", - "description": "Asks the delivery provider and DNS about the domain immediately instead of waiting for the next background check, and returns the updated domain. A `failed` domain starts verification over with fresh DKIM records, which have to be published again. Its tracking domain, if any, is checked too (and set up again if it `failed`).", + "summary": "Send a test event", + "description": "Sends a sample event of a type to the environment's enabled endpoints subscribed to it and its open listeners (`coss listen`), exactly as a real one is sent: same body, signature, retries and log lines. `coss trigger` calls it. Sandbox environments only. The ids in the sample are made up (pass real ones in `data` for a handler that looks them up), and nothing marks the event as a test: your handler takes it like the real thing. When nothing is subscribed, the event goes nowhere and isn't kept.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The domain's id.", - "examples": [ - "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The domain's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTestWebhookEventRequest" + } + } } - ], + }, "responses": { "200": { - "description": "The domain, as the check left it.", + "description": "The event that was sent, and to how many receivers.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44334,13 +52342,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/WebhookTestEvent" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44382,7 +52390,7 @@ } }, "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "description": "The API key lacks the `webhook:create` permission, its organization is suspended, or it belongs to a live environment (`sandbox_only`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44402,8 +52410,8 @@ } } }, - "404": { - "description": "No domain with this id exists in this organization (`not_found`).", + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44423,8 +52431,8 @@ } } }, - "422": { - "description": "The organization already has 5 domains awaiting verification, so a `failed` one cannot restart (`too_many_pending_domains`).", + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44444,8 +52452,8 @@ } } }, - "429": { - "description": "The domain was checked moments ago (`domain_recently_checked`), or too many requests with this API key (`rate_limited`). Retry after `Retry-After` seconds, or just read the domain.", + "422": { + "description": "The body failed validation (`validation_error`), the type only reaches live environments (`live_only_event_type`: `domain.*`, `account.*`, `billing.*`), or `data` doesn't fit the type (`invalid_test_event`, with `issues`; unknown fields included).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44455,12 +52463,6 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Retry-After": { - "description": "Seconds to wait.", - "schema": { - "type": "integer" - } } }, "content": { @@ -44471,8 +52473,8 @@ } } }, - "500": { - "description": "Unexpected server error (`api_error` / `internal_error`).", + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44482,6 +52484,12 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } } }, "content": { @@ -44492,8 +52500,8 @@ } } }, - "502": { - "description": "The delivery provider could not be reached; retry (`provider_error`).", + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44534,53 +52542,40 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/domains/{id}/tracking": { + "/v1/suppressions": { "post": { - "operationId": "createTrackingDomain", + "operationId": "createSuppression", "tags": [ - "Domains" + "Suppressions" ], - "summary": "Add a custom tracking domain", - "description": "Sets up a subdomain of the domain (e.g. `links.acme.com`) to serve open and click tracking. Publish the CNAME record in `tracking.record`; coss.com then issues its certificate and checks it in the background, giving up after 72 hours. Open pixels and click redirects of emails sent from the domain (or its subdomains without a domain of their own) use the tracking domain while it is `active`, and the default tracking host otherwise. A domain has at most one tracking domain: remove it to use another hostname.", + "summary": "Add an address to the suppression list", + "description": "Suppresses an address in the API key's environment: later sends skip it, either for every email (`scope: \"*\"`, the default) or only for emails with the given `category` (e.g. to back an unsubscribe link). Adding an address that is already suppressed with the same scope returns the existing entry with status 200.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The domain's id.", - "examples": [ - "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The domain's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateTrackingDomainRequest" + "$ref": "#/components/schemas/CreateSuppressionRequest" } } } }, "responses": { "200": { - "description": "The domain, with its tracking domain and the CNAME to publish.", + "description": "The address already had an entry with this scope.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44604,13 +52599,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/Suppression" } } } }, - "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "201": { + "description": "The new entry.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44620,18 +52615,27 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } } }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/Suppression" } } } }, - "401": { - "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "400": { + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44651,8 +52655,8 @@ } } }, - "402": { - "description": "The organization's plan doesn't include custom tracking domains (`plan_feature_unavailable`) (`billing_error`). Never retried: it needs a plan change or a payment.", + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44694,7 +52698,7 @@ } }, "404": { - "description": "No domain with this id exists in this organization (`not_found`).", + "description": "`subaccount` names no sub-account of the API key's environment (`subaccount_not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44757,7 +52761,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`), the hostname is not a subdomain of the domain it can use (`invalid_tracking_hostname`), the domain already has a tracking domain (`tracking_domain_exists`), or the hostname is already in use (`tracking_domain_taken`).", + "description": "The body failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44825,27 +52829,6 @@ } } }, - "502": { - "description": "The certificate provider could not set the hostname up; retry (`provider_error`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "503": { "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { @@ -44867,15 +52850,20 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] }, - "delete": { - "operationId": "deleteTrackingDomain", + "get": { + "operationId": "listSuppressions", "tags": [ - "Domains" + "Suppressions" ], - "summary": "Remove a custom tracking domain", - "description": "Removes the domain's tracking domain. Later emails use the default tracking host, but links and open pixels in emails already sent through the tracking domain stop working.", + "summary": "List suppressed addresses", + "description": "The suppression list of the API key's environment, newest first. Pass the last `id` as `after` to get the next page while `has_more` is true.", "security": [ { "bearerAuth": [] @@ -44885,15 +52873,76 @@ { "schema": { "type": "string", - "description": "The domain's id.", + "minLength": 1, + "maxLength": 256, + "description": "List a sub-account's own list (its id or your uid) instead of the environment's.", "examples": [ - "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" + "user_8812" ] }, - "required": true, - "description": "The domain's id.", - "name": "id", - "in": "path" + "required": false, + "description": "List a sub-account's own list (its id or your uid) instead of the environment's.", + "name": "subaccount", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Only entries for this address." + }, + "required": false, + "description": "Only entries for this address.", + "name": "address", + "in": "query" + }, + { + "schema": { + "anyOf": [ + { + "type": "string", + "enum": [ + "*" + ] + }, + { + "type": "string", + "enum": [ + "stream:marketing" + ] + }, + { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$" + } + ], + "description": "Only entries with this scope." + }, + "required": false, + "description": "Only entries with this scope.", + "name": "scope", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "1-100, default 50." + }, + "required": false, + "description": "1-100, default 50.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Cursor: the `id` of the last entry of the previous page." + }, + "required": false, + "description": "Cursor: the `id` of the last entry of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -44901,7 +52950,7 @@ ], "responses": { "200": { - "description": "The domain, without a tracking domain.", + "description": "A page of entries.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44925,7 +52974,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/SuppressionList" } } } @@ -44994,7 +53043,7 @@ } }, "404": { - "description": "No domain with this id exists in this organization, or it has no tracking domain (`not_found`).", + "description": "`subaccount` names no sub-account of the API key's environment (`subaccount_not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45014,8 +53063,8 @@ } } }, - "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "422": { + "description": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45025,12 +53074,6 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Retry-After": { - "description": "Seconds to wait before retrying.", - "schema": { - "type": "integer" - } } }, "content": { @@ -45041,8 +53084,8 @@ } } }, - "500": { - "description": "Unexpected server error (`api_error` / `internal_error`).", + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45052,6 +53095,12 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } } }, "content": { @@ -45062,8 +53111,8 @@ } } }, - "502": { - "description": "The certificate provider could not remove the hostname; retry (`provider_error`).", + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45107,14 +53156,14 @@ } } }, - "/v1/domains/{id}/receiving": { - "post": { - "operationId": "createReceivingDomain", + "/v1/suppressions/check": { + "get": { + "operationId": "checkSuppression", "tags": [ - "Domains" + "Suppressions" ], - "summary": "Receive email at a domain", - "description": "Starts receiving email at the domain itself or a subdomain (e.g. `inbound.acme.com`): email sent to any address at that hostname becomes a received email of the API key's environment (an `email.received` webhook, `GET /v1/received_emails`). Publish the MX record in `receiving.record`; coss.com checks it in the background. Using the domain itself moves all of its mail to coss.com. A domain has at most one receiving hostname: remove it to use another one.", + "summary": "Check whether an address is suppressed", + "description": "Whether a send to `address` (with `category`, if given) would skip it, and the entries that match: those with scope `*` and, with a `category`, those for that category.", "security": [ { "bearerAuth": [] @@ -45124,33 +53173,60 @@ { "schema": { "type": "string", - "description": "The domain's id.", + "description": "The address to check.", "examples": [ - "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" + "user@example.com" ] }, "required": true, - "description": "The domain's id.", - "name": "id", - "in": "path" + "description": "The address to check.", + "name": "address", + "in": "query" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "The category of the email you plan to send. Without it, only `*` entries count." + }, + "required": false, + "description": "The category of the email you plan to send. Without it, only `*` entries count.", + "name": "category", + "in": "query" + }, + { + "schema": { + "type": "string", + "enum": [ + "transactional", + "marketing" + ], + "description": "The stream of the email you plan to send (default `transactional`). `marketing` also counts `stream:marketing` entries." + }, + "required": false, + "description": "The stream of the email you plan to send (default `transactional`). `marketing` also counts `stream:marketing` entries.", + "name": "stream", + "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "The sub-account you plan to send for (its id or your uid): its own entries count too." + }, + "required": false, + "description": "The sub-account you plan to send for (its id or your uid): its own entries count too.", + "name": "subaccount", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateReceivingDomainRequest" - } - } - } - }, "responses": { "200": { - "description": "The domain, with its receiving domain and the MX record to publish.", + "description": "The result.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45174,13 +53250,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/SuppressionCheck" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45243,49 +53319,7 @@ } }, "404": { - "description": "No domain with this id exists in this organization (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "description": "`subaccount` names no sub-account of the API key's environment (`subaccount_not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45306,7 +53340,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`), the hostname is not one the domain can receive at (`invalid_receiving_hostname`), the domain already receives email (`receiving_domain_exists`), the hostname is already in use (`receiving_domain_taken`), or this deployment does not receive email (`receiving_unavailable`).", + "description": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45396,40 +53430,34 @@ } } } - }, - "delete": { - "operationId": "deleteReceivingDomain", + } + }, + "/v1/suppressions/import": { + "post": { + "operationId": "importSuppressions", "tags": [ - "Domains" + "Suppressions" ], - "summary": "Stop receiving email at a domain", - "description": "Stops receiving at the domain's receiving hostname: later mail to it is dropped (remove the MX record too). Emails already received stay until their retention ends.", + "summary": "Import suppressions in bulk", + "description": "Adds up to 1000 entries at once, with reason `import`. Each entry is validated on its own: invalid ones are skipped and listed in `invalid`, entries already on the list are left unchanged and counted in `existing`. Importing again is safe.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The domain's id.", - "examples": [ - "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The domain's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportSuppressionsRequest" + } + } } - ], + }, "responses": { "200": { - "description": "The domain, without a receiving domain.", + "description": "What the import did.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45453,13 +53481,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/SuppressionImport" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45522,162 +53550,7 @@ } }, "404": { - "description": "No domain with this id exists in this organization, or it does not receive email (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - }, - "Retry-After": { - "description": "Seconds to wait before retrying.", - "schema": { - "type": "integer" - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "500": { - "description": "Unexpected server error (`api_error` / `internal_error`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - }, - "/v1/domains/{id}/dmarc": { - "get": { - "operationId": "getDomainDmarc", - "tags": [ - "Domains" - ], - "summary": "Retrieve a domain's DMARC status", - "description": "The domain's published DMARC record, where it is on the path from `p=none` to `p=reject`, the record to publish next and whether the last 14 days of aggregate reports say that is safe (`ready`, with the `checks` behind it), and a summary of those reports. Mailbox providers send DMARC aggregate reports to the domain's `report_address` once its `_dmarc` record names it (`rua=mailto:…`), usually once a day each.", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The domain's id.", - "examples": [ - "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The domain's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], - "responses": { - "200": { - "description": "The domain's DMARC status.", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - }, - "Coss-Version": { - "description": "The API version this response is rendered in.", - "schema": { - "type": "string", - "examples": [ - "2026-10-10" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DomainDmarc" - } - } - } - }, - "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "`subaccount` names no sub-account of the API key's environment (`subaccount_not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45697,8 +53570,8 @@ } } }, - "401": { - "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45718,8 +53591,8 @@ } } }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45739,8 +53612,8 @@ } } }, - "404": { - "description": "No domain with this id exists in this organization (`not_found`).", + "422": { + "description": "The body is not a list of 1-1000 `{ address, scope? }` objects (`validation_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45829,17 +53702,22 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/domains/{id}/dmarc/reports": { + "/v1/suppressions/export": { "get": { - "operationId": "listDomainDmarcReports", + "operationId": "exportSuppressions", "tags": [ - "Domains" + "Suppressions" ], - "summary": "List a domain's DMARC reports", - "description": "The aggregate reports received about the domain, newest first, without their rows (retrieve one for those). Pass the last `id` as `after` to get the next page while `has_more` is true. Mailbox providers send DMARC aggregate reports to the domain's `report_address` once its `_dmarc` record names it (`rua=mailto:…`), usually once a day each.", + "summary": "Export the suppression list as CSV", + "description": "The whole suppression list of the API key's environment (optionally filtered), oldest first, as CSV with the columns `address,scope,reason,created_at,email_id,id`. The file can be imported again as is. Values a spreadsheet would run as a formula are prefixed with `'`.", "security": [ { "bearerAuth": [] @@ -45849,37 +53727,63 @@ { "schema": { "type": "string", - "description": "The domain's id.", + "minLength": 1, + "maxLength": 256, + "description": "Export a sub-account's own list (its id or your uid) instead of the environment's.", "examples": [ - "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" + "user_8812" ] }, - "required": true, - "description": "The domain's id.", - "name": "id", - "in": "path" + "required": false, + "description": "Export a sub-account's own list (its id or your uid) instead of the environment's.", + "name": "subaccount", + "in": "query" }, { "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 20." + "anyOf": [ + { + "type": "string", + "enum": [ + "*" + ] + }, + { + "type": "string", + "enum": [ + "stream:marketing" + ] + }, + { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$" + } + ], + "description": "Only entries with this scope." }, "required": false, - "description": "1-100, default 20.", - "name": "limit", + "description": "Only entries with this scope.", + "name": "scope", "in": "query" }, { "schema": { "type": "string", - "maxLength": 64, - "description": "Cursor: the `id` of the last report of the previous page." + "enum": [ + "bounce", + "complaint", + "manual", + "import", + "unsubscribe" + ], + "description": "Only entries with this reason.", + "examples": [ + "bounce" + ] }, "required": false, - "description": "Cursor: the `id` of the last report of the previous page.", - "name": "after", + "description": "Only entries with this reason.", + "name": "reason", "in": "query" }, { @@ -45888,7 +53792,7 @@ ], "responses": { "200": { - "description": "A page of DMARC reports.", + "description": "The CSV file.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45910,9 +53814,9 @@ } }, "content": { - "application/json": { + "text/csv": { "schema": { - "$ref": "#/components/schemas/DmarcReportList" + "type": "string" } } } @@ -45981,7 +53885,7 @@ } }, "404": { - "description": "No domain with this id exists in this organization (`not_found`).", + "description": "`subaccount` names no sub-account of the API key's environment (`subaccount_not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -46002,7 +53906,7 @@ } }, "422": { - "description": "A query parameter failed validation (`validation_error`).", + "description": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -46094,14 +53998,14 @@ } } }, - "/v1/domains/{id}/dmarc/reports/{report_id}": { + "/v1/suppressions/{id}": { "get": { - "operationId": "getDomainDmarcReport", + "operationId": "getSuppression", "tags": [ - "Domains" + "Suppressions" ], - "summary": "Retrieve a DMARC report", - "description": "One aggregate report with its rows: each source IP, how many messages it sent, whether they passed DMARC (aligned DKIM or SPF) and what the receiver did with them.", + "summary": "Retrieve a suppression", + "description": "One entry of the suppression list of the API key's environment.", "security": [ { "bearerAuth": [] @@ -46111,36 +54015,23 @@ { "schema": { "type": "string", - "description": "The domain's id.", + "description": "The suppression's id.", "examples": [ - "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" + "sup_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The domain's id.", + "description": "The suppression's id.", "name": "id", "in": "path" }, - { - "schema": { - "type": "string", - "description": "The report's id.", - "examples": [ - "dmarcrep_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The report's id.", - "name": "report_id", - "in": "path" - }, { "$ref": "#/components/parameters/CossVersion" } ], "responses": { "200": { - "description": "The report.", + "description": "The entry.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -46164,7 +54055,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DmarcReport" + "$ref": "#/components/schemas/Suppression" } } } @@ -46233,7 +54124,7 @@ } }, "404": { - "description": "No such domain in this organization, or no such report of it (`not_found`).", + "description": "No suppression with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -46323,24 +54214,40 @@ } } } - } - }, - "/v1/dedicated_ips": { - "get": { - "operationId": "getDedicatedIpPool", + }, + "delete": { + "operationId": "deleteSuppression", "tags": [ - "Dedicated IPs" + "Suppressions" ], - "summary": "Retrieve the organization's dedicated IPs", - "description": "The organization's dedicated IP pool (the Dedicated IPs add-on, bought on the dashboard's billing page): its status, its IPs and how much of the last week's mail they delivered. Organization-wide, like domains: every environment's key sees the same pool; only live emails go through it. Without the add-on the status is `not_enabled`.", + "summary": "Remove a suppression", + "description": "Removes the entry, so later sends reach the address again (unless another entry still matches). Entries added for bounces and complaints can be removed too.", "security": [ { "bearerAuth": [] } ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The suppression's id.", + "examples": [ + "sup_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The suppression's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "responses": { "200": { - "description": "The organization's dedicated IPs.", + "description": "The entry was removed.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -46364,7 +54271,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DedicatedIpPool" + "$ref": "#/components/schemas/DeletedSuppression" } } } @@ -46432,6 +54339,27 @@ } } }, + "404": { + "description": "No suppression with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -46501,22 +54429,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/webhook_endpoints": { + "/v1/subaccounts": { "post": { - "operationId": "createWebhookEndpoint", + "operationId": "createSubaccount", "tags": [ - "Webhook endpoints" + "Sub-accounts" ], - "summary": "Create a webhook endpoint", - "description": "Registers a URL that receives signed event notifications. Webhook endpoints belong to the API key's own environment: a `coss_test_…` key only ever sees sandbox endpoints, and a `coss_live_…` key only production ones. The response carries the signing secret, which is returned only here and when it is rolled: store it, and verify every delivery with it (`verifyWebhook` in the SDK).", + "summary": "Create a sub-account", + "description": "Creates a sub-account: one of your users, who gets their own domains, suppressions, quotas and reputation. Give it a `uid` (e.g. your user's id) to address it by in every path and send. Sub-accounts belong to the API key's own environment: a `coss_test_…` key only ever sees sandbox ones, a `coss_live_…` key only production ones.", "security": [ { "bearerAuth": [] @@ -46527,14 +54450,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateWebhookEndpointRequest" + "$ref": "#/components/schemas/CreateSubaccountRequest" } } } }, "responses": { "200": { - "description": "The endpoint and its signing secret.", + "description": "The sub-account.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -46558,7 +54481,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEndpointWithSecret" + "$ref": "#/components/schemas/Subaccount" } } } @@ -46605,6 +54528,27 @@ } } }, + "402": { + "description": "Live sub-accounts need a plan with sub-accounts (`plan_feature_unavailable`), up to its limit (`plan_limit_reached`). (`billing_error`). Never retried: it needs a plan change or a payment.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -46669,7 +54613,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`, including an unknown `api_version`), the URL is not a usable https:// URL (`invalid_url`), or the environment already has 10 endpoints (`too_many_endpoints`).", + "description": "The body failed validation (`validation_error`), the `uid` is invalid (`invalid_uid`) or taken (`uid_taken`), the quotas don't fit together (`invalid_quotas`), or the sandbox environment has the most sub-accounts it can (100: `too_many_subaccounts`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -46766,20 +54710,50 @@ ] }, "get": { - "operationId": "listWebhookEndpoints", + "operationId": "listSubaccounts", "tags": [ - "Webhook endpoints" + "Sub-accounts" ], - "summary": "List webhook endpoints", - "description": "Every endpoint of the API key's environment, oldest first. Webhook endpoints belong to the API key's own environment: a `coss_test_…` key only ever sees sandbox endpoints, and a `coss_live_…` key only production ones.", + "summary": "List sub-accounts", + "description": "The environment's sub-accounts, newest first. Sub-accounts belong to the API key's own environment: a `coss_test_…` key only ever sees sandbox ones, a `coss_live_…` key only production ones.", "security": [ { "bearerAuth": [] } ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "1-100, default 50." + }, + "required": false, + "description": "1-100, default 50.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "The last `id` of the previous page.", + "examples": [ + "sub_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": false, + "description": "The last `id` of the previous page.", + "name": "after", + "in": "query" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "responses": { "200": { - "description": "The environment's webhook endpoints.", + "description": "A page of sub-accounts.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -46803,7 +54777,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEndpointList" + "$ref": "#/components/schemas/SubaccountList" } } } @@ -46940,22 +54914,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/webhook_endpoints/{id}": { + "/v1/subaccounts/{subaccount}": { "get": { - "operationId": "getWebhookEndpoint", + "operationId": "getSubaccount", "tags": [ - "Webhook endpoints" + "Sub-accounts" ], - "summary": "Retrieve a webhook endpoint", - "description": "The endpoint without its signing secret, which is only returned when created or rolled.", + "summary": "Retrieve a sub-account", + "description": "A sub-account of the API key's environment, by its id or uid, with its quotas and reputation.", "security": [ { "bearerAuth": [] @@ -46965,14 +54934,14 @@ { "schema": { "type": "string", - "description": "The webhook endpoint's id.", + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", "examples": [ - "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" + "user_8812" ] }, "required": true, - "description": "The webhook endpoint's id.", - "name": "id", + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", + "name": "subaccount", "in": "path" }, { @@ -46981,7 +54950,7 @@ ], "responses": { "200": { - "description": "The webhook endpoint.", + "description": "The sub-account.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -47005,7 +54974,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEndpoint" + "$ref": "#/components/schemas/Subaccount" } } } @@ -47074,7 +55043,7 @@ } }, "404": { - "description": "No webhook endpoint with this id exists in the API key's environment (`not_found`).", + "description": "No sub-account with this id or uid exists in the API key's environment (`subaccount_not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -47166,12 +55135,12 @@ } }, "patch": { - "operationId": "updateWebhookEndpoint", + "operationId": "updateSubaccount", "tags": [ - "Webhook endpoints" + "Sub-accounts" ], - "summary": "Update a webhook endpoint", - "description": "Changes the URL, description, subscribed event types, API version or `enabled` flag. Omitted fields keep their current value, and the signing secret stays the same (see Roll a signing secret), so receivers keep verifying deliveries. Disabling an endpoint stops deliveries without losing its configuration.", + "summary": "Update a sub-account", + "description": "Changes the name, uid, metadata, status or quotas. Fields left out keep their value; `status: \"disabled\"` refuses its sends until it is `active` again.", "security": [ { "bearerAuth": [] @@ -47181,14 +55150,14 @@ { "schema": { "type": "string", - "description": "The webhook endpoint's id.", + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", "examples": [ - "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" + "user_8812" ] }, "required": true, - "description": "The webhook endpoint's id.", - "name": "id", + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", + "name": "subaccount", "in": "path" }, { @@ -47200,14 +55169,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateWebhookEndpointRequest" + "$ref": "#/components/schemas/UpdateSubaccountRequest" } } } }, "responses": { "200": { - "description": "The updated endpoint.", + "description": "The sub-account.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -47231,7 +55200,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEndpoint" + "$ref": "#/components/schemas/Subaccount" } } } @@ -47300,7 +55269,7 @@ } }, "404": { - "description": "No webhook endpoint with this id exists in the API key's environment (`not_found`).", + "description": "No sub-account with this id or uid exists in the API key's environment (`subaccount_not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -47363,223 +55332,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`) or the URL is not a usable https:// URL (`invalid_url`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - }, - "Retry-After": { - "description": "Seconds to wait before retrying.", - "schema": { - "type": "integer" - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "500": { - "description": "Unexpected server error (`api_error` / `internal_error`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - }, - "delete": { - "operationId": "deleteWebhookEndpoint", - "tags": [ - "Webhook endpoints" - ], - "summary": "Delete a webhook endpoint", - "description": "Removes the endpoint and its delivery log. Deliveries already queued for it are dropped. To stop deliveries temporarily, set `enabled` to `false` instead.", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The webhook endpoint's id.", - "examples": [ - "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The webhook endpoint's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], - "responses": { - "200": { - "description": "The endpoint was deleted.", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - }, - "Coss-Version": { - "description": "The API version this response is rendered in.", - "schema": { - "type": "string", - "examples": [ - "2026-10-10" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeletedWebhookEndpoint" - } - } - } - }, - "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "401": { - "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "404": { - "description": "No webhook endpoint with this id exists in the API key's environment (`not_found`).", + "description": "The body failed validation (`validation_error`), the uid is invalid (`invalid_uid`) or taken (`uid_taken`), the quotas don't fit together (`invalid_quotas`), or it is being deleted (`subaccount_deleting`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -47669,16 +55422,14 @@ } } } - } - }, - "/v1/webhook_endpoints/{id}/roll_secret": { - "post": { - "operationId": "rollWebhookEndpointSecret", + }, + "delete": { + "operationId": "deleteSubaccount", "tags": [ - "Webhook endpoints" + "Sub-accounts" ], - "summary": "Roll a signing secret", - "description": "Replaces the endpoint's signing secret with a new one, returned in the response (the only time it is). Until `previous_secret_expires_at`, every delivery is signed with both secrets (`webhook-signature` lists two signatures), so receivers verifying with either one accept it: deploy the new secret, then let the old one expire. Rolling again in that time stops the older secret at once. Not idempotent: after a lost response, roll again.", + "summary": "Delete a sub-account", + "description": "Starts deleting the sub-account: its sends are refused at once, then its domains are released and it is removed with its suppressions (`status` is `deleting` until then). Its emails stay, with its id. Repeating the call returns it as it is.", "security": [ { "bearerAuth": [] @@ -47688,33 +55439,23 @@ { "schema": { "type": "string", - "description": "The webhook endpoint's id.", + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", "examples": [ - "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" + "user_8812" ] }, "required": true, - "description": "The webhook endpoint's id.", - "name": "id", + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", + "name": "subaccount", "in": "path" }, { "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RollWebhookEndpointSecretRequest" - } - } - } - }, "responses": { - "200": { - "description": "The endpoint and its new signing secret.", + "202": { + "description": "The sub-account, being deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -47738,13 +55479,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEndpointWithSecret" + "$ref": "#/components/schemas/Subaccount" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -47807,70 +55548,7 @@ } }, "404": { - "description": "No webhook endpoint with this id exists in the API key's environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The body failed validation (`validation_error`).", + "description": "No sub-account with this id or uid exists in the API key's environment (`subaccount_not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -47962,14 +55640,14 @@ } } }, - "/v1/webhook_endpoints/{id}/recover": { + "/v1/subaccounts/{subaccount}/reinstate": { "post": { - "operationId": "recoverWebhookEndpoint", + "operationId": "reinstateSubaccount", "tags": [ - "Webhook endpoints" + "Sub-accounts" ], - "summary": "Recover failed deliveries", - "description": "Queues the endpoint's deliveries that `failed` since `since` for one more attempt each, oldest first, spread over the next minute, e.g. after fixing a receiver that was down. At most 1000 per call: while `has_more` is `true`, call again with the same `since`. The endpoint must be enabled.", + "summary": "Reinstate a sub-account", + "description": "Lifts the sub-account's reputation pause (or warning) once you fixed what caused it: its rates are measured again from now on. A pause is never lifted on its own. Repeating the call returns it as it is. Your organization's own reputation still counts its sending.", "security": [ { "bearerAuth": [] @@ -47979,33 +55657,23 @@ { "schema": { "type": "string", - "description": "The webhook endpoint's id.", + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", "examples": [ - "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" + "user_8812" ] }, "required": true, - "description": "The webhook endpoint's id.", - "name": "id", + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", + "name": "subaccount", "in": "path" }, { "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RecoverWebhookEndpointRequest" - } - } - } - }, "responses": { "200": { - "description": "How many deliveries were queued.", + "description": "The sub-account.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48029,13 +55697,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEndpointRecovery" + "$ref": "#/components/schemas/Subaccount" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48098,70 +55766,7 @@ } }, "404": { - "description": "No webhook endpoint with this id exists in the API key's environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The body failed validation (`validation_error`), or the endpoint is disabled (`endpoint_disabled`).", + "description": "No sub-account with this id or uid exists in the API key's environment (`subaccount_not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48253,14 +55858,14 @@ } } }, - "/v1/webhook_endpoints/{id}/deliveries": { + "/v1/subaccounts/{subaccount}/stats": { "get": { - "operationId": "listWebhookDeliveries", + "operationId": "getSubaccountStats", "tags": [ - "Webhook endpoints" + "Sub-accounts" ], - "summary": "List an endpoint's deliveries", - "description": "The endpoint's delivery log, newest first: one delivery per event sent to it, with every HTTP attempt and the payload as sent. Pass the last `id` as `after` to get the next page while `has_more` is true. Deleting the endpoint deletes its log.", + "summary": "Retrieve a sub-account's stats", + "description": "The sub-account's sending per UTC day: recipients, sent, delivered, bounced, complained, failed and suppressed, counted from its emails as they are now (a later bounce moves its day's numbers). Emails are kept 45 days, so earlier days are left out.", "security": [ { "bearerAuth": [] @@ -48270,61 +55875,42 @@ { "schema": { "type": "string", - "description": "The webhook endpoint's id.", + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", "examples": [ - "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" + "user_8812" ] }, "required": true, - "description": "The webhook endpoint's id.", - "name": "id", + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", + "name": "subaccount", "in": "path" }, { "schema": { "type": "string", - "enum": [ - "pending", - "succeeded", - "failed" - ], - "description": "Only deliveries with this status." - }, - "required": false, - "description": "Only deliveries with this status.", - "name": "status", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "Only the delivery of this event." - }, - "required": false, - "description": "Only the delivery of this event.", - "name": "event_id", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 50." + "format": "date", + "description": "First day (UTC, `YYYY-MM-DD`); default 30 days before `to`.", + "examples": [ + "2026-10-01" + ] }, "required": false, - "description": "1-100, default 50.", - "name": "limit", + "description": "First day (UTC, `YYYY-MM-DD`); default 30 days before `to`.", + "name": "from", "in": "query" }, { "schema": { "type": "string", - "description": "Cursor: the `id` of the last delivery of the previous page." + "format": "date", + "description": "Last day (UTC, `YYYY-MM-DD`), included; default today.", + "examples": [ + "2026-10-15" + ] }, "required": false, - "description": "Cursor: the `id` of the last delivery of the previous page.", - "name": "after", + "description": "Last day (UTC, `YYYY-MM-DD`), included; default today.", + "name": "to", "in": "query" }, { @@ -48333,7 +55919,7 @@ ], "responses": { "200": { - "description": "A page of deliveries.", + "description": "The sub-account's stats.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48357,7 +55943,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookDeliveryList" + "$ref": "#/components/schemas/SubaccountStats" } } } @@ -48426,7 +56012,7 @@ } }, "404": { - "description": "No webhook endpoint with this id exists in the API key's environment (`not_found`).", + "description": "No sub-account with this id or uid exists in the API key's environment (`subaccount_not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48447,7 +56033,7 @@ } }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "description": "A query parameter failed validation (`validation_error`), or `from` is after `to` (`invalid_range`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48539,14 +56125,14 @@ } } }, - "/v1/webhook_endpoints/{id}/deliveries/{delivery_id}": { - "get": { - "operationId": "getWebhookDelivery", + "/v1/sms": { + "post": { + "operationId": "sendSms", "tags": [ - "Webhook endpoints" + "SMS" ], - "summary": "Retrieve a delivery", - "description": "One delivery of the endpoint, with every HTTP attempt and the payload as sent.", + "summary": "Send an SMS", + "description": "Queues a text message from the API key's environment and returns it with `status: \"queued\"`; it is handed to the carrier moments later. Follow it with `GET /v1/sms/{id}` or the `sms.sent`, `sms.delivered`, `sms.undelivered` and `sms.failed` webhooks. Live (`coss_live_…`) keys send `from` one of the organization's numbers whose carrier registration is approved, to the countries the organization allows (US and Canada unless changed in the dashboard). Sandbox (`coss_test_…`) keys accept any `from`, deliver nothing and answer the SMS simulator's numbers (`+15005550006` delivered, `+15005550001` invalid, `+15005550004` filtered by the carrier, `+15005550009` opted out, `+15005550008` replies). A number on the environment's SMS suppression list (it replied STOP, or it can't receive SMS) is skipped: the message is `failed` with `suppressed`. Segments count against the organization's send quota, and each is billed by the destination country.", "security": [ { "bearerAuth": [] @@ -48556,36 +56142,35 @@ { "schema": { "type": "string", - "description": "The webhook endpoint's id.", - "examples": [ - "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The webhook endpoint's id.", - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "string", - "description": "The delivery's id.", + "minLength": 1, + "maxLength": 256, + "description": "Makes retries safe: a repeated request with the same key and body returns the original message (with its current status) instead of sending again, for 24 hours. 1-256 characters.", "examples": [ - "whdel_01k7c8n2b5d7f9h1k3m5p7r9tv" + "welcome-user-123" ] }, - "required": true, - "description": "The delivery's id.", - "name": "delivery_id", - "in": "path" + "required": false, + "description": "Makes retries safe: a repeated request with the same key and body returns the original message (with its current status) instead of sending again, for 24 hours. 1-256 characters.", + "name": "Idempotency-Key", + "in": "header" }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendSmsRequest" + } + } + } + }, "responses": { - "200": { - "description": "The delivery.", + "202": { + "description": "The message, accepted (`queued` or `scheduled`; `failed` when its number is suppressed). A replay returns the original message with its current status.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48596,6 +56181,15 @@ ] } }, + "Idempotent-Replayed": { + "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", + "schema": { + "type": "string", + "enum": [ + "true" + ] + } + }, "Coss-Version": { "description": "The API version this response is rendered in.", "schema": { @@ -48609,13 +56203,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookDelivery" + "$ref": "#/components/schemas/Sms" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`) or the `Idempotency-Key` header is invalid (`invalid_idempotency_key`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48656,8 +56250,29 @@ } } }, + "402": { + "description": "Live sending is paused for an overdue invoice or the spend limit (`billing_hold`) or a used-up budget (`budget_exceeded`), the plan doesn't include SMS or the destination country isn't priced (`plan_feature_unavailable`) (`billing_error`). Never retried: it needs a plan change or a payment.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "description": "The API key lacks the permission this endpoint needs (`insufficient_permissions`), its organization is suspended (`organization_suspended`), or live sending is suspended (`account_suspended`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48677,8 +56292,77 @@ } } }, - "404": { - "description": "No webhook endpoint with this id exists in the API key's environment, or the endpoint has no delivery with this id (`not_found`).", + "409": { + "description": "Requests with this `Idempotency-Key` are racing for an expired key (`request_in_progress`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Retry-After": { + "description": "Seconds to wait.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "The request body is larger than 5 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The body failed validation (`validation_error`, see `issues`), `to` isn't a valid number (`invalid_phone_number`), `from` isn't one of the organization's numbers (`sender_not_found`) or can't send yet (`sender_not_ready`), the destination country isn't allowed (`country_not_allowed`), or the `Idempotency-Key` was already used with a different body (`idempotency_key_reused`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48699,7 +56383,7 @@ } }, "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "description": "Too many requests with this API key (`rate_limited`), or the message's segments exceed the send quota or too many messages went to this number in the last hour (`quota_exceeded`). Retry after `Retry-After` seconds.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48711,7 +56395,7 @@ } }, "Retry-After": { - "description": "Seconds to wait before retrying.", + "description": "Seconds to wait.", "schema": { "type": "integer" } @@ -48768,16 +56452,14 @@ } } } - } - }, - "/v1/webhook_endpoints/{id}/deliveries/{delivery_id}/retry": { - "post": { - "operationId": "retryWebhookDelivery", + }, + "get": { + "operationId": "listSms", "tags": [ - "Webhook endpoints" + "SMS" ], - "summary": "Retry a delivery", - "description": "Queues a finished delivery (`succeeded` or `failed`) for one more attempt, made within seconds, signed with the endpoint's current secret and sent to its current URL. A delivery that is still `pending` is returned unchanged: it is already scheduled for `next_attempt_at` (automatic retries stop after 8 attempts).", + "summary": "List SMS", + "description": "Messages sent from the API key's environment, newest first. Pass the last `id` as `after` to get the next page while `has_more` is true.", "security": [ { "bearerAuth": [] @@ -48787,28 +56469,68 @@ { "schema": { "type": "string", - "description": "The webhook endpoint's id.", - "examples": [ - "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "pattern": "^\\+[1-9]\\d{6,14}$", + "description": "Only messages to this number." }, - "required": true, - "description": "The webhook endpoint's id.", - "name": "id", - "in": "path" + "required": false, + "description": "Only messages to this number.", + "name": "to", + "in": "query" }, { "schema": { "type": "string", - "description": "The delivery's id.", - "examples": [ - "whdel_01k7c8n2b5d7f9h1k3m5p7r9tv" - ] + "description": "Only messages in these statuses, comma-separated: `failed`." }, - "required": true, - "description": "The delivery's id.", - "name": "delivery_id", - "in": "path" + "required": false, + "description": "Only messages in these statuses, comma-separated: `failed`.", + "name": "status", + "in": "query" + }, + { + "schema": { + "type": "string", + "format": "date-time", + "description": "Only messages created at or after this time." + }, + "required": false, + "description": "Only messages created at or after this time.", + "name": "created_after", + "in": "query" + }, + { + "schema": { + "type": "string", + "format": "date-time", + "description": "Only messages created before this time." + }, + "required": false, + "description": "Only messages created before this time.", + "name": "created_before", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "1-100, default 20." + }, + "required": false, + "description": "1-100, default 20.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "maxLength": 64, + "description": "Cursor: the `id` of the last item of the previous page." + }, + "required": false, + "description": "Cursor: the `id` of the last item of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -48816,7 +56538,7 @@ ], "responses": { "200": { - "description": "The delivery, now `pending`.", + "description": "A page of messages.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48840,7 +56562,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookDelivery" + "$ref": "#/components/schemas/SmsList" } } } @@ -48908,8 +56630,8 @@ } } }, - "404": { - "description": "No webhook endpoint with this id exists in the API key's environment, or the endpoint has no delivery with this id (`not_found`).", + "422": { + "description": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49001,14 +56723,14 @@ } } }, - "/v1/webhook_listeners": { + "/v1/sms/suppressions": { "post": { - "operationId": "createWebhookListener", + "operationId": "createSmsSuppression", "tags": [ - "Webhook endpoints" + "SMS" ], - "summary": "Open a webhook listener", - "description": "Listeners are how `coss listen` forwards events to a local server: coss.com can't reach `localhost`, so the CLI pulls them. They belong to the API key's environment, receive events created after they open, and don't count toward the plan's webhook endpoints. A listener closes when it is deleted or after 10 minutes without a poll; an environment has at most 5 open at once. Events reach it unsigned: the CLI signs what it forwards with its own secret.", + "summary": "Add a number to the SMS suppression list", + "description": "Later sends from the API key's environment to the number are skipped. Adding a number already on the list returns its entry with status 200.", "security": [ { "bearerAuth": [] @@ -49019,14 +56741,44 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateWebhookListenerRequest" + "$ref": "#/components/schemas/CreateSmsSuppressionRequest" } } } }, "responses": { "200": { - "description": "The listener.", + "description": "The number already had an entry.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SmsSuppression" + } + } + } + }, + "201": { + "description": "The new entry.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49050,7 +56802,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookListener" + "$ref": "#/components/schemas/SmsSuppression" } } } @@ -49161,7 +56913,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`, including an unknown `api_version`), or the environment already has 5 open listeners (`too_many_listeners`).", + "description": "The body failed validation (`validation_error`) or the number isn't valid (`invalid_phone_number`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49256,16 +57008,14 @@ "$ref": "#/components/parameters/CossVersion" } ] - } - }, - "/v1/webhook_listeners/{id}/poll": { - "post": { - "operationId": "pollWebhookListener", + }, + "get": { + "operationId": "listSmsSuppressions", "tags": [ - "Webhook endpoints" + "SMS" ], - "summary": "Poll a webhook listener", - "description": "Acknowledges deliveries handed out earlier, keeps the listener open for another 10 minutes, and waits up to `wait_ms` (at most 25 s) for events. Deliveries come oldest first; one not acknowledged within two minutes is handed out again (with the same `event_id`). The answer may come early, with no deliveries, when the API restarts: poll again. Polls are not request-logged; each acknowledgement is logged as a `webhook.forward` line.", + "summary": "List suppressed numbers", + "description": "The SMS suppression list of the API key's environment, newest first: opt-outs, invalid numbers and numbers you added.", "security": [ { "bearerAuth": [] @@ -49275,33 +57025,44 @@ { "schema": { "type": "string", - "description": "The webhook listener's id.", - "examples": [ - "whlis_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "pattern": "^\\+[1-9]\\d{6,14}$", + "description": "Only this number's entry." }, - "required": true, - "description": "The webhook listener's id.", - "name": "id", - "in": "path" + "required": false, + "description": "Only this number's entry.", + "name": "phone_number", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "1-100, default 20." + }, + "required": false, + "description": "1-100, default 20.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "maxLength": 64, + "description": "Cursor: the `id` of the last item of the previous page." + }, + "required": false, + "description": "Cursor: the `id` of the last item of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PollWebhookListenerRequest" - } - } - } - }, "responses": { "200": { - "description": "Deliveries to forward (possibly none).", + "description": "A page of entries.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49325,13 +57086,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookListenerPoll" + "$ref": "#/components/schemas/SmsSuppressionList" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49393,71 +57154,8 @@ } } }, - "404": { - "description": "No open webhook listener with this id exists in the API key's environment: it was closed, or expired after 10 minutes without a poll (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "422": { - "description": "The body failed validation (`validation_error`).", + "description": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49549,14 +57247,14 @@ } } }, - "/v1/webhook_listeners/{id}": { - "delete": { - "operationId": "deleteWebhookListener", + "/v1/sms/suppressions/check": { + "get": { + "operationId": "checkSmsSuppression", "tags": [ - "Webhook endpoints" + "SMS" ], - "summary": "Close a webhook listener", - "description": "Closes the listener: it receives nothing more, and deliveries it was still holding are dropped. `coss listen` closes its listener when it exits.", + "summary": "Check whether a number is suppressed", + "description": "Whether a phone number is on the SMS suppression list of the API key's environment, with its entry if it is.", "security": [ { "bearerAuth": [] @@ -49566,15 +57264,11 @@ { "schema": { "type": "string", - "description": "The webhook listener's id.", - "examples": [ - "whlis_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "pattern": "^\\+[1-9]\\d{6,14}$" }, "required": true, - "description": "The webhook listener's id.", - "name": "id", - "in": "path" + "name": "phone_number", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -49582,7 +57276,7 @@ ], "responses": { "200": { - "description": "The listener was closed.", + "description": "The result.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49606,7 +57300,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedWebhookListener" + "$ref": "#/components/schemas/SmsSuppressionCheck" } } } @@ -49674,8 +57368,8 @@ } } }, - "404": { - "description": "No open webhook listener with this id exists in the API key's environment: it was closed, or expired after 10 minutes without a poll (`not_found`).", + "422": { + "description": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49767,32 +57461,40 @@ } } }, - "/v1/webhook_events/test": { - "post": { - "operationId": "createTestWebhookEvent", + "/v1/sms/suppressions/{id}": { + "get": { + "operationId": "getSmsSuppression", "tags": [ - "Webhook endpoints" + "SMS" ], - "summary": "Send a test event", - "description": "Sends a sample event of a type to the environment's enabled endpoints subscribed to it and its open listeners (`coss listen`), exactly as a real one is sent: same body, signature, retries and log lines. `coss trigger` calls it. Sandbox environments only. The ids in the sample are made up (pass real ones in `data` for a handler that looks them up), and nothing marks the event as a test: your handler takes it like the real thing. When nothing is subscribed, the event goes nowhere and isn't kept.", + "summary": "Retrieve an SMS suppression", + "description": "One entry of the SMS suppression list of the API key's environment.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTestWebhookEventRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The entry's id.", + "examples": [ + "smssup_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The entry's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { "200": { - "description": "The event that was sent, and to how many receivers.", + "description": "The entry.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49816,13 +57518,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookTestEvent" + "$ref": "#/components/schemas/SmsSuppression" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49864,49 +57566,7 @@ } }, "403": { - "description": "The API key lacks the `webhook:create` permission, its organization is suspended, or it belongs to a live environment (`sandbox_only`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49926,8 +57586,8 @@ } } }, - "422": { - "description": "The body failed validation (`validation_error`), the type only reaches live environments (`live_only_event_type`: `domain.*`, `account.*`, `billing.*`), or `data` doesn't fit the type (`invalid_test_event`, with `issues`; unknown fields included).", + "404": { + "description": "No entry with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -50016,70 +57676,41 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - } - }, - "/v1/suppressions": { - "post": { - "operationId": "createSuppression", + } + }, + "delete": { + "operationId": "deleteSmsSuppression", "tags": [ - "Suppressions" + "SMS" ], - "summary": "Add an address to the suppression list", - "description": "Suppresses an address in the API key's environment: later sends skip it, either for every email (`scope: \"*\"`, the default) or only for emails with the given `category` (e.g. to back an unsubscribe link). Adding an address that is already suppressed with the same scope returns the existing entry with status 200.", + "summary": "Remove a number from the SMS suppression list", + "description": "Sends to the number go out again. Remove an opt-out only when the person asked to receive messages again (they can also reply START).", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSuppressionRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The entry's id.", + "examples": [ + "smssup_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The entry's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { "200": { - "description": "The address already had an entry with this scope.", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - }, - "Coss-Version": { - "description": "The API version this response is rendered in.", - "schema": { - "type": "string", - "examples": [ - "2026-10-10" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Suppression" - } - } - } - }, - "201": { - "description": "The new entry.", + "description": "The entry was removed.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -50103,13 +57734,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Suppression" + "$ref": "#/components/schemas/DeletedSmsSuppression" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -50172,70 +57803,7 @@ } }, "404": { - "description": "`subaccount` names no sub-account of the API key's environment (`subaccount_not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The body failed validation (`validation_error`, see `issues`).", + "description": "No entry with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -50324,20 +57892,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, + } + } + }, + "/v1/sms/received": { "get": { - "operationId": "listSuppressions", + "operationId": "listReceivedSms", "tags": [ - "Suppressions" + "SMS" ], - "summary": "List suppressed addresses", - "description": "The suppression list of the API key's environment, newest first. Pass the last `id` as `after` to get the next page while `has_more` is true.", + "summary": "List received SMS", + "description": "Messages sent to the organization's numbers whose replies go to the API key's environment, newest first.", "security": [ { "bearerAuth": [] @@ -50347,53 +57912,41 @@ { "schema": { "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "List a sub-account's own list (its id or your uid) instead of the environment's.", - "examples": [ - "user_8812" - ] + "pattern": "^\\+[1-9]\\d{6,14}$", + "description": "Only messages from this number." }, "required": false, - "description": "List a sub-account's own list (its id or your uid) instead of the environment's.", - "name": "subaccount", + "description": "Only messages from this number.", + "name": "from", "in": "query" }, { "schema": { "type": "string", - "description": "Only entries for this address." + "pattern": "^\\+[1-9]\\d{6,14}$", + "description": "Only messages to this number of yours." }, "required": false, - "description": "Only entries for this address.", - "name": "address", + "description": "Only messages to this number of yours.", + "name": "to", "in": "query" }, { "schema": { - "anyOf": [ - { - "type": "string", - "enum": [ - "*" - ] - }, - { - "type": "string", - "enum": [ - "stream:marketing" - ] - }, - { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$" - } - ], - "description": "Only entries with this scope." + "type": "string", + "format": "date-time" }, "required": false, - "description": "Only entries with this scope.", - "name": "scope", + "name": "created_after", + "in": "query" + }, + { + "schema": { + "type": "string", + "format": "date-time" + }, + "required": false, + "name": "created_before", "in": "query" }, { @@ -50401,20 +57954,21 @@ "type": "integer", "minimum": 1, "maximum": 100, - "description": "1-100, default 50." + "description": "1-100, default 20." }, "required": false, - "description": "1-100, default 50.", + "description": "1-100, default 20.", "name": "limit", "in": "query" }, { "schema": { "type": "string", - "description": "Cursor: the `id` of the last entry of the previous page." + "maxLength": 64, + "description": "Cursor: the `id` of the last item of the previous page." }, "required": false, - "description": "Cursor: the `id` of the last entry of the previous page.", + "description": "Cursor: the `id` of the last item of the previous page.", "name": "after", "in": "query" }, @@ -50424,7 +57978,7 @@ ], "responses": { "200": { - "description": "A page of entries.", + "description": "A page of received messages.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -50448,7 +58002,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuppressionList" + "$ref": "#/components/schemas/ReceivedSmsList" } } } @@ -50516,27 +58070,6 @@ } } }, - "404": { - "description": "`subaccount` names no sub-account of the API key's environment (`subaccount_not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "422": { "description": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { @@ -50630,14 +58163,14 @@ } } }, - "/v1/suppressions/check": { + "/v1/sms/received/{id}": { "get": { - "operationId": "checkSuppression", + "operationId": "getReceivedSms", "tags": [ - "Suppressions" + "SMS" ], - "summary": "Check whether an address is suppressed", - "description": "Whether a send to `address` (with `category`, if given) would skip it, and the entries that match: those with scope `*` and, with a `category`, those for that category.", + "summary": "Retrieve a received SMS", + "description": "One message received on a number of the organization and routed to the API key's environment.", "security": [ { "bearerAuth": [] @@ -50647,52 +58180,15 @@ { "schema": { "type": "string", - "description": "The address to check.", + "description": "The received message's id.", "examples": [ - "user@example.com" + "smsin_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The address to check.", - "name": "address", - "in": "query" - }, - { - "schema": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$", - "description": "The category of the email you plan to send. Without it, only `*` entries count." - }, - "required": false, - "description": "The category of the email you plan to send. Without it, only `*` entries count.", - "name": "category", - "in": "query" - }, - { - "schema": { - "type": "string", - "enum": [ - "transactional", - "marketing" - ], - "description": "The stream of the email you plan to send (default `transactional`). `marketing` also counts `stream:marketing` entries." - }, - "required": false, - "description": "The stream of the email you plan to send (default `transactional`). `marketing` also counts `stream:marketing` entries.", - "name": "stream", - "in": "query" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "The sub-account you plan to send for (its id or your uid): its own entries count too." - }, - "required": false, - "description": "The sub-account you plan to send for (its id or your uid): its own entries count too.", - "name": "subaccount", - "in": "query" + "description": "The received message's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -50700,7 +58196,7 @@ ], "responses": { "200": { - "description": "The result.", + "description": "The received message.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -50724,7 +58220,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuppressionCheck" + "$ref": "#/components/schemas/ReceivedSms" } } } @@ -50793,28 +58289,7 @@ } }, "404": { - "description": "`subaccount` names no sub-account of the API key's environment (`subaccount_not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "description": "No received message with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -50906,32 +58381,22 @@ } } }, - "/v1/suppressions/import": { - "post": { - "operationId": "importSuppressions", + "/v1/sms/numbers": { + "get": { + "operationId": "listSmsNumbers", "tags": [ - "Suppressions" + "SMS" ], - "summary": "Import suppressions in bulk", - "description": "Adds up to 1000 entries at once, with reason `import`. Each entry is validated on its own: invalid ones are skipped and listed in `invalid`, entries already on the list are left unchanged and counted in `existing`. Importing again is safe.", + "summary": "List the organization's phone numbers", + "description": "Every number of the organization, whether it can send live messages now (its carrier registration is approved) and whether its replies arrive in the API key's environment. Numbers are bought and registered in the dashboard.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ImportSuppressionsRequest" - } - } - } - }, "responses": { "200": { - "description": "What the import did.", + "description": "The numbers.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -50955,13 +58420,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuppressionImport" + "$ref": "#/components/schemas/SmsNumberList" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51023,8 +58488,8 @@ } } }, - "404": { - "description": "`subaccount` names no sub-account of the API key's environment (`subaccount_not_found`).", + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51034,6 +58499,12 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } } }, "content": { @@ -51044,8 +58515,8 @@ } } }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51065,8 +58536,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51085,9 +58556,142 @@ } } } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + } + }, + "/v1/sms/{id}": { + "get": { + "operationId": "getSms", + "tags": [ + "SMS" + ], + "summary": "Retrieve an SMS", + "description": "One message sent from the API key's environment, with its current status.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The message's id.", + "examples": [ + "sms_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The message's id.", + "name": "id", + "in": "path" }, - "422": { - "description": "The body is not a list of 1-1000 `{ address, scope? }` objects (`validation_error`).", + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "The message.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sms" + } + } + } + }, + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "No message with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51176,22 +58780,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/suppressions/export": { + "/v1/sms/{id}/events": { "get": { - "operationId": "exportSuppressions", + "operationId": "listSmsEvents", "tags": [ - "Suppressions" + "SMS" ], - "summary": "Export the suppression list as CSV", - "description": "The whole suppression list of the API key's environment (optionally filtered), oldest first, as CSV with the columns `address,scope,reason,created_at,email_id,id`. The file can be imported again as is. Values a spreadsheet would run as a formula are prefixed with `'`.", + "summary": "List an SMS's events", + "description": "Everything that happened to the message, oldest first.", "security": [ { "bearerAuth": [] @@ -51201,64 +58800,15 @@ { "schema": { "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Export a sub-account's own list (its id or your uid) instead of the environment's.", - "examples": [ - "user_8812" - ] - }, - "required": false, - "description": "Export a sub-account's own list (its id or your uid) instead of the environment's.", - "name": "subaccount", - "in": "query" - }, - { - "schema": { - "anyOf": [ - { - "type": "string", - "enum": [ - "*" - ] - }, - { - "type": "string", - "enum": [ - "stream:marketing" - ] - }, - { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{1,256}$" - } - ], - "description": "Only entries with this scope." - }, - "required": false, - "description": "Only entries with this scope.", - "name": "scope", - "in": "query" - }, - { - "schema": { - "type": "string", - "enum": [ - "bounce", - "complaint", - "manual", - "import", - "unsubscribe" - ], - "description": "Only entries with this reason.", + "description": "The message's id.", "examples": [ - "bounce" + "sms_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, - "required": false, - "description": "Only entries with this reason.", - "name": "reason", - "in": "query" + "required": true, + "description": "The message's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -51266,7 +58816,7 @@ ], "responses": { "200": { - "description": "The CSV file.", + "description": "The events.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51288,9 +58838,9 @@ } }, "content": { - "text/csv": { + "application/json": { "schema": { - "type": "string" + "$ref": "#/components/schemas/SmsEventList" } } } @@ -51359,28 +58909,7 @@ } }, "404": { - "description": "`subaccount` names no sub-account of the API key's environment (`subaccount_not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "description": "No message with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51472,14 +59001,14 @@ } } }, - "/v1/suppressions/{id}": { - "get": { - "operationId": "getSuppression", + "/v1/sms/{id}/cancel": { + "post": { + "operationId": "cancelSms", "tags": [ - "Suppressions" + "SMS" ], - "summary": "Retrieve a suppression", - "description": "One entry of the suppression list of the API key's environment.", + "summary": "Cancel a scheduled SMS", + "description": "Cancels a message scheduled with `scheduled_at` before it is sent. Canceling a canceled message returns it as is.", "security": [ { "bearerAuth": [] @@ -51489,13 +59018,13 @@ { "schema": { "type": "string", - "description": "The suppression's id.", + "description": "The message's id.", "examples": [ - "sup_01k7c8m4q2f3e9v6t5r1n0p8yz" + "sms_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The suppression's id.", + "description": "The message's id.", "name": "id", "in": "path" }, @@ -51505,7 +59034,7 @@ ], "responses": { "200": { - "description": "The entry.", + "description": "The canceled message.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51529,7 +59058,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Suppression" + "$ref": "#/components/schemas/Sms" } } } @@ -51598,7 +59127,28 @@ } }, "404": { - "description": "No suppression with this id exists in the API key's environment (`not_found`).", + "description": "No message with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The message isn't scheduled (`sms_not_cancelable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51688,40 +59238,34 @@ } } } - }, - "delete": { - "operationId": "deleteSuppression", + } + }, + "/v1/sign/documents": { + "post": { + "operationId": "uploadSignDocument", "tags": [ - "Suppressions" + "Sign documents" ], - "summary": "Remove a suppression", - "description": "Removes the entry, so later sends reach the address again (unless another entry still matches). Entries added for bounces and complaints can be removed too.", + "summary": "Upload a document", + "description": "Uploads a PDF (base64, at most 25 MB and 500 pages, not password-protected) for signature requests to use. The response lists each page's size and the text anchors found in it (`{{signature:signer_1}}`), which fields can be placed by. Requests can also take PDFs inline.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The suppression's id.", - "examples": [ - "sup_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The suppression's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadSignDocumentRequest" + } + } } - ], + }, "responses": { - "200": { - "description": "The entry was removed.", + "201": { + "description": "The uploaded document.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51745,13 +59289,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSuppression" + "$ref": "#/components/schemas/SignDocument" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51813,8 +59357,8 @@ } } }, - "404": { - "description": "No suppression with this id exists in the API key's environment (`not_found`).", + "413": { + "description": "The request body is larger than 40 MB (`payload_too_large`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51834,8 +59378,8 @@ } } }, - "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51845,12 +59389,6 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Retry-After": { - "description": "Seconds to wait before retrying.", - "schema": { - "type": "integer" - } } }, "content": { @@ -51861,8 +59399,8 @@ } } }, - "500": { - "description": "Unexpected server error (`api_error` / `internal_error`).", + "422": { + "description": "The body failed validation (`validation_error`, see `issues`), or the file isn't a PDF coss.com can read (`invalid_document`: damaged, encrypted or too many pages).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51882,8 +59420,8 @@ } } }, - "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51893,6 +59431,12 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } } }, "content": { @@ -51902,36 +59446,9 @@ } } } - } - } - } - }, - "/v1/subaccounts": { - "post": { - "operationId": "createSubaccount", - "tags": [ - "Sub-accounts" - ], - "summary": "Create a sub-account", - "description": "Creates a sub-account: one of your users, who gets their own domains, suppressions, quotas and reputation. Give it a `uid` (e.g. your user's id) to address it by in every path and send. Sub-accounts belong to the API key's own environment: a `coss_test_…` key only ever sees sandbox ones, a `coss_live_…` key only production ones.", - "security": [ - { - "bearerAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSubaccountRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The sub-account.", + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51941,27 +59458,18 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Coss-Version": { - "description": "The API version this response is rendered in.", - "schema": { - "type": "string", - "examples": [ - "2026-10-10" - ] - } } }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Subaccount" + "$ref": "#/components/schemas/Error" } } } }, - "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "503": { + "description": "Sign is temporarily unavailable (`sign_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51980,9 +59488,57 @@ } } } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, + "get": { + "operationId": "listSignDocuments", + "tags": [ + "Sign documents" + ], + "summary": "List documents", + "description": "Documents uploaded to the API key's environment, newest first. Pass the last `id` as `after` to get the next page while `has_more` is true.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "1-100, default 20." + }, + "required": false, + "description": "1-100, default 20.", + "name": "limit", + "in": "query" }, - "401": { - "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + { + "schema": { + "type": "string", + "maxLength": 64, + "description": "Cursor: the `id` of the last item of the previous page." + }, + "required": false, + "description": "Cursor: the `id` of the last item of the previous page.", + "name": "after", + "in": "query" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "A page of documents.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51992,25 +59548,13 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "402": { - "description": "Live sub-accounts need a plan with sub-accounts (`plan_feature_unavailable`), up to its limit (`plan_limit_reached`). (`billing_error`). Never retried: it needs a plan change or a payment.", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", "schema": { "type": "string", "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + "2026-10-10" ] } } @@ -52018,13 +59562,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/SignDocumentList" } } } }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52044,8 +59588,8 @@ } } }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52065,8 +59609,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52087,7 +59631,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`), the `uid` is invalid (`invalid_uid`) or taken (`uid_taken`), the quotas don't fit together (`invalid_quotas`), or the sandbox environment has the most sub-accounts it can (100: `too_many_subaccounts`).", + "description": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52176,50 +59720,35 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, + } + } + }, + "/v1/sign/documents/{id}": { "get": { - "operationId": "listSubaccounts", + "operationId": "getSignDocument", "tags": [ - "Sub-accounts" + "Sign documents" ], - "summary": "List sub-accounts", - "description": "The environment's sub-accounts, newest first. Sub-accounts belong to the API key's own environment: a `coss_test_…` key only ever sees sandbox ones, a `coss_live_…` key only production ones.", + "summary": "Retrieve a document", + "description": "An uploaded document's details: its hash, page sizes and text anchors. Its bytes are never returned here.", "security": [ { "bearerAuth": [] } ], "parameters": [ - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 50." - }, - "required": false, - "description": "1-100, default 50.", - "name": "limit", - "in": "query" - }, { "schema": { "type": "string", - "description": "The last `id` of the previous page.", + "description": "The document's id.", "examples": [ - "sub_01k7c8m4q2f3e9v6t5r1n0p8yz" + "sdoc_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, - "required": false, - "description": "The last `id` of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The document's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -52227,7 +59756,7 @@ ], "responses": { "200": { - "description": "A page of sub-accounts.", + "description": "The document.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52251,7 +59780,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SubaccountList" + "$ref": "#/components/schemas/SignDocument" } } } @@ -52319,6 +59848,27 @@ } } }, + "404": { + "description": "No document with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -52389,16 +59939,14 @@ } } } - } - }, - "/v1/subaccounts/{subaccount}": { - "get": { - "operationId": "getSubaccount", + }, + "delete": { + "operationId": "deleteSignDocument", "tags": [ - "Sub-accounts" + "Sign documents" ], - "summary": "Retrieve a sub-account", - "description": "A sub-account of the API key's environment, by its id or uid, with its quotas and reputation.", + "summary": "Delete a document", + "description": "Deletes an uploaded document and its file. Documents a request uses can't be deleted.", "security": [ { "bearerAuth": [] @@ -52408,14 +59956,14 @@ { "schema": { "type": "string", - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", + "description": "The document's id.", "examples": [ - "user_8812" + "sdoc_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", - "name": "subaccount", + "description": "The document's id.", + "name": "id", "in": "path" }, { @@ -52424,7 +59972,7 @@ ], "responses": { "200": { - "description": "The sub-account.", + "description": "The document was deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52448,7 +59996,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Subaccount" + "$ref": "#/components/schemas/DeletedSignDocument" } } } @@ -52517,7 +60065,28 @@ } }, "404": { - "description": "No sub-account with this id or uid exists in the API key's environment (`subaccount_not_found`).", + "description": "No document with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "A signature request uses the document (`document_in_use`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52586,7 +60155,7 @@ } }, "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "description": "Sign is temporarily unavailable (`sign_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52607,14 +60176,16 @@ } } } - }, - "patch": { - "operationId": "updateSubaccount", + } + }, + "/v1/sign/requests": { + "post": { + "operationId": "createSignRequest", "tags": [ - "Sub-accounts" + "Signature requests" ], - "summary": "Update a sub-account", - "description": "Changes the name, uid, metadata, status or quotas. Fields left out keep their value; `status: \"disabled\"` refuses its sends until it is `active` again.", + "summary": "Create a signature request", + "description": "Asks one or more signers to sign one or more PDFs. Give documents by id (uploaded) or inline, and each signer's fields by position (`page`, `x`, `y`, `width`, `height` in PDF points from the page's top left) or by text anchor (`{{signature:signer_1}}`). Every signer needs a `signature` field. Sent at once unless `send` is `false` (a `draft`). Each signer signs by email (`delivery: \"email\"`, the default) or in your app (`\"embedded\"`).", "security": [ { "bearerAuth": [] @@ -52624,15 +60195,17 @@ { "schema": { "type": "string", - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", + "minLength": 1, + "maxLength": 256, + "description": "Makes retries safe: a repeated request with the same key and body returns the original request (in its current state) instead of creating another, for 24 hours. 1-256 characters.", "examples": [ - "user_8812" + "welcome-user-123" ] }, - "required": true, - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", - "name": "subaccount", - "in": "path" + "required": false, + "description": "Makes retries safe: a repeated request with the same key and body returns the original request (in its current state) instead of creating another, for 24 hours. 1-256 characters.", + "name": "Idempotency-Key", + "in": "header" }, { "$ref": "#/components/parameters/CossVersion" @@ -52643,14 +60216,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateSubaccountRequest" + "$ref": "#/components/schemas/CreateSignRequestRequest" } } } }, "responses": { - "200": { - "description": "The sub-account.", + "201": { + "description": "The signature request (`sent`, or `draft`). A replay returns the original request in its current state.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52661,6 +60234,15 @@ ] } }, + "Idempotent-Replayed": { + "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", + "schema": { + "type": "string", + "enum": [ + "true" + ] + } + }, "Coss-Version": { "description": "The API version this response is rendered in.", "schema": { @@ -52674,13 +60256,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Subaccount" + "$ref": "#/components/schemas/SignRequest" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`) or the `Idempotency-Key` header is invalid (`invalid_idempotency_key`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52721,6 +60303,27 @@ } } }, + "402": { + "description": "In live environments: an overdue invoice or spend limit (`billing_hold`), or the project's budget (`budget_exceeded`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -52742,8 +60345,8 @@ } } }, - "404": { - "description": "No sub-account with this id or uid exists in the API key's environment (`subaccount_not_found`).", + "409": { + "description": "Requests with this `Idempotency-Key` are racing for an expired key (`request_in_progress`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52764,7 +60367,7 @@ } }, "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", + "description": "The request body is larger than 40 MB (`payload_too_large`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52806,7 +60409,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`), the uid is invalid (`invalid_uid`) or taken (`uid_taken`), the quotas don't fit together (`invalid_quotas`), or it is being deleted (`subaccount_deleting`).", + "description": "The body failed validation (`validation_error`, see `issues`), a document isn't a PDF coss.com can read (`invalid_document`) or isn't in the environment (`not_found`), a field doesn't fit on its page or names an anchor no document has (`invalid_fields`), a signer is listed twice (`invalid_signers`), `expires_at` is out of range (`invalid_expiry`), or the `Idempotency-Key` was already used with a different body (`idempotency_key_reused`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52875,7 +60478,7 @@ } }, "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "description": "Sign is temporarily unavailable (`sign_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52897,13 +60500,13 @@ } } }, - "delete": { - "operationId": "deleteSubaccount", + "get": { + "operationId": "listSignRequests", "tags": [ - "Sub-accounts" + "Signature requests" ], - "summary": "Delete a sub-account", - "description": "Starts deleting the sub-account: its sends are refused at once, then its domains are released and it is removed with its suppressions (`status` is `deleting` until then). Its emails stay, with its id. Repeating the call returns it as it is.", + "summary": "List signature requests", + "description": "Signature requests of the API key's environment, newest first. Pass the last `id` as `after` to get the next page while `has_more` is true.", "security": [ { "bearerAuth": [] @@ -52913,23 +60516,43 @@ { "schema": { "type": "string", - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", - "examples": [ - "user_8812" - ] + "description": "Only requests in these statuses, comma-separated: `sent,draft`." }, - "required": true, - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", - "name": "subaccount", - "in": "path" + "required": false, + "description": "Only requests in these statuses, comma-separated: `sent,draft`.", + "name": "status", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "1-100, default 20." + }, + "required": false, + "description": "1-100, default 20.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "maxLength": 64, + "description": "Cursor: the `id` of the last item of the previous page." + }, + "required": false, + "description": "Cursor: the `id` of the last item of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" } ], "responses": { - "202": { - "description": "The sub-account, being deleted.", + "200": { + "description": "A page of signature requests.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52953,7 +60576,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Subaccount" + "$ref": "#/components/schemas/SignRequestList" } } } @@ -53021,8 +60644,8 @@ } } }, - "404": { - "description": "No sub-account with this id or uid exists in the API key's environment (`subaccount_not_found`).", + "422": { + "description": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53114,14 +60737,14 @@ } } }, - "/v1/subaccounts/{subaccount}/reinstate": { - "post": { - "operationId": "reinstateSubaccount", + "/v1/sign/requests/{id}": { + "get": { + "operationId": "getSignRequest", "tags": [ - "Sub-accounts" + "Signature requests" ], - "summary": "Reinstate a sub-account", - "description": "Lifts the sub-account's reputation pause (or warning) once you fixed what caused it: its rates are measured again from now on. A pause is never lifted on its own. Repeating the call returns it as it is. Your organization's own reputation still counts its sending.", + "summary": "Retrieve a signature request", + "description": "A signature request with its documents, signers and fields, and `signed_document` once it's sealed.", "security": [ { "bearerAuth": [] @@ -53131,14 +60754,14 @@ { "schema": { "type": "string", - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", + "description": "The signature request's id.", "examples": [ - "user_8812" + "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", - "name": "subaccount", + "description": "The signature request's id.", + "name": "id", "in": "path" }, { @@ -53147,7 +60770,7 @@ ], "responses": { "200": { - "description": "The sub-account.", + "description": "The signature request.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53171,7 +60794,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Subaccount" + "$ref": "#/components/schemas/SignRequest" } } } @@ -53240,7 +60863,7 @@ } }, "404": { - "description": "No sub-account with this id or uid exists in the API key's environment (`subaccount_not_found`).", + "description": "No signature request with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53330,16 +60953,14 @@ } } } - } - }, - "/v1/subaccounts/{subaccount}/stats": { - "get": { - "operationId": "getSubaccountStats", + }, + "delete": { + "operationId": "deleteSignRequest", "tags": [ - "Sub-accounts" + "Signature requests" ], - "summary": "Retrieve a sub-account's stats", - "description": "The sub-account's sending per UTC day: recipients, sent, delivered, bounced, complained, failed and suppressed, counted from its emails as they are now (a later bounce moves its day's numbers). Emails are kept 45 days, so earlier days are left out.", + "summary": "Delete a signature request", + "description": "Deletes a request that isn't `sent` (cancel it first) with its signers, fields, audit trail, signatures and signed document, for good. Its documents stay (delete them on their own). Keep the signed document first if you need it: it is your record.", "security": [ { "bearerAuth": [] @@ -53349,51 +60970,23 @@ { "schema": { "type": "string", - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", + "description": "The signature request's id.", "examples": [ - "user_8812" + "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", - "name": "subaccount", + "description": "The signature request's id.", + "name": "id", "in": "path" }, - { - "schema": { - "type": "string", - "format": "date", - "description": "First day (UTC, `YYYY-MM-DD`); default 30 days before `to`.", - "examples": [ - "2026-10-01" - ] - }, - "required": false, - "description": "First day (UTC, `YYYY-MM-DD`); default 30 days before `to`.", - "name": "from", - "in": "query" - }, - { - "schema": { - "type": "string", - "format": "date", - "description": "Last day (UTC, `YYYY-MM-DD`), included; default today.", - "examples": [ - "2026-10-15" - ] - }, - "required": false, - "description": "Last day (UTC, `YYYY-MM-DD`), included; default today.", - "name": "to", - "in": "query" - }, { "$ref": "#/components/parameters/CossVersion" } ], "responses": { "200": { - "description": "The sub-account's stats.", + "description": "The deleted request.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53417,7 +61010,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SubaccountStats" + "$ref": "#/components/schemas/DeletedSignRequest" } } } @@ -53486,7 +61079,7 @@ } }, "404": { - "description": "No sub-account with this id or uid exists in the API key's environment (`subaccount_not_found`).", + "description": "No signature request with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53507,7 +61100,7 @@ } }, "422": { - "description": "A query parameter failed validation (`validation_error`), or `from` is after `to` (`invalid_range`).", + "description": "The request is `sent`: cancel it first (`request_not_deletable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53576,7 +61169,7 @@ } }, "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "description": "Sign is temporarily unavailable (`sign_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53599,14 +61192,14 @@ } } }, - "/v1/sms": { + "/v1/sign/requests/{id}/send": { "post": { - "operationId": "sendSms", + "operationId": "sendSignRequest", "tags": [ - "SMS" + "Signature requests" ], - "summary": "Send an SMS", - "description": "Queues a text message from the API key's environment and returns it with `status: \"queued\"`; it is handed to the carrier moments later. Follow it with `GET /v1/sms/{id}` or the `sms.sent`, `sms.delivered`, `sms.undelivered` and `sms.failed` webhooks. Live (`coss_live_…`) keys send `from` one of the organization's numbers whose carrier registration is approved, to the countries the organization allows (US and Canada unless changed in the dashboard). Sandbox (`coss_test_…`) keys accept any `from`, deliver nothing and answer the SMS simulator's numbers (`+15005550006` delivered, `+15005550001` invalid, `+15005550004` filtered by the carrier, `+15005550009` opted out, `+15005550008` replies). A number on the environment's SMS suppression list (it replied STOP, or it can't receive SMS) is skipped: the message is `failed` with `suppressed`. Segments count against the organization's send quota, and each is billed by the destination country.", + "summary": "Send a draft signature request", + "description": "Sends a `draft`. An already sent request is returned as is.", "security": [ { "bearerAuth": [] @@ -53616,35 +61209,23 @@ { "schema": { "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Makes retries safe: a repeated request with the same key and body returns the original message (with its current status) instead of sending again, for 24 hours. 1-256 characters.", + "description": "The signature request's id.", "examples": [ - "welcome-user-123" + "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, - "required": false, - "description": "Makes retries safe: a repeated request with the same key and body returns the original message (with its current status) instead of sending again, for 24 hours. 1-256 characters.", - "name": "Idempotency-Key", - "in": "header" + "required": true, + "description": "The signature request's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SendSmsRequest" - } - } - } - }, "responses": { - "202": { - "description": "The message, accepted (`queued` or `scheduled`; `failed` when its number is suppressed). A replay returns the original message with its current status.", + "200": { + "description": "The sent request.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53655,15 +61236,6 @@ ] } }, - "Idempotent-Replayed": { - "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", - "schema": { - "type": "string", - "enum": [ - "true" - ] - } - }, "Coss-Version": { "description": "The API version this response is rendered in.", "schema": { @@ -53677,13 +61249,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Sms" + "$ref": "#/components/schemas/SignRequest" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`) or the `Idempotency-Key` header is invalid (`invalid_idempotency_key`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53725,7 +61297,7 @@ } }, "402": { - "description": "Live sending is paused for an overdue invoice or the spend limit (`billing_hold`) or a used-up budget (`budget_exceeded`), the plan doesn't include SMS or the destination country isn't priced (`plan_feature_unavailable`) (`billing_error`). Never retried: it needs a plan change or a payment.", + "description": "In live environments: an overdue invoice or spend limit (`billing_hold`), or the project's budget (`budget_exceeded`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53746,55 +61318,7 @@ } }, "403": { - "description": "The API key lacks the permission this endpoint needs (`insufficient_permissions`), its organization is suspended (`organization_suspended`), or live sending is suspended (`account_suspended`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "409": { - "description": "Requests with this `Idempotency-Key` are racing for an expired key (`request_in_progress`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - }, - "Retry-After": { - "description": "Seconds to wait.", - "schema": { - "type": "integer" - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53814,8 +61338,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "404": { + "description": "No signature request with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53836,7 +61360,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`, see `issues`), `to` isn't a valid number (`invalid_phone_number`), `from` isn't one of the organization's numbers (`sender_not_found`) or can't send yet (`sender_not_ready`), the destination country isn't allowed (`country_not_allowed`), or the `Idempotency-Key` was already used with a different body (`idempotency_key_reused`).", + "description": "The request isn't a draft (`request_not_draft`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53857,7 +61381,7 @@ } }, "429": { - "description": "Too many requests with this API key (`rate_limited`), or the message's segments exceed the send quota or too many messages went to this number in the last hour (`quota_exceeded`). Retry after `Retry-After` seconds.", + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53869,7 +61393,7 @@ } }, "Retry-After": { - "description": "Seconds to wait.", + "description": "Seconds to wait before retrying.", "schema": { "type": "integer" } @@ -53926,14 +61450,16 @@ } } } - }, - "get": { - "operationId": "listSms", + } + }, + "/v1/sign/requests/{id}/cancel": { + "post": { + "operationId": "cancelSignRequest", "tags": [ - "SMS" + "Signature requests" ], - "summary": "List SMS", - "description": "Messages sent from the API key's environment, newest first. Pass the last `id` as `after` to get the next page while `has_more` is true.", + "summary": "Cancel a signature request", + "description": "Cancels a `draft` or `sent` request: its signers can no longer sign. An already canceled request is returned as is.", "security": [ { "bearerAuth": [] @@ -53943,68 +61469,15 @@ { "schema": { "type": "string", - "pattern": "^\\+[1-9]\\d{6,14}$", - "description": "Only messages to this number." - }, - "required": false, - "description": "Only messages to this number.", - "name": "to", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "Only messages in these statuses, comma-separated: `failed`." - }, - "required": false, - "description": "Only messages in these statuses, comma-separated: `failed`.", - "name": "status", - "in": "query" - }, - { - "schema": { - "type": "string", - "format": "date-time", - "description": "Only messages created at or after this time." - }, - "required": false, - "description": "Only messages created at or after this time.", - "name": "created_after", - "in": "query" - }, - { - "schema": { - "type": "string", - "format": "date-time", - "description": "Only messages created before this time." - }, - "required": false, - "description": "Only messages created before this time.", - "name": "created_before", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 20." - }, - "required": false, - "description": "1-100, default 20.", - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "maxLength": 64, - "description": "Cursor: the `id` of the last item of the previous page." + "description": "The signature request's id.", + "examples": [ + "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "required": false, - "description": "Cursor: the `id` of the last item of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The signature request's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -54012,7 +61485,7 @@ ], "responses": { "200": { - "description": "A page of messages.", + "description": "The canceled request.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54036,7 +61509,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SmsList" + "$ref": "#/components/schemas/SignRequest" } } } @@ -54104,8 +61577,29 @@ } } }, + "404": { + "description": "No signature request with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "description": "The request is already completed, declined or expired (`request_not_cancelable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54197,62 +61691,40 @@ } } }, - "/v1/sms/suppressions": { - "post": { - "operationId": "createSmsSuppression", + "/v1/sign/requests/{id}/audit_trail": { + "get": { + "operationId": "listSignRequestEvents", "tags": [ - "SMS" + "Signature requests" ], - "summary": "Add a number to the SMS suppression list", - "description": "Later sends from the API key's environment to the number are skipped. Adding a number already on the list returns its entry with status 200.", + "summary": "List a signature request's audit trail", + "description": "Everything that happened to the request, oldest first: who did it, from which address, the documents' SHA-256 when it mattered, and a hash chain (`previous_hash`, `hash`) that shows the trail is complete and unchanged.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSmsSuppressionRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The signature request's id.", + "examples": [ + "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The signature request's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { "200": { - "description": "The number already had an entry.", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - }, - "Coss-Version": { - "description": "The API version this response is rendered in.", - "schema": { - "type": "string", - "examples": [ - "2026-10-10" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SmsSuppression" - } - } - } - }, - "201": { - "description": "The new entry.", + "description": "The audit trail.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54276,13 +61748,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SmsSuppression" + "$ref": "#/components/schemas/SignEventList" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54344,50 +61816,8 @@ } } }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The body failed validation (`validation_error`) or the number isn't valid (`invalid_phone_number`).", + "404": { + "description": "No signature request with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54476,20 +61906,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, - "get": { - "operationId": "listSmsSuppressions", + } + } + }, + "/v1/sign/requests/{id}/remind": { + "post": { + "operationId": "remindSignRequest", "tags": [ - "SMS" + "Signature requests" ], - "summary": "List suppressed numbers", - "description": "The SMS suppression list of the API key's environment, newest first: opt-outs, invalid numbers and numbers you added.", + "summary": "Remind signers", + "description": "Emails a reminder, with a fresh link, to every email signer of a sent request who hasn't signed and whose turn it is, unless they were emailed in the last hour. Embedded signers are yours to remind. Sent within seconds; each is a `reminded` event in the audit trail.", "security": [ { "bearerAuth": [] @@ -54499,36 +61926,15 @@ { "schema": { "type": "string", - "pattern": "^\\+[1-9]\\d{6,14}$", - "description": "Only this number's entry." - }, - "required": false, - "description": "Only this number's entry.", - "name": "phone_number", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 20." - }, - "required": false, - "description": "1-100, default 20.", - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "maxLength": 64, - "description": "Cursor: the `id` of the last item of the previous page." + "description": "The signature request's id.", + "examples": [ + "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "required": false, - "description": "Cursor: the `id` of the last item of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The signature request's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -54536,7 +61942,7 @@ ], "responses": { "200": { - "description": "A page of entries.", + "description": "The request.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54560,7 +61966,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SmsSuppressionList" + "$ref": "#/components/schemas/SignRequest" } } } @@ -54628,8 +62034,29 @@ } } }, + "404": { + "description": "No signature request with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "description": "The request isn't `sent`, or nobody can be reminded now (`reminder_not_due`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54721,14 +62148,14 @@ } } }, - "/v1/sms/suppressions/check": { + "/v1/sign/requests/{id}/signed_document": { "get": { - "operationId": "checkSmsSuppression", + "operationId": "getSignedDocument", "tags": [ - "SMS" + "Signature requests" ], - "summary": "Check whether a number is suppressed", - "description": "Whether a phone number is on the SMS suppression list of the API key's environment, with its entry if it is.", + "summary": "Download the signed document", + "description": "The completed request's sealed PDF: every document with the signatures and values filled in, then a certificate of completion (signers, how they were identified, what they agreed to, the documents' SHA-256 and the audit trail). coss.com's seal makes any later change visible in PDF viewers. Ready once `signed_document` is set, which the `sign_request.completed` webhook announces.", "security": [ { "bearerAuth": [] @@ -54738,11 +62165,15 @@ { "schema": { "type": "string", - "pattern": "^\\+[1-9]\\d{6,14}$" + "description": "The signature request's id.", + "examples": [ + "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, "required": true, - "name": "phone_number", - "in": "query" + "description": "The signature request's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -54750,7 +62181,7 @@ ], "responses": { "200": { - "description": "The result.", + "description": "The sealed PDF.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54772,9 +62203,10 @@ } }, "content": { - "application/json": { + "application/pdf": { "schema": { - "$ref": "#/components/schemas/SmsSuppressionCheck" + "type": "string", + "format": "binary" } } } @@ -54842,8 +62274,29 @@ } } }, + "404": { + "description": "No signature request with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "description": "The request isn't completed, or its document is still being sealed (`signed_document_not_ready`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54912,7 +62365,7 @@ } }, "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "description": "Sign is temporarily unavailable (`sign_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54935,14 +62388,14 @@ } } }, - "/v1/sms/suppressions/{id}": { - "get": { - "operationId": "getSmsSuppression", + "/v1/sign/requests/{id}/signers/{signer_id}/sessions": { + "post": { + "operationId": "createSignSession", "tags": [ - "SMS" + "Signature requests" ], - "summary": "Retrieve an SMS suppression", - "description": "One entry of the SMS suppression list of the API key's environment.", + "summary": "Create a signing session", + "description": "A short-lived link to the signing page for one `embedded` signer of a sent request. Your app vouches for who the signer is (the audit trail records `customer_asserted`), so create it only for that signed-in user, when they open the page that shows it. Embed the `url` in an iframe from an origin in your Sign settings' `allowed_origins` (`mountSigning` from `coss/sign` does it), or redirect to it. Each call creates a new session.", "security": [ { "bearerAuth": [] @@ -54952,23 +62405,46 @@ { "schema": { "type": "string", - "description": "The entry's id.", + "description": "The signature request's id.", "examples": [ - "smssup_01k7c8m4q2f3e9v6t5r1n0p8yz" + "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The entry's id.", + "description": "The signature request's id.", "name": "id", "in": "path" }, + { + "schema": { + "type": "string", + "description": "The signer's id (`signers[].id` of the request).", + "examples": [ + "ssgn_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The signer's id (`signers[].id` of the request).", + "name": "signer_id", + "in": "path" + }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignSessionRequest" + } + } + } + }, "responses": { - "200": { - "description": "The entry.", + "201": { + "description": "The session, with its link.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54992,7 +62468,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SmsSuppression" + "$ref": "#/components/schemas/SignSession" } } } @@ -55061,7 +62537,28 @@ } }, "404": { - "description": "No entry with this id exists in the API key's environment (`not_found`).", + "description": "No such signer on a signature request in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The signer signs by email (`signer_not_embedded`), already signed or declined (`signer_finished`), or the request isn't `sent` or has expired (`request_not_signable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -55130,7 +62627,7 @@ } }, "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "description": "Sign is temporarily unavailable (`sign_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -55151,40 +62648,24 @@ } } } - }, - "delete": { - "operationId": "deleteSmsSuppression", + } + }, + "/v1/sign/settings": { + "get": { + "operationId": "getSignSettings", "tags": [ - "SMS" + "Sign settings" ], - "summary": "Remove a number from the SMS suppression list", - "description": "Sends to the number go out again. Remove an opt-out only when the person asked to receive messages again (they can also reply START).", + "summary": "Get the Sign settings", + "description": "The API key's environment's settings: which sites may embed the signing page, how it looks, and the consent text signers accept.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The entry's id.", - "examples": [ - "smssup_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The entry's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], "responses": { "200": { - "description": "The entry was removed.", + "description": "The settings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -55208,7 +62689,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSmsSuppression" + "$ref": "#/components/schemas/SignSettings" } } } @@ -55276,27 +62757,6 @@ } } }, - "404": { - "description": "No entry with this id exists in the API key's environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -55366,93 +62826,38 @@ } } } - } - } - }, - "/v1/sms/received": { - "get": { - "operationId": "listReceivedSms", + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, + "patch": { + "operationId": "updateSignSettings", "tags": [ - "SMS" + "Sign settings" ], - "summary": "List received SMS", - "description": "Messages sent to the organization's numbers whose replies go to the API key's environment, newest first.", + "summary": "Update the Sign settings", + "description": "Changes the fields given; the others keep their values.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "pattern": "^\\+[1-9]\\d{6,14}$", - "description": "Only messages from this number." - }, - "required": false, - "description": "Only messages from this number.", - "name": "from", - "in": "query" - }, - { - "schema": { - "type": "string", - "pattern": "^\\+[1-9]\\d{6,14}$", - "description": "Only messages to this number of yours." - }, - "required": false, - "description": "Only messages to this number of yours.", - "name": "to", - "in": "query" - }, - { - "schema": { - "type": "string", - "format": "date-time" - }, - "required": false, - "name": "created_after", - "in": "query" - }, - { - "schema": { - "type": "string", - "format": "date-time" - }, - "required": false, - "name": "created_before", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 20." - }, - "required": false, - "description": "1-100, default 20.", - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "maxLength": 64, - "description": "Cursor: the `id` of the last item of the previous page." - }, - "required": false, - "description": "Cursor: the `id` of the last item of the previous page.", - "name": "after", - "in": "query" - }, - { - "$ref": "#/components/parameters/CossVersion" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSignSettingsRequest" + } + } } - ], + }, "responses": { "200": { - "description": "A page of received messages.", + "description": "The settings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -55476,7 +62881,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReceivedSmsList" + "$ref": "#/components/schemas/SignSettings" } } } @@ -55545,7 +62950,7 @@ } }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "description": "The body failed validation (`validation_error`), or an origin isn't `https://host[:port]` (`invalid_origin`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -55634,43 +63039,40 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/sms/received/{id}": { - "get": { - "operationId": "getReceivedSms", + "/v1/sign/templates": { + "post": { + "operationId": "createSignTemplate", "tags": [ - "SMS" + "Sign templates" ], - "summary": "Retrieve a received SMS", - "description": "One message received on a number of the organization and routed to the API key's environment.", + "summary": "Create a template", + "description": "Documents with fields for roles (`client`, `company`) instead of named signers, made into requests by naming a signer per role. Fields by position or text anchor (resolved now). Can start empty and be built in the embedded builder (create a builder session).", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The received message's id.", - "examples": [ - "smsin_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The received message's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignTemplateRequest" + } + } } - ], + }, "responses": { - "200": { - "description": "The received message.", + "201": { + "description": "The template.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -55694,13 +63096,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReceivedSms" + "$ref": "#/components/schemas/SignTemplate" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -55762,8 +63164,29 @@ } } }, - "404": { - "description": "No received message with this id exists in the API key's environment (`not_found`).", + "413": { + "description": "The request body is larger than 40 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The body failed validation (`validation_error`), a document isn't a PDF coss.com can read (`invalid_document`) or isn't in the environment (`not_found`), a field doesn't fit or names an unknown role or anchor (`invalid_fields`), or a role is listed twice (`invalid_signers`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -55832,7 +63255,7 @@ } }, "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "description": "Sign is temporarily unavailable (`sign_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -55852,25 +63275,56 @@ } } } - } - } - }, - "/v1/sms/numbers": { + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, "get": { - "operationId": "listSmsNumbers", + "operationId": "listSignTemplates", "tags": [ - "SMS" + "Sign templates" ], - "summary": "List the organization's phone numbers", - "description": "Every number of the organization, whether it can send live messages now (its carrier registration is approved) and whether its replies arrive in the API key's environment. Numbers are bought and registered in the dashboard.", + "summary": "List templates", + "description": "Newest first. Pass the last `id` as `after` while `has_more` is true.", "security": [ { "bearerAuth": [] } ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "1-100, default 20." + }, + "required": false, + "description": "1-100, default 20.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "maxLength": 64, + "description": "Cursor: the `id` of the last item of the previous page." + }, + "required": false, + "description": "Cursor: the `id` of the last item of the previous page.", + "name": "after", + "in": "query" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "responses": { "200": { - "description": "The numbers.", + "description": "A page of templates.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -55894,7 +63348,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SmsNumberList" + "$ref": "#/components/schemas/SignTemplateList" } } } @@ -55962,6 +63416,27 @@ } } }, + "422": { + "description": "A query parameter failed validation (`validation_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -56031,22 +63506,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/sms/{id}": { + "/v1/sign/templates/{id}": { "get": { - "operationId": "getSms", + "operationId": "getSignTemplate", "tags": [ - "SMS" + "Sign templates" ], - "summary": "Retrieve an SMS", - "description": "One message sent from the API key's environment, with its current status.", + "summary": "Retrieve a template", + "description": "A template with its documents, roles and fields. `ready` says whether requests can be made from it.", "security": [ { "bearerAuth": [] @@ -56056,13 +63526,13 @@ { "schema": { "type": "string", - "description": "The message's id.", + "description": "The template's id.", "examples": [ - "sms_01k7c8m4q2f3e9v6t5r1n0p8yz" + "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The message's id.", + "description": "The template's id.", "name": "id", "in": "path" }, @@ -56072,7 +63542,7 @@ ], "responses": { "200": { - "description": "The message.", + "description": "The template.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56096,7 +63566,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Sms" + "$ref": "#/components/schemas/SignTemplate" } } } @@ -56165,7 +63635,7 @@ } }, "404": { - "description": "No message with this id exists in the API key's environment (`not_found`).", + "description": "No template with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56255,16 +63725,14 @@ } } } - } - }, - "/v1/sms/{id}/events": { - "get": { - "operationId": "listSmsEvents", + }, + "patch": { + "operationId": "updateSignTemplate", "tags": [ - "SMS" + "Sign templates" ], - "summary": "List an SMS's events", - "description": "Everything that happened to the message, oldest first.", + "summary": "Update a template", + "description": "Changes the fields given. `documents`, `roles` and `fields` each replace the whole list. Requests already made from it don't change.", "security": [ { "bearerAuth": [] @@ -56274,13 +63742,13 @@ { "schema": { "type": "string", - "description": "The message's id.", + "description": "The template's id.", "examples": [ - "sms_01k7c8m4q2f3e9v6t5r1n0p8yz" + "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The message's id.", + "description": "The template's id.", "name": "id", "in": "path" }, @@ -56288,9 +63756,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSignTemplateRequest" + } + } + } + }, "responses": { "200": { - "description": "The events.", + "description": "The template.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56314,13 +63792,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SmsEventList" + "$ref": "#/components/schemas/SignTemplate" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56383,7 +63861,49 @@ } }, "404": { - "description": "No message with this id exists in the API key's environment (`not_found`).", + "description": "No template with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "The request body is larger than 40 MB (`payload_too_large`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The body failed validation (`validation_error`), a document isn't a PDF coss.com can read (`invalid_document`) or isn't in the environment (`not_found`), a field doesn't fit or names an unknown role or anchor (`invalid_fields`), or a role is listed twice (`invalid_signers`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56452,7 +63972,7 @@ } }, "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "description": "Sign is temporarily unavailable (`sign_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56473,16 +63993,14 @@ } } } - } - }, - "/v1/sms/{id}/cancel": { - "post": { - "operationId": "cancelSms", + }, + "delete": { + "operationId": "deleteSignTemplate", "tags": [ - "SMS" + "Sign templates" ], - "summary": "Cancel a scheduled SMS", - "description": "Cancels a message scheduled with `scheduled_at` before it is sent. Canceling a canceled message returns it as is.", + "summary": "Delete a template", + "description": "Requests made from it, and its documents, stay.", "security": [ { "bearerAuth": [] @@ -56492,13 +64010,13 @@ { "schema": { "type": "string", - "description": "The message's id.", + "description": "The template's id.", "examples": [ - "sms_01k7c8m4q2f3e9v6t5r1n0p8yz" + "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The message's id.", + "description": "The template's id.", "name": "id", "in": "path" }, @@ -56508,7 +64026,7 @@ ], "responses": { "200": { - "description": "The canceled message.", + "description": "The deleted template.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56532,7 +64050,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Sms" + "$ref": "#/components/schemas/DeletedSignTemplate" } } } @@ -56601,28 +64119,7 @@ } }, "404": { - "description": "No message with this id exists in the API key's environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The message isn't scheduled (`sms_not_cancelable`).", + "description": "No template with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56714,32 +64211,65 @@ } } }, - "/v1/sign/documents": { + "/v1/sign/templates/{id}/requests": { "post": { - "operationId": "uploadSignDocument", + "operationId": "createSignRequestFromTemplate", "tags": [ - "Sign documents" + "Sign templates" ], - "summary": "Upload a document", - "description": "Uploads a PDF (base64, at most 25 MB and 500 pages, not password-protected) for signature requests to use. The response lists each page's size and the text anchors found in it (`{{signature:signer_1}}`), which fields can be placed by. Requests can also take PDFs inline.", + "summary": "Create a request from a template", + "description": "A signature request with the template's documents and fields: name one signer for each role, and give its merge fields' values. Like creating a request, it is sent at once unless `send` is `false`, and an `Idempotency-Key` makes retries safe.", "security": [ { "bearerAuth": [] } ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The template's id.", + "examples": [ + "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The template's id.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Makes retries safe: a repeated request with the same key and body returns the original request (in its current state) instead of creating another, for 24 hours. 1-256 characters.", + "examples": [ + "welcome-user-123" + ] + }, + "required": false, + "description": "Makes retries safe: a repeated request with the same key and body returns the original request (in its current state) instead of creating another, for 24 hours. 1-256 characters.", + "name": "Idempotency-Key", + "in": "header" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UploadSignDocumentRequest" + "$ref": "#/components/schemas/CreateSignRequestFromTemplateRequest" } } } }, "responses": { "201": { - "description": "The uploaded document.", + "description": "The signature request.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56750,6 +64280,15 @@ ] } }, + "Idempotent-Replayed": { + "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", + "schema": { + "type": "string", + "enum": [ + "true" + ] + } + }, "Coss-Version": { "description": "The API version this response is rendered in.", "schema": { @@ -56763,13 +64302,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignDocument" + "$ref": "#/components/schemas/SignRequest" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`) or the `Idempotency-Key` header is invalid (`invalid_idempotency_key`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56831,8 +64370,8 @@ } } }, - "413": { - "description": "The request body is larger than 40 MB (`payload_too_large`).", + "404": { + "description": "No template with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56852,8 +64391,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "409": { + "description": "Requests with this `Idempotency-Key` are racing (`request_in_progress`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56874,7 +64413,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`, see `issues`), or the file isn't a PDF coss.com can read (`invalid_document`: damaged, encrypted or too many pages).", + "description": "The template isn't ready (`template_incomplete`), a role has no signer or two (`invalid_signers`), a merge field doesn't exist (`invalid_fields`), `expires_at` is out of range (`invalid_expiry`), or the `Idempotency-Key` was used with a different body (`idempotency_key_reused`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56963,56 +64502,53 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, - "get": { - "operationId": "listSignDocuments", + } + } + }, + "/v1/sign/templates/{id}/builder_sessions": { + "post": { + "operationId": "createSignBuilderSession", "tags": [ - "Sign documents" + "Sign templates" ], - "summary": "List documents", - "description": "Documents uploaded to the API key's environment, newest first. Pass the last `id` as `after` to get the next page while `has_more` is true.", + "summary": "Create a builder session", + "description": "A short-lived link to the embedded builder for this template: upload documents, add roles, and drag fields onto pages. Embed it with `mountBuilder` from `coss/sign` (from an origin in your Sign settings' `allowed_origins`) for whoever edits the template.", "security": [ { "bearerAuth": [] } ], "parameters": [ - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 20." - }, - "required": false, - "description": "1-100, default 20.", - "name": "limit", - "in": "query" - }, { "schema": { "type": "string", - "maxLength": 64, - "description": "Cursor: the `id` of the last item of the previous page." + "description": "The template's id.", + "examples": [ + "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "required": false, - "description": "Cursor: the `id` of the last item of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The template's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignBuilderSessionRequest" + } + } + } + }, "responses": { - "200": { - "description": "A page of documents.", + "201": { + "description": "The builder session, with its link.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57036,7 +64572,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignDocumentList" + "$ref": "#/components/schemas/SignBuilderSession" } } } @@ -57104,8 +64640,8 @@ } } }, - "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "404": { + "description": "No template with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57174,7 +64710,7 @@ } }, "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "description": "Sign is temporarily unavailable (`sign_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57197,14 +64733,14 @@ } } }, - "/v1/sign/documents/{id}": { - "get": { - "operationId": "getSignDocument", + "/v1/verifications": { + "post": { + "operationId": "startVerification", "tags": [ - "Sign documents" + "Verifications" ], - "summary": "Retrieve a document", - "description": "An uploaded document's details: its hash, page sizes and text anchors. Its bytes are never returned here.", + "summary": "Start a verification", + "description": "Sends a one-time code to a phone number (SMS) or an email address and returns the verification, `pending` until you check the code the end user typed (`POST /v1/verifications/check`). Starting again for the same destination while it is pending sends a new code (the earlier one stops working), up to 5 times. Before a new verification costs anything, the fraud guard checks it (your Risk lists, premium-rate numbers, Tor, and velocity per IP, user, number range and country, with how often their codes are entered): a blocked one is stored with its `reasons` and nothing is sent. Pass the end user's `ip` (and your `user_id`) so it can tell them apart. SMS codes come from coss.com's shared numbers unless the environment's Verify settings name one of your own; email codes from your sender, else coss.com's. Sandbox (`coss_test_…`) keys deliver nothing and the code is always zeros.", "security": [ { "bearerAuth": [] @@ -57214,23 +64750,35 @@ { "schema": { "type": "string", - "description": "The document's id.", + "minLength": 1, + "maxLength": 256, + "description": "Makes retries safe: a repeated request with the same key and body returns the original verification instead of sending another code, for 24 hours. 1-256 characters.", "examples": [ - "sdoc_01k7c8m4q2f3e9v6t5r1n0p8yz" + "welcome-user-123" ] }, - "required": true, - "description": "The document's id.", - "name": "id", - "in": "path" + "required": false, + "description": "Makes retries safe: a repeated request with the same key and body returns the original verification instead of sending another code, for 24 hours. 1-256 characters.", + "name": "Idempotency-Key", + "in": "header" }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartVerificationRequest" + } + } + } + }, "responses": { - "200": { - "description": "The document.", + "201": { + "description": "The verification, `pending`, with the send that just went out.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57241,6 +64789,15 @@ ] } }, + "Idempotent-Replayed": { + "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", + "schema": { + "type": "string", + "enum": [ + "true" + ] + } + }, "Coss-Version": { "description": "The API version this response is rendered in.", "schema": { @@ -57254,13 +64811,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignDocument" + "$ref": "#/components/schemas/Verification" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`) or the `Idempotency-Key` header is invalid (`invalid_idempotency_key`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57301,6 +64858,27 @@ } } }, + "402": { + "description": "Live sending is paused for an overdue invoice or the spend limit (`billing_hold`) or a used-up budget (`budget_exceeded`), or the plan doesn't include Verify or the destination country isn't priced (`plan_feature_unavailable`) (`billing_error`). Never retried: it needs a plan change or a payment.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -57322,8 +64900,35 @@ } } }, - "404": { - "description": "No document with this id exists in the API key's environment (`not_found`).", + "409": { + "description": "Requests with this `Idempotency-Key` are racing (`request_in_progress`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The body failed validation (`validation_error`, see `issues`); `to` isn't a valid number or address (`invalid_destination`) or its country isn't allowed (`country_not_allowed`); the fraud guard blocked it (`verification_blocked`: the verification is stored with its `reasons`); the code was already sent 5 times (`too_many_sends`); the recipient opted out or is suppressed (`recipient_suppressed`); the code couldn't be sent (`send_failed`, `sender_not_ready`…); or the `Idempotency-Key` was already used with a different body (`idempotency_key_reused`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57344,7 +64949,7 @@ } }, "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "description": "Too many requests with this API key (`rate_limited`), or too many codes to this destination or from coss.com's shared numbers today (`quota_exceeded`). Retry after `Retry-After` seconds.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57392,7 +64997,7 @@ } }, "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "description": "coss.com's shared numbers aren't available for this destination (`shared_sender_unavailable`): name your own number in Verify's settings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57414,13 +65019,13 @@ } } }, - "delete": { - "operationId": "deleteSignDocument", + "get": { + "operationId": "listVerifications", "tags": [ - "Sign documents" + "Verifications" ], - "summary": "Delete a document", - "description": "Deletes an uploaded document and its file. Documents a request uses can't be deleted.", + "summary": "List verifications", + "description": "Verifications of the API key's environment, newest first. Pass the last `id` as `after` to get the next page while `has_more` is true.", "security": [ { "bearerAuth": [] @@ -57430,15 +65035,55 @@ { "schema": { "type": "string", - "description": "The document's id.", - "examples": [ - "sdoc_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "minLength": 1, + "maxLength": 320, + "description": "Only verifications to this destination." }, - "required": true, - "description": "The document's id.", - "name": "id", - "in": "path" + "required": false, + "description": "Only verifications to this destination.", + "name": "to", + "in": "query" + }, + { + "schema": { + "type": "string", + "enum": [ + "pending", + "approved", + "expired", + "canceled", + "failed", + "blocked" + ], + "description": "Only verifications in this status." + }, + "required": false, + "description": "Only verifications in this status.", + "name": "status", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "1-100, default 20." + }, + "required": false, + "description": "1-100, default 20.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "maxLength": 64, + "description": "Cursor: the `id` of the last item of the previous page." + }, + "required": false, + "description": "Cursor: the `id` of the last item of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -57446,7 +65091,7 @@ ], "responses": { "200": { - "description": "The document was deleted.", + "description": "A page of verifications.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57470,7 +65115,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSignDocument" + "$ref": "#/components/schemas/VerificationList" } } } @@ -57538,29 +65183,8 @@ } } }, - "404": { - "description": "No document with this id exists in the API key's environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "422": { - "description": "A signature request uses the document (`document_in_use`).", + "description": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57629,7 +65253,7 @@ } }, "503": { - "description": "Sign is temporarily unavailable (`sign_unavailable`).", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57652,14 +65276,14 @@ } } }, - "/v1/sign/requests": { + "/v1/verifications/check": { "post": { - "operationId": "createSignRequest", + "operationId": "checkVerification", "tags": [ - "Signature requests" + "Verifications" ], - "summary": "Create a signature request", - "description": "Asks one or more signers to sign one or more PDFs. Give documents by id (uploaded) or inline, and each signer's fields by position (`page`, `x`, `y`, `width`, `height` in PDF points from the page's top left) or by text anchor (`{{signature:signer_1}}`). Every signer needs a `signature` field. Sent at once unless `send` is `false` (a `draft`). Each signer signs by email (`delivery: \"email\"`, the default) or in your app (`\"embedded\"`).", + "summary": "Check a code", + "description": "Checks the code the end user typed, by the verification's id or by the destination (its latest verification). `valid: true` approves it (`verification.approved`, billed once); a wrong code spends an attempt, and the last one fails it. Checking an approved verification again with its code stays valid until it expires, without being billed again.", "security": [ { "bearerAuth": [] @@ -57671,13 +65295,13 @@ "type": "string", "minLength": 1, "maxLength": 256, - "description": "Makes retries safe: a repeated request with the same key and body returns the original request (in its current state) instead of creating another, for 24 hours. 1-256 characters.", + "description": "Makes retries safe: a repeated check with the same key returns the same answer without spending another attempt. 1-256 characters.", "examples": [ "welcome-user-123" ] }, "required": false, - "description": "Makes retries safe: a repeated request with the same key and body returns the original request (in its current state) instead of creating another, for 24 hours. 1-256 characters.", + "description": "Makes retries safe: a repeated check with the same key returns the same answer without spending another attempt. 1-256 characters.", "name": "Idempotency-Key", "in": "header" }, @@ -57690,14 +65314,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateSignRequestRequest" + "$ref": "#/components/schemas/CheckVerificationRequest" } } } }, "responses": { - "201": { - "description": "The signature request (`sent`, or `draft`). A replay returns the original request in its current state.", + "200": { + "description": "Whether the code is right, and the verification after the check.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57708,15 +65332,6 @@ ] } }, - "Idempotent-Replayed": { - "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", - "schema": { - "type": "string", - "enum": [ - "true" - ] - } - }, "Coss-Version": { "description": "The API version this response is rendered in.", "schema": { @@ -57730,7 +65345,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignRequest" + "$ref": "#/components/schemas/VerificationCheck" } } } @@ -57777,27 +65392,6 @@ } } }, - "402": { - "description": "In live environments: an overdue invoice or spend limit (`billing_hold`), or the project's budget (`budget_exceeded`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -57819,50 +65413,8 @@ } } }, - "409": { - "description": "Requests with this `Idempotency-Key` are racing for an expired key (`request_in_progress`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "413": { - "description": "The request body is larger than 40 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "404": { + "description": "No such verification in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57883,7 +65435,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`, see `issues`), a document isn't a PDF coss.com can read (`invalid_document`) or isn't in the environment (`not_found`), a field doesn't fit on its page or names an anchor no document has (`invalid_fields`), a signer is listed twice (`invalid_signers`), `expires_at` is out of range (`invalid_expiry`), or the `Idempotency-Key` was already used with a different body (`idempotency_key_reused`).", + "description": "The body failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57904,7 +65456,7 @@ } }, "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "description": "Too many requests with this API key (`rate_limited`), or too many checks for this destination (`quota_exceeded`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57952,7 +65504,7 @@ } }, "503": { - "description": "Sign is temporarily unavailable (`sign_unavailable`).", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57973,14 +65525,16 @@ } } } - }, + } + }, + "/v1/verifications/{id}": { "get": { - "operationId": "listSignRequests", + "operationId": "getVerification", "tags": [ - "Signature requests" + "Verifications" ], - "summary": "List signature requests", - "description": "Signature requests of the API key's environment, newest first. Pass the last `id` as `after` to get the next page while `has_more` is true.", + "summary": "Retrieve a verification", + "description": "A verification's status and destination. Its code is never returned.", "security": [ { "bearerAuth": [] @@ -57990,35 +65544,15 @@ { "schema": { "type": "string", - "description": "Only requests in these statuses, comma-separated: `sent,draft`." - }, - "required": false, - "description": "Only requests in these statuses, comma-separated: `sent,draft`.", - "name": "status", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 20." - }, - "required": false, - "description": "1-100, default 20.", - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "maxLength": 64, - "description": "Cursor: the `id` of the last item of the previous page." + "description": "The verification's id.", + "examples": [ + "vrf_01k8e2m4q2f3e9v6t5r1n0p8yz" + ] }, - "required": false, - "description": "Cursor: the `id` of the last item of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The verification's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -58026,7 +65560,7 @@ ], "responses": { "200": { - "description": "A page of signature requests.", + "description": "The verification.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58050,7 +65584,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignRequestList" + "$ref": "#/components/schemas/Verification" } } } @@ -58118,8 +65652,8 @@ } } }, - "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "404": { + "description": "No such verification in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58211,14 +65745,14 @@ } } }, - "/v1/sign/requests/{id}": { - "get": { - "operationId": "getSignRequest", + "/v1/verifications/{id}/cancel": { + "post": { + "operationId": "cancelVerification", "tags": [ - "Signature requests" + "Verifications" ], - "summary": "Retrieve a signature request", - "description": "A signature request with its documents, signers and fields, and `signed_document` once it's sealed.", + "summary": "Cancel a verification", + "description": "Cancels a pending verification: its code stops working. Canceling it again returns it as is.", "security": [ { "bearerAuth": [] @@ -58228,13 +65762,13 @@ { "schema": { "type": "string", - "description": "The signature request's id.", + "description": "The verification's id.", "examples": [ - "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" + "vrf_01k8e2m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The signature request's id.", + "description": "The verification's id.", "name": "id", "in": "path" }, @@ -58244,7 +65778,7 @@ ], "responses": { "200": { - "description": "The signature request.", + "description": "The canceled verification.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58268,7 +65802,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignRequest" + "$ref": "#/components/schemas/Verification" } } } @@ -58337,7 +65871,28 @@ } }, "404": { - "description": "No signature request with this id exists in the API key's environment (`not_found`).", + "description": "No such verification in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The verification isn't pending (`verification_not_cancelable`): approved, failed, blocked or expired.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58427,40 +65982,24 @@ } } } - }, - "delete": { - "operationId": "deleteSignRequest", + } + }, + "/v1/verify/settings": { + "get": { + "operationId": "getVerifySettings", "tags": [ - "Signature requests" + "Verify settings" ], - "summary": "Delete a signature request", - "description": "Deletes a request that isn't `sent` (cancel it first) with its signers, fields, audit trail, signatures and signed document, for good. Its documents stay (delete them on their own). Keep the signed document first if you need it: it is your record.", + "summary": "Get the Verify settings", + "description": "The API key's environment's settings, with defaults filled in: the name codes are sent for, code length, lifetime and attempts, the senders, and the countries SMS codes may go to.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The signature request's id.", - "examples": [ - "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The signature request's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], "responses": { "200": { - "description": "The deleted request.", + "description": "The settings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58484,7 +66023,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSignRequest" + "$ref": "#/components/schemas/VerifySettings" } } } @@ -58552,48 +66091,6 @@ } } }, - "404": { - "description": "No signature request with this id exists in the API key's environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The request is `sent`: cancel it first (`request_not_deletable`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -58643,7 +66140,7 @@ } }, "503": { - "description": "Sign is temporarily unavailable (`sign_unavailable`).", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58663,43 +66160,38 @@ } } } - } - } - }, - "/v1/sign/requests/{id}/send": { - "post": { - "operationId": "sendSignRequest", + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, + "patch": { + "operationId": "updateVerifySettings", "tags": [ - "Signature requests" + "Verify settings" ], - "summary": "Send a draft signature request", - "description": "Sends a `draft`. An already sent request is returned as is.", + "summary": "Update the Verify settings", + "description": "Changes the fields given; the others keep their values. `sms_from` must be one of the organization's numbers, and a live key's `email_from` must be on one of its verified domains.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The signature request's id.", - "examples": [ - "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The signature request's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateVerifySettingsRequest" + } + } } - ], + }, "responses": { "200": { - "description": "The sent request.", + "description": "The settings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58723,7 +66215,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignRequest" + "$ref": "#/components/schemas/VerifySettings" } } } @@ -58770,27 +66262,6 @@ } } }, - "402": { - "description": "In live environments: an overdue invoice or spend limit (`billing_hold`), or the project's budget (`budget_exceeded`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -58812,29 +66283,8 @@ } } }, - "404": { - "description": "No signature request with this id exists in the API key's environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "422": { - "description": "The request isn't a draft (`request_not_draft`).", + "description": "The body failed validation (`validation_error`), or a sender isn't the organization's (`invalid_settings`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58923,43 +66373,40 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/sign/requests/{id}/cancel": { + "/v1/forms": { "post": { - "operationId": "cancelSignRequest", + "operationId": "createForm", "tags": [ - "Signature requests" + "Forms" ], - "summary": "Cancel a signature request", - "description": "Cancels a `draft` or `sent` request: its signers can no longer sign. An already canceled request is returned as is.", + "summary": "Create a form", + "description": "A hosted form endpoint: point an HTML `
` (or `coss/forms`) at its `endpoint`. Every submission is screened by Risk (the honeypot, captcha, email, IP, device, velocity and content) and accepted ones go to webhooks (`form.submission.received`), notification emails and contacts. Without `fields`, any field is accepted.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The signature request's id.", - "examples": [ - "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The signature request's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFormRequest" + } + } } - ], + }, "responses": { - "200": { - "description": "The canceled request.", + "201": { + "description": "The form.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58983,13 +66430,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignRequest" + "$ref": "#/components/schemas/Form" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59051,8 +66498,8 @@ } } }, - "404": { - "description": "No signature request with this id exists in the API key's environment (`not_found`).", + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59073,7 +66520,7 @@ } }, "422": { - "description": "The request is already completed, declined or expired (`request_not_cancelable`).", + "description": "The body failed validation (`validation_error`), or the form can't be saved: duplicate or misplaced fields (`invalid_fields`), an origin or redirect that isn't https (`invalid_origin`, `invalid_redirect`), a sender or recipient that isn't verified (`invalid_sender`, `invalid_recipient`), an unknown audience, topic or property (`invalid_contacts`), no captcha in the risk settings (`captcha_not_configured`), a hosted page without fields or asking for credentials (`hosted_needs_fields`, `hosted_content_refused`), a blocked file type or uploads turned off while a `file` field remains (`invalid_files`), or too many forms (`form_limit_reached`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59142,7 +66589,7 @@ } }, "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "description": "Forms isn't available in this deployment yet (`forms_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59162,35 +66609,48 @@ } } } - } - } - }, - "/v1/sign/requests/{id}/audit_trail": { + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, "get": { - "operationId": "listSignRequestEvents", + "operationId": "listForms", "tags": [ - "Signature requests" + "Forms" ], - "summary": "List a signature request's audit trail", - "description": "Everything that happened to the request, oldest first: who did it, from which address, the documents' SHA-256 when it mattered, and a hash chain (`previous_hash`, `hash`) that shows the trail is complete and unchanged.", + "summary": "List forms", + "description": "The environment's forms, newest first.", "security": [ { "bearerAuth": [] } ], "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "1-100, default 20." + }, + "required": false, + "description": "1-100, default 20.", + "name": "limit", + "in": "query" + }, { "schema": { "type": "string", - "description": "The signature request's id.", - "examples": [ - "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "maxLength": 64, + "description": "Cursor: the `id` of the last item of the previous page." }, - "required": true, - "description": "The signature request's id.", - "name": "id", - "in": "path" + "required": false, + "description": "Cursor: the `id` of the last item of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -59198,7 +66658,7 @@ ], "responses": { "200": { - "description": "The audit trail.", + "description": "A page of forms.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59222,7 +66682,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignEventList" + "$ref": "#/components/schemas/FormList" } } } @@ -59290,8 +66750,8 @@ } } }, - "404": { - "description": "No signature request with this id exists in the API key's environment (`not_found`).", + "422": { + "description": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59360,7 +66820,7 @@ } }, "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "description": "Forms isn't available in this deployment yet (`forms_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59383,14 +66843,14 @@ } } }, - "/v1/sign/requests/{id}/remind": { - "post": { - "operationId": "remindSignRequest", + "/v1/forms/{id}": { + "get": { + "operationId": "getForm", "tags": [ - "Signature requests" + "Forms" ], - "summary": "Remind signers", - "description": "Emails a reminder, with a fresh link, to every email signer of a sent request who hasn't signed and whose turn it is, unless they were emailed in the last hour. Embedded signers are yours to remind. Sent within seconds; each is a `reminded` event in the audit trail.", + "summary": "Get a form", + "description": "A form's settings, with the `endpoint` it posts to and its `hosted_url`.", "security": [ { "bearerAuth": [] @@ -59400,13 +66860,13 @@ { "schema": { "type": "string", - "description": "The signature request's id.", + "description": "The form's id.", "examples": [ - "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" + "form_01kd2m8n3p4q5r6s7t8v9w0x1y" ] }, "required": true, - "description": "The signature request's id.", + "description": "The form's id.", "name": "id", "in": "path" }, @@ -59416,7 +66876,7 @@ ], "responses": { "200": { - "description": "The request.", + "description": "The form.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59440,7 +66900,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignRequest" + "$ref": "#/components/schemas/Form" } } } @@ -59509,28 +66969,7 @@ } }, "404": { - "description": "No signature request with this id exists in the API key's environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The request isn't `sent`, or nobody can be reminded now (`reminder_not_due`).", + "description": "No form with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59599,7 +67038,7 @@ } }, "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "description": "Forms isn't available in this deployment yet (`forms_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59620,16 +67059,14 @@ } } } - } - }, - "/v1/sign/requests/{id}/signed_document": { - "get": { - "operationId": "getSignedDocument", + }, + "patch": { + "operationId": "updateForm", "tags": [ - "Signature requests" + "Forms" ], - "summary": "Download the signed document", - "description": "The completed request's sealed PDF: every document with the signatures and values filled in, then a certificate of completion (signers, how they were identified, what they agreed to, the documents' SHA-256 and the audit trail). coss.com's seal makes any later change visible in PDF viewers. Ready once `signed_document` is set, which the `sign_request.completed` webhook announces.", + "summary": "Update a form", + "description": "Only the fields given change, within each group (`spam`, `contacts`, …) too.", "security": [ { "bearerAuth": [] @@ -59639,13 +67076,13 @@ { "schema": { "type": "string", - "description": "The signature request's id.", + "description": "The form's id.", "examples": [ - "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" + "form_01kd2m8n3p4q5r6s7t8v9w0x1y" ] }, "required": true, - "description": "The signature request's id.", + "description": "The form's id.", "name": "id", "in": "path" }, @@ -59653,9 +67090,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFormRequest" + } + } + } + }, "responses": { "200": { - "description": "The sealed PDF.", + "description": "The form.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59677,16 +67124,15 @@ } }, "content": { - "application/pdf": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/Form" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59749,7 +67195,55 @@ } }, "404": { - "description": "No signature request with this id exists in the API key's environment (`not_found`).", + "description": "No form with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Other updates of this form kept racing this one (`form_conflict`). Retry after `Retry-After` seconds.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Retry-After": { + "description": "Seconds to wait.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59770,7 +67264,7 @@ } }, "422": { - "description": "The request isn't completed, or its document is still being sealed (`signed_document_not_ready`).", + "description": "The body failed validation (`validation_error`), or the form can't be saved: duplicate or misplaced fields (`invalid_fields`), an origin or redirect that isn't https (`invalid_origin`, `invalid_redirect`), a sender or recipient that isn't verified (`invalid_sender`, `invalid_recipient`), an unknown audience, topic or property (`invalid_contacts`), no captcha in the risk settings (`captcha_not_configured`), a hosted page without fields or asking for credentials (`hosted_needs_fields`, `hosted_content_refused`), a blocked file type or uploads turned off while a `file` field remains (`invalid_files`), or too many forms (`form_limit_reached`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59839,7 +67333,7 @@ } }, "503": { - "description": "Sign is temporarily unavailable (`sign_unavailable`).", + "description": "Forms isn't available in this deployment yet (`forms_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59860,16 +67354,14 @@ } } } - } - }, - "/v1/sign/requests/{id}/signers/{signer_id}/sessions": { - "post": { - "operationId": "createSignSession", + }, + "delete": { + "operationId": "deleteForm", "tags": [ - "Signature requests" + "Forms" ], - "summary": "Create a signing session", - "description": "A short-lived link to the signing page for one `embedded` signer of a sent request. Your app vouches for who the signer is (the audit trail records `customer_asserted`), so create it only for that signed-in user, when they open the page that shows it. Embed the `url` in an iframe from an origin in your Sign settings' `allowed_origins` (`mountSigning` from `coss/sign` does it), or redirect to it. Each call creates a new session.", + "summary": "Delete a form", + "description": "Deletes the form with every submission and uploaded file. Its endpoint answers 404 from then on.", "security": [ { "bearerAuth": [] @@ -59879,46 +67371,23 @@ { "schema": { "type": "string", - "description": "The signature request's id.", + "description": "The form's id.", "examples": [ - "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" + "form_01kd2m8n3p4q5r6s7t8v9w0x1y" ] }, "required": true, - "description": "The signature request's id.", + "description": "The form's id.", "name": "id", "in": "path" }, - { - "schema": { - "type": "string", - "description": "The signer's id (`signers[].id` of the request).", - "examples": [ - "ssgn_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The signer's id (`signers[].id` of the request).", - "name": "signer_id", - "in": "path" - }, { "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSignSessionRequest" - } - } - } - }, "responses": { - "201": { - "description": "The session, with its link.", + "200": { + "description": "The form was deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59942,7 +67411,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignSession" + "$ref": "#/components/schemas/DeletedForm" } } } @@ -60011,28 +67480,7 @@ } }, "404": { - "description": "No such signer on a signature request in the API key's environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The signer signs by email (`signer_not_embedded`), already signed or declined (`signer_finished`), or the request isn't `sent` or has expired (`request_not_signable`).", + "description": "No form with this id exists in the API key's environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60101,7 +67549,7 @@ } }, "503": { - "description": "Sign is temporarily unavailable (`sign_unavailable`).", + "description": "Forms isn't available in this deployment yet (`forms_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60124,22 +67572,73 @@ } } }, - "/v1/sign/settings": { + "/v1/forms/{id}/submissions": { "get": { - "operationId": "getSignSettings", + "operationId": "listFormSubmissions", "tags": [ - "Sign settings" + "Form submissions" ], - "summary": "Get the Sign settings", - "description": "The API key's environment's settings: which sites may embed the signing page, how it looks, and the consent text signers accept.", + "summary": "List a form's submissions", + "description": "Newest first. `status` narrows them: `accepted` (delivered), `held` (Risk asked for a review), `spam` and `pending_confirmation` (waiting for the submitter's email link).", "security": [ { "bearerAuth": [] } ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The form's id.", + "examples": [ + "form_01kd2m8n3p4q5r6s7t8v9w0x1y" + ] + }, + "required": true, + "description": "The form's id.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "Only submissions in these statuses, comma-separated: `held,spam`." + }, + "required": false, + "description": "Only submissions in these statuses, comma-separated: `held,spam`.", + "name": "status", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "1-100, default 20." + }, + "required": false, + "description": "1-100, default 20.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "maxLength": 64, + "description": "Cursor: the `id` of the last item of the previous page." + }, + "required": false, + "description": "Cursor: the `id` of the last item of the previous page.", + "name": "after", + "in": "query" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "responses": { "200": { - "description": "The settings.", + "description": "A page of submissions.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60163,7 +67662,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignSettings" + "$ref": "#/components/schemas/FormSubmissionList" } } } @@ -60231,6 +67730,48 @@ } } }, + "404": { + "description": "No form with this id exists in the API key's environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -60300,38 +67841,56 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, - "patch": { - "operationId": "updateSignSettings", + } + } + }, + "/v1/forms/{id}/submissions/{submission_id}": { + "get": { + "operationId": "getFormSubmission", "tags": [ - "Sign settings" + "Form submissions" ], - "summary": "Update the Sign settings", - "description": "Changes the fields given; the others keep their values.", + "summary": "Get a submission", + "description": "One submission of the form, with its data, files and Risk's assessment.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateSignSettingsRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The form's id.", + "examples": [ + "form_01kd2m8n3p4q5r6s7t8v9w0x1y" + ] + }, + "required": true, + "description": "The form's id.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "The submission's id.", + "examples": [ + "fsub_01kd2m9a0b1c2d3e4f5g6h7j8k" + ] + }, + "required": true, + "description": "The submission's id.", + "name": "submission_id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { "200": { - "description": "The settings.", + "description": "The submission.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60355,7 +67914,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignSettings" + "$ref": "#/components/schemas/FormSubmission" } } } @@ -60423,8 +67982,8 @@ } } }, - "422": { - "description": "The body failed validation (`validation_error`), or an origin isn't `https://host[:port]` (`invalid_origin`).", + "404": { + "description": "No such form, or no submission with this id in it (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60513,40 +68072,54 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - } - }, - "/v1/sign/templates": { - "post": { - "operationId": "createSignTemplate", + } + }, + "delete": { + "operationId": "deleteFormSubmission", "tags": [ - "Sign templates" + "Form submissions" ], - "summary": "Create a template", - "description": "Documents with fields for roles (`client`, `company`) instead of named signers, made into requests by naming a signer per role. Fields by position or text anchor (resolved now). Can start empty and be built in the embedded builder (create a builder session).", + "summary": "Delete a submission", + "description": "Deletes the submission and its files.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSignTemplateRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The form's id.", + "examples": [ + "form_01kd2m8n3p4q5r6s7t8v9w0x1y" + ] + }, + "required": true, + "description": "The form's id.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "The submission's id.", + "examples": [ + "fsub_01kd2m9a0b1c2d3e4f5g6h7j8k" + ] + }, + "required": true, + "description": "The submission's id.", + "name": "submission_id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { - "201": { - "description": "The template.", + "200": { + "description": "The submission was deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60570,13 +68143,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignTemplate" + "$ref": "#/components/schemas/DeletedFormSubmission" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60638,29 +68211,8 @@ } } }, - "413": { - "description": "The request body is larger than 40 MB (`payload_too_large`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The body failed validation (`validation_error`), a document isn't a PDF coss.com can read (`invalid_document`) or isn't in the environment (`not_found`), a field doesn't fit or names an unknown role or anchor (`invalid_fields`), or a role is listed twice (`invalid_signers`).", + "404": { + "description": "No such form, or no submission with this id in it (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60729,7 +68281,7 @@ } }, "503": { - "description": "Sign is temporarily unavailable (`sign_unavailable`).", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60749,20 +68301,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, - "get": { - "operationId": "listSignTemplates", + } + } + }, + "/v1/forms/{id}/submissions/{submission_id}/release": { + "post": { + "operationId": "releaseFormSubmission", "tags": [ - "Sign templates" + "Form submissions" ], - "summary": "List templates", - "description": "Newest first. Pass the last `id` as `after` while `has_more` is true.", + "summary": "Release a submission", + "description": "Not spam: a `held` or `spam` submission becomes `accepted` and is delivered (webhooks, notifications, contacts), and Risk records its assessment as `legitimate`. Releasing an accepted submission returns it as is.", "security": [ { "bearerAuth": [] @@ -60771,26 +68320,29 @@ "parameters": [ { "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 20." + "type": "string", + "description": "The form's id.", + "examples": [ + "form_01kd2m8n3p4q5r6s7t8v9w0x1y" + ] }, - "required": false, - "description": "1-100, default 20.", - "name": "limit", - "in": "query" + "required": true, + "description": "The form's id.", + "name": "id", + "in": "path" }, { "schema": { "type": "string", - "maxLength": 64, - "description": "Cursor: the `id` of the last item of the previous page." + "description": "The submission's id.", + "examples": [ + "fsub_01kd2m9a0b1c2d3e4f5g6h7j8k" + ] }, - "required": false, - "description": "Cursor: the `id` of the last item of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The submission's id.", + "name": "submission_id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -60798,7 +68350,7 @@ ], "responses": { "200": { - "description": "A page of templates.", + "description": "The released submission.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60822,7 +68374,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignTemplateList" + "$ref": "#/components/schemas/FormSubmission" } } } @@ -60890,8 +68442,29 @@ } } }, + "404": { + "description": "No such form, or no submission with this id in it (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "422": { - "description": "A query parameter failed validation (`validation_error`).", + "description": "Only held and spam submissions can be released (`submission_not_releasable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60983,14 +68556,14 @@ } } }, - "/v1/sign/templates/{id}": { + "/v1/forms/{id}/submissions/{submission_id}/files/{file_id}": { "get": { - "operationId": "getSignTemplate", + "operationId": "getFormSubmissionFile", "tags": [ - "Sign templates" + "Form submissions" ], - "summary": "Retrieve a template", - "description": "A template with its documents, roles and fields. `ready` says whether requests can be made from it.", + "summary": "Download a file", + "description": "A file uploaded with the submission, as `application/octet-stream`: whatever its type, treat it as untrusted (a submitter chose it).", "security": [ { "bearerAuth": [] @@ -61000,23 +68573,49 @@ { "schema": { "type": "string", - "description": "The template's id.", + "description": "The form's id.", "examples": [ - "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + "form_01kd2m8n3p4q5r6s7t8v9w0x1y" ] }, "required": true, - "description": "The template's id.", + "description": "The form's id.", "name": "id", "in": "path" }, + { + "schema": { + "type": "string", + "description": "The submission's id.", + "examples": [ + "fsub_01kd2m9a0b1c2d3e4f5g6h7j8k" + ] + }, + "required": true, + "description": "The submission's id.", + "name": "submission_id", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "The file's id (`files[].id` of the submission).", + "examples": [ + "ffile_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] + }, + "required": true, + "description": "The file's id (`files[].id` of the submission).", + "name": "file_id", + "in": "path" + }, { "$ref": "#/components/parameters/CossVersion" } ], "responses": { "200": { - "description": "The template.", + "description": "The file's bytes.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61038,9 +68637,10 @@ } }, "content": { - "application/json": { + "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/SignTemplate" + "type": "string", + "format": "binary" } } } @@ -61109,7 +68709,7 @@ } }, "404": { - "description": "No template with this id exists in the API key's environment (`not_found`).", + "description": "No such form, submission or file (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61178,7 +68778,7 @@ } }, "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "description": "Forms isn't available in this deployment yet (`forms_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61199,50 +68799,34 @@ } } } - }, - "patch": { - "operationId": "updateSignTemplate", + } + }, + "/v1/scheduling/hosts": { + "post": { + "operationId": "createSchedulingHost", "tags": [ - "Sign templates" + "Scheduling hosts" ], - "summary": "Update a template", - "description": "Changes the fields given. `documents`, `roles` and `fields` each replace the whole list. Requests already made from it don't change.", + "summary": "Create a host", + "description": "A person who can be booked: one of your users, such as a doctor or a tutor.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The template's id.", - "examples": [ - "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The template's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateSignTemplateRequest" + "$ref": "#/components/schemas/CreateHostRequest" } } } }, "responses": { - "200": { - "description": "The template.", + "201": { + "description": "The host.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61266,7 +68850,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignTemplate" + "$ref": "#/components/schemas/SchedulingHost" } } } @@ -61313,8 +68897,8 @@ } } }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "402": { + "description": "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past the plan's limit (`plan_limit_reached`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61334,8 +68918,8 @@ } } }, - "404": { - "description": "No template with this id exists in the API key's environment (`not_found`).", + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61355,8 +68939,8 @@ } } }, - "413": { - "description": "The request body is larger than 40 MB (`payload_too_large`).", + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61377,7 +68961,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`), a document isn't a PDF coss.com can read (`invalid_document`) or isn't in the environment (`not_found`), a field doesn't fit or names an unknown role or anchor (`invalid_fields`), or a role is listed twice (`invalid_signers`).", + "description": "The request failed validation (`validation_error`, see `issues`), `time_zone` isn't an IANA name (`invalid_time_zone`), or another host has the `uid` (`uid_taken`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61446,7 +69030,7 @@ } }, "503": { - "description": "Sign is temporarily unavailable (`sign_unavailable`).", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61466,33 +69050,46 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] }, - "delete": { - "operationId": "deleteSignTemplate", + "get": { + "operationId": "listSchedulingHosts", "tags": [ - "Sign templates" + "Scheduling hosts" ], - "summary": "Delete a template", - "description": "Requests made from it, and its documents, stay.", + "summary": "List hosts", + "description": "The environment's hosts, newest first.", "security": [ { "bearerAuth": [] } ], "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "required": false, + "name": "limit", + "in": "query" + }, { "schema": { "type": "string", - "description": "The template's id.", - "examples": [ - "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "description": "The last id of the previous page." }, - "required": true, - "description": "The template's id.", - "name": "id", - "in": "path" + "required": false, + "description": "The last id of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -61500,7 +69097,7 @@ ], "responses": { "200": { - "description": "The deleted template.", + "description": "A page of hosts.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61524,7 +69121,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSignTemplate" + "$ref": "#/components/schemas/SchedulingHostList" } } } @@ -61592,8 +69189,8 @@ } } }, - "404": { - "description": "No template with this id exists in the API key's environment (`not_found`).", + "422": { + "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61685,14 +69282,14 @@ } } }, - "/v1/sign/templates/{id}/requests": { - "post": { - "operationId": "createSignRequestFromTemplate", + "/v1/scheduling/hosts/{host}": { + "get": { + "operationId": "getSchedulingHost", "tags": [ - "Sign templates" + "Scheduling hosts" ], - "summary": "Create a request from a template", - "description": "A signature request with the template's documents and fields: name one signer for each role, and give its merge fields' values. Like creating a request, it is sent at once unless `send` is `false`, and an `Idempotency-Key` makes retries safe.", + "summary": "Retrieve a host", + "description": "A host by its id or your uid.", "security": [ { "bearerAuth": [] @@ -61702,48 +69299,23 @@ { "schema": { "type": "string", - "description": "The template's id.", + "description": "The host's id or uid.", "examples": [ - "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The template's id.", - "name": "id", + "description": "The host's id or uid.", + "name": "host", "in": "path" }, - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Makes retries safe: a repeated request with the same key and body returns the original request (in its current state) instead of creating another, for 24 hours. 1-256 characters.", - "examples": [ - "welcome-user-123" - ] - }, - "required": false, - "description": "Makes retries safe: a repeated request with the same key and body returns the original request (in its current state) instead of creating another, for 24 hours. 1-256 characters.", - "name": "Idempotency-Key", - "in": "header" - }, { "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSignRequestFromTemplateRequest" - } - } - } - }, "responses": { - "201": { - "description": "The signature request.", + "200": { + "description": "The host.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61754,15 +69326,6 @@ ] } }, - "Idempotent-Replayed": { - "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", - "schema": { - "type": "string", - "enum": [ - "true" - ] - } - }, "Coss-Version": { "description": "The API version this response is rendered in.", "schema": { @@ -61776,13 +69339,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignRequest" + "$ref": "#/components/schemas/SchedulingHost" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`) or the `Idempotency-Key` header is invalid (`invalid_idempotency_key`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61845,49 +69408,7 @@ } }, "404": { - "description": "No template with this id exists in the API key's environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "409": { - "description": "Requests with this `Idempotency-Key` are racing (`request_in_progress`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The template isn't ready (`template_incomplete`), a role has no signer or two (`invalid_signers`), a merge field doesn't exist (`invalid_fields`), `expires_at` is out of range (`invalid_expiry`), or the `Idempotency-Key` was used with a different body (`idempotency_key_reused`).", + "description": "Nothing with this id or uid exists in the environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61956,7 +69477,7 @@ } }, "503": { - "description": "Sign is temporarily unavailable (`sign_unavailable`).", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61977,16 +69498,14 @@ } } } - } - }, - "/v1/sign/templates/{id}/builder_sessions": { - "post": { - "operationId": "createSignBuilderSession", + }, + "patch": { + "operationId": "updateSchedulingHost", "tags": [ - "Sign templates" + "Scheduling hosts" ], - "summary": "Create a builder session", - "description": "A short-lived link to the embedded builder for this template: upload documents, add roles, and drag fields onto pages. Embed it with `mountBuilder` from `coss/sign` (from an origin in your Sign settings' `allowed_origins`) for whoever edits the template.", + "summary": "Update a host", + "description": "Changes what's given. A `disabled` host keeps its bookings but gets no new ones.", "security": [ { "bearerAuth": [] @@ -61996,14 +69515,14 @@ { "schema": { "type": "string", - "description": "The template's id.", + "description": "The host's id or uid.", "examples": [ - "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The template's id.", - "name": "id", + "description": "The host's id or uid.", + "name": "host", "in": "path" }, { @@ -62011,18 +69530,18 @@ } ], "requestBody": { - "required": false, + "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateSignBuilderSessionRequest" + "$ref": "#/components/schemas/UpdateHostRequest" } } } }, "responses": { - "201": { - "description": "The builder session, with its link.", + "200": { + "description": "The host.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62046,13 +69565,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignBuilderSession" + "$ref": "#/components/schemas/SchedulingHost" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62115,7 +69634,49 @@ } }, "404": { - "description": "No template with this id exists in the API key's environment (`not_found`).", + "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The request failed validation (`validation_error`, see `issues`), `time_zone` isn't an IANA name (`invalid_time_zone`), or another host has the `uid` (`uid_taken`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62184,7 +69745,7 @@ } }, "503": { - "description": "Sign is temporarily unavailable (`sign_unavailable`).", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62205,16 +69766,14 @@ } } } - } - }, - "/v1/verifications": { - "post": { - "operationId": "startVerification", + }, + "delete": { + "operationId": "deleteSchedulingHost", "tags": [ - "Verifications" + "Scheduling hosts" ], - "summary": "Start a verification", - "description": "Sends a one-time code to a phone number (SMS) or an email address and returns the verification, `pending` until you check the code the end user typed (`POST /v1/verifications/check`). Starting again for the same destination while it is pending sends a new code (the earlier one stops working), up to 5 times. Before a new verification costs anything, the fraud guard checks it (your Risk lists, premium-rate numbers, Tor, and velocity per IP, user, number range and country, with how often their codes are entered): a blocked one is stored with its `reasons` and nothing is sent. Pass the end user's `ip` (and your `user_id`) so it can tell them apart. SMS codes come from coss.com's shared numbers unless the environment's Verify settings name one of your own; email codes from your sender, else coss.com's. Sandbox (`coss_test_…`) keys deliver nothing and the code is always zeros.", + "summary": "Delete a host", + "description": "Removes the host and its schedules and calendars. Refused while bookings are still to come (`host_has_live_bookings`): cancel them, or set the host's status to `disabled`.", "security": [ { "bearerAuth": [] @@ -62224,35 +69783,23 @@ { "schema": { "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Makes retries safe: a repeated request with the same key and body returns the original verification instead of sending another code, for 24 hours. 1-256 characters.", + "description": "The host's id or uid.", "examples": [ - "welcome-user-123" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, - "required": false, - "description": "Makes retries safe: a repeated request with the same key and body returns the original verification instead of sending another code, for 24 hours. 1-256 characters.", - "name": "Idempotency-Key", - "in": "header" + "required": true, + "description": "The host's id or uid.", + "name": "host", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StartVerificationRequest" - } - } - } - }, "responses": { - "201": { - "description": "The verification, `pending`, with the send that just went out.", + "200": { + "description": "Deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62263,15 +69810,6 @@ ] } }, - "Idempotent-Replayed": { - "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", - "schema": { - "type": "string", - "enum": [ - "true" - ] - } - }, "Coss-Version": { "description": "The API version this response is rendered in.", "schema": { @@ -62285,13 +69823,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Verification" + "$ref": "#/components/schemas/DeletedSchedulingHost" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`) or the `Idempotency-Key` header is invalid (`invalid_idempotency_key`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62332,27 +69870,6 @@ } } }, - "402": { - "description": "Live sending is paused for an overdue invoice or the spend limit (`billing_hold`) or a used-up budget (`budget_exceeded`), or the plan doesn't include Verify or the destination country isn't priced (`plan_feature_unavailable`) (`billing_error`). Never retried: it needs a plan change or a payment.", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -62374,8 +69891,8 @@ } } }, - "409": { - "description": "Requests with this `Idempotency-Key` are racing (`request_in_progress`).", + "404": { + "description": "Nothing with this id or uid exists in the environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62385,12 +69902,6 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Retry-After": { - "description": "Seconds to wait before retrying.", - "schema": { - "type": "integer" - } } }, "content": { @@ -62402,7 +69913,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`, see `issues`); `to` isn't a valid number or address (`invalid_destination`) or its country isn't allowed (`country_not_allowed`); the fraud guard blocked it (`verification_blocked`: the verification is stored with its `reasons`); the code was already sent 5 times (`too_many_sends`); the recipient opted out or is suppressed (`recipient_suppressed`); the code couldn't be sent (`send_failed`, `sender_not_ready`…); or the `Idempotency-Key` was already used with a different body (`idempotency_key_reused`).", + "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62423,7 +69934,7 @@ } }, "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many codes to this destination or from coss.com's shared numbers today (`quota_exceeded`). Retry after `Retry-After` seconds.", + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62471,7 +69982,7 @@ } }, "503": { - "description": "coss.com's shared numbers aren't available for this destination (`shared_sender_unavailable`): name your own number in Verify's settings.", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62492,14 +70003,16 @@ } } } - }, - "get": { - "operationId": "listVerifications", + } + }, + "/v1/scheduling/bookings/{id}/reassign": { + "post": { + "operationId": "reassignSchedulingBooking", "tags": [ - "Verifications" + "Scheduling bookings" ], - "summary": "List verifications", - "description": "Verifications of the API key's environment, newest first. Pass the last `id` as `after` to get the next page while `has_more` is true.", + "summary": "Reassign a booking", + "description": "Gives a booking to another host, keeping its id and time. Works for every kind: a group session moves as a whole, from any of its seats, and one that's already started changes for the record, with nobody told. Whoever leaves and joins is told, and the attendee unless you say not to.", "security": [ { "bearerAuth": [] @@ -62509,63 +70022,33 @@ { "schema": { "type": "string", - "minLength": 1, - "maxLength": 320, - "description": "Only verifications to this destination." - }, - "required": false, - "description": "Only verifications to this destination.", - "name": "to", - "in": "query" - }, - { - "schema": { - "type": "string", - "enum": [ - "pending", - "approved", - "expired", - "canceled", - "failed", - "blocked" - ], - "description": "Only verifications in this status." - }, - "required": false, - "description": "Only verifications in this status.", - "name": "status", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 20." - }, - "required": false, - "description": "1-100, default 20.", - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "maxLength": 64, - "description": "Cursor: the `id` of the last item of the previous page." + "description": "The booking's id.", + "examples": [ + "book_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] }, - "required": false, - "description": "Cursor: the `id` of the last item of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The booking's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReassignSchedulingBookingRequest" + } + } + } + }, "responses": { "200": { - "description": "A page of verifications.", + "description": "The booking.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62589,13 +70072,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerificationList" + "$ref": "#/components/schemas/SchedulingBooking" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62657,8 +70140,50 @@ } } }, + "404": { + "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "description": "The request doesn't make sense (`invalid_reassignment`), that host can't take it (`reassign_target_unavailable`), nobody can (`no_reassign_candidate`), or the booking can't change (`booking_not_changeable`, `hold_expired`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62750,14 +70275,14 @@ } } }, - "/v1/verifications/check": { - "post": { - "operationId": "checkVerification", + "/v1/scheduling/bookings/{id}/reassign_candidates": { + "get": { + "operationId": "listSchedulingReassignCandidates", "tags": [ - "Verifications" + "Scheduling bookings" ], - "summary": "Check a code", - "description": "Checks the code the end user typed, by the verification's id or by the destination (its latest verification). `valid: true` approves it (`verification.approved`, billed once); a wrong code spends an attempt, and the last one fails it. Checking an approved verification again with its code stays valid until it expires, without being billed again.", + "summary": "List who a booking could go to", + "description": "The environment's other active hosts: members of the event type first, in the order a pool would pick them, each saying whether they're free and why not. Things can change before you ask: reassigning decides again.", "security": [ { "bearerAuth": [] @@ -62767,35 +70292,33 @@ { "schema": { "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Makes retries safe: a repeated check with the same key returns the same answer without spending another attempt. 1-256 characters.", + "description": "The booking's id.", "examples": [ - "welcome-user-123" + "book_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, + "required": true, + "description": "The booking's id.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "The host who'd leave, when it could be several." + }, "required": false, - "description": "Makes retries safe: a repeated check with the same key returns the same answer without spending another attempt. 1-256 characters.", - "name": "Idempotency-Key", - "in": "header" + "description": "The host who'd leave, when it could be several.", + "name": "from_host", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CheckVerificationRequest" - } - } - } - }, "responses": { "200": { - "description": "Whether the code is right, and the verification after the check.", + "description": "The candidates.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62819,13 +70342,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerificationCheck" + "$ref": "#/components/schemas/SchedulingReassignCandidateList" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`) or the `Idempotency-Key` header is invalid (`invalid_idempotency_key`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62888,7 +70411,7 @@ } }, "404": { - "description": "No such verification in the API key's environment (`not_found`).", + "description": "Nothing with this id or uid exists in the environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62909,7 +70432,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`, see `issues`).", + "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62930,7 +70453,7 @@ } }, "429": { - "description": "Too many requests with this API key (`rate_limited`), or too many checks for this destination (`quota_exceeded`).", + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63001,40 +70524,32 @@ } } }, - "/v1/verifications/{id}": { - "get": { - "operationId": "getVerification", + "/v1/scheduling/reassignments/preview": { + "post": { + "operationId": "previewSchedulingReassignment", "tags": [ - "Verifications" + "Scheduling reassignments" ], - "summary": "Retrieve a verification", - "description": "A verification's status and destination. Its code is never returned.", + "summary": "Preview a bulk reassignment", + "description": "Who each booking would go to, and why not when nobody can take it, without moving anything. It works down them in order, so two can't both be promised the same free hour. Change any of it and start the run with `assignments`. Covers the first 500. Things can change before you start it: the run decides again.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The verification's id.", - "examples": [ - "vrf_01k8e2m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The verification's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PreviewSchedulingReassignmentRequest" + } + } } - ], + }, "responses": { "200": { - "description": "The verification.", + "description": "What would happen.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63058,13 +70573,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Verification" + "$ref": "#/components/schemas/SchedulingReassignmentPreview" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63127,7 +70642,49 @@ } }, "404": { - "description": "No such verification in the API key's environment (`not_found`).", + "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The selection or strategy doesn't make sense (`invalid_reassignment`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63216,17 +70773,22 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/verifications/{id}/cancel": { + "/v1/scheduling/reassignments": { "post": { - "operationId": "cancelVerification", + "operationId": "createSchedulingReassignment", "tags": [ - "Verifications" + "Scheduling reassignments" ], - "summary": "Cancel a verification", - "description": "Cancels a pending verification: its code stops working. Canceling it again returns it as is.", + "summary": "Start a bulk reassignment", + "description": "Moves up to 5,000 bookings in the background, each on its own, so one that can't move never holds up the rest. Nothing's ever cancelled: a booking nobody can take stays where it is, and its item says so. It answers at once: follow the run by its id, or wait for `scheduling.reassignment.completed`.", "security": [ { "bearerAuth": [] @@ -63236,23 +70798,35 @@ { "schema": { "type": "string", - "description": "The verification's id.", + "minLength": 1, + "maxLength": 256, + "description": "Makes retries safe: a repeated request with the same key and body returns the original run instead of starting another, for 24 hours. 1-256 characters.", "examples": [ - "vrf_01k8e2m4q2f3e9v6t5r1n0p8yz" + "welcome-user-123" ] }, - "required": true, - "description": "The verification's id.", - "name": "id", - "in": "path" + "required": false, + "description": "Makes retries safe: a repeated request with the same key and body returns the original run instead of starting another, for 24 hours. 1-256 characters.", + "name": "Idempotency-Key", + "in": "header" }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSchedulingReassignmentRequest" + } + } + } + }, "responses": { - "200": { - "description": "The canceled verification.", + "202": { + "description": "The run, waiting to start.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63263,6 +70837,15 @@ ] } }, + "Idempotent-Replayed": { + "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", + "schema": { + "type": "string", + "enum": [ + "true" + ] + } + }, "Coss-Version": { "description": "The API version this response is rendered in.", "schema": { @@ -63276,13 +70859,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Verification" + "$ref": "#/components/schemas/SchedulingReassignment" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63345,7 +70928,49 @@ } }, "404": { - "description": "No such verification in the API key's environment (`not_found`).", + "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "A request with this key is still running (`idempotency_in_progress`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63366,7 +70991,7 @@ } }, "422": { - "description": "The verification isn't pending (`verification_not_cancelable`): approved, failed, blocked or expired.", + "description": "The selection or strategy doesn't make sense, or covers too many bookings (`invalid_reassignment`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63456,24 +71081,48 @@ } } } - } - }, - "/v1/verify/settings": { + }, "get": { - "operationId": "getVerifySettings", + "operationId": "listSchedulingReassignments", "tags": [ - "Verify settings" + "Scheduling reassignments" ], - "summary": "Get the Verify settings", - "description": "The API key's environment's settings, with defaults filled in: the name codes are sent for, code length, lifetime and attempts, the senders, and the countries SMS codes may go to.", + "summary": "List bulk reassignments", + "description": "Newest first, with how each is going.", "security": [ { "bearerAuth": [] } ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "The last id of the previous page." + }, + "required": false, + "description": "The last id of the previous page.", + "name": "after", + "in": "query" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "responses": { "200": { - "description": "The settings.", + "description": "The runs.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63497,7 +71146,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerifySettings" + "$ref": "#/components/schemas/SchedulingReassignmentList" } } } @@ -63565,6 +71214,27 @@ } } }, + "422": { + "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -63634,38 +71304,40 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, - "patch": { - "operationId": "updateVerifySettings", + } + } + }, + "/v1/scheduling/reassignments/{id}": { + "get": { + "operationId": "getSchedulingReassignment", "tags": [ - "Verify settings" + "Scheduling reassignments" ], - "summary": "Update the Verify settings", - "description": "Changes the fields given; the others keep their values. `sms_from` must be one of the organization's numbers, and a live key's `email_from` must be on one of its verified domains.", + "summary": "Retrieve a bulk reassignment", + "description": "How it's going.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateVerifySettingsRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The reassignment's id." + }, + "required": true, + "description": "The reassignment's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { "200": { - "description": "The settings.", + "description": "The run.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63689,7 +71361,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerifySettings" + "$ref": "#/components/schemas/SchedulingReassignment" } } } @@ -63757,8 +71429,8 @@ } } }, - "422": { - "description": "The body failed validation (`validation_error`), or a sender isn't the organization's (`invalid_settings`).", + "404": { + "description": "Nothing with this id or uid exists in the environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63847,40 +71519,61 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/forms": { - "post": { - "operationId": "createForm", + "/v1/scheduling/reassignments/{id}/items": { + "get": { + "operationId": "listSchedulingReassignmentItems", "tags": [ - "Forms" + "Scheduling reassignments" ], - "summary": "Create a form", - "description": "A hosted form endpoint: point an HTML `` (or `coss/forms`) at its `endpoint`. Every submission is screened by Risk (the honeypot, captcha, email, IP, device, velocity and content) and accepted ones go to webhooks (`form.submission.received`), notification emails and contacts. Without `fields`, any field is accepted.", + "summary": "List a bulk reassignment's bookings", + "description": "Each booking it covers and what happened to it. A group session has one, for one of its seats. Empty until the worker's started on it.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateFormRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The reassignment's id." + }, + "required": true, + "description": "The reassignment's id.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "The last id of the previous page." + }, + "required": false, + "description": "The last id of the previous page.", + "name": "after", + "in": "query" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { - "201": { - "description": "The form.", + "200": { + "description": "The items.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63904,13 +71597,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Form" + "$ref": "#/components/schemas/SchedulingReassignmentItemList" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63972,8 +71665,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "404": { + "description": "Nothing with this id or uid exists in the environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63994,7 +71687,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`), or the form can't be saved: duplicate or misplaced fields (`invalid_fields`), an origin or redirect that isn't https (`invalid_origin`, `invalid_redirect`), a sender or recipient that isn't verified (`invalid_sender`, `invalid_recipient`), an unknown audience, topic or property (`invalid_contacts`), no captcha in the risk settings (`captcha_not_configured`), a hosted page without fields or asking for credentials (`hosted_needs_fields`, `hosted_content_refused`), a blocked file type or uploads turned off while a `file` field remains (`invalid_files`), or too many forms (`form_limit_reached`).", + "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64063,7 +71756,7 @@ } }, "503": { - "description": "Forms isn't available in this deployment yet (`forms_unavailable`).", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64083,48 +71776,32 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, - "get": { - "operationId": "listForms", + } + } + }, + "/v1/scheduling/reassignments/{id}/cancel": { + "post": { + "operationId": "cancelSchedulingReassignment", "tags": [ - "Forms" + "Scheduling reassignments" ], - "summary": "List forms", - "description": "The environment's forms, newest first.", + "summary": "Cancel a bulk reassignment", + "description": "Stops what hasn't happened yet. What's moved stays moved: undo it for that. Cancelling one that's finished changes nothing.", "security": [ { "bearerAuth": [] } ], "parameters": [ - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 20." - }, - "required": false, - "description": "1-100, default 20.", - "name": "limit", - "in": "query" - }, { "schema": { "type": "string", - "maxLength": 64, - "description": "Cursor: the `id` of the last item of the previous page." + "description": "The reassignment's id." }, - "required": false, - "description": "Cursor: the `id` of the last item of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The reassignment's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -64132,7 +71809,7 @@ ], "responses": { "200": { - "description": "A page of forms.", + "description": "The run.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64156,7 +71833,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FormList" + "$ref": "#/components/schemas/SchedulingReassignment" } } } @@ -64224,8 +71901,8 @@ } } }, - "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "404": { + "description": "Nothing with this id or uid exists in the environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64294,7 +71971,7 @@ } }, "503": { - "description": "Forms isn't available in this deployment yet (`forms_unavailable`).", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64317,14 +71994,14 @@ } } }, - "/v1/forms/{id}": { - "get": { - "operationId": "getForm", + "/v1/scheduling/reassignments/{id}/undo": { + "post": { + "operationId": "undoSchedulingReassignment", "tags": [ - "Forms" + "Scheduling reassignments" ], - "summary": "Get a form", - "description": "A form's settings, with the `endpoint` it posts to and its `hosted_url`.", + "summary": "Undo a bulk reassignment", + "description": "Gives back what a finished run moved, as a new run. A booking that's moved on since stays where it went, and a former host who's no longer free keeps it from going back, unless you pass `overrides`.", "security": [ { "bearerAuth": [] @@ -64334,13 +72011,10 @@ { "schema": { "type": "string", - "description": "The form's id.", - "examples": [ - "form_01kd2m8n3p4q5r6s7t8v9w0x1y" - ] + "description": "The reassignment's id." }, "required": true, - "description": "The form's id.", + "description": "The reassignment's id.", "name": "id", "in": "path" }, @@ -64348,9 +72022,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UndoSchedulingReassignmentRequest" + } + } + } + }, "responses": { - "200": { - "description": "The form.", + "202": { + "description": "The new run.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64374,13 +72058,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Form" + "$ref": "#/components/schemas/SchedulingReassignment" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64443,7 +72127,49 @@ } }, "404": { - "description": "No form with this id exists in the API key's environment (`not_found`).", + "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "It's still running (`reassignment_not_finished`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64512,7 +72238,7 @@ } }, "503": { - "description": "Forms isn't available in this deployment yet (`forms_unavailable`).", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64533,50 +72259,34 @@ } } } - }, - "patch": { - "operationId": "updateForm", + } + }, + "/v1/scheduling/holiday_calendars": { + "post": { + "operationId": "createSchedulingHolidayCalendar", "tags": [ - "Forms" + "Scheduling holiday calendars" ], - "summary": "Update a form", - "description": "Only the fields given change, within each group (`spam`, `contacts`, …) too.", + "summary": "Create a holiday calendar", + "description": "Applies a country's public holidays, or your own feed, to some hosts as ordinary out of office entries, kept up to date by coss.com. A host who removes one keeps it removed.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The form's id.", - "examples": [ - "form_01kd2m8n3p4q5r6s7t8v9w0x1y" - ] - }, - "required": true, - "description": "The form's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateFormRequest" + "$ref": "#/components/schemas/CreateSchedulingHolidayCalendarRequest" } } } }, "responses": { - "200": { - "description": "The form.", + "201": { + "description": "The calendar.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64600,7 +72310,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Form" + "$ref": "#/components/schemas/SchedulingHolidayCalendar" } } } @@ -64669,7 +72379,7 @@ } }, "404": { - "description": "No form with this id exists in the API key's environment (`not_found`).", + "description": "Nothing with this id or uid exists in the environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64689,8 +72399,50 @@ } } }, - "409": { - "description": "Other updates of this form kept racing this one (`form_conflict`). Retry after `Retry-After` seconds.", + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "That isn't something to read (`invalid_holiday_calendar`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64702,7 +72454,7 @@ } }, "Retry-After": { - "description": "Seconds to wait.", + "description": "Seconds to wait before retrying.", "schema": { "type": "integer" } @@ -64716,8 +72468,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64737,8 +72489,121 @@ } } }, - "422": { - "description": "The body failed validation (`validation_error`), or the form can't be saved: duplicate or misplaced fields (`invalid_fields`), an origin or redirect that isn't https (`invalid_origin`, `invalid_redirect`), a sender or recipient that isn't verified (`invalid_sender`, `invalid_recipient`), an unknown audience, topic or property (`invalid_contacts`), no captcha in the risk settings (`captcha_not_configured`), a hosted page without fields or asking for credentials (`hosted_needs_fields`, `hosted_content_refused`), a blocked file type or uploads turned off while a `file` field remains (`invalid_files`), or too many forms (`form_limit_reached`).", + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, + "get": { + "operationId": "listSchedulingHolidayCalendars", + "tags": [ + "Scheduling holiday calendars" + ], + "summary": "List holiday calendars", + "description": "By name. A feed's URL is never shown again.", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "The calendars.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchedulingHolidayCalendarList" + } + } + } + }, + "400": { + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64807,7 +72672,7 @@ } }, "503": { - "description": "Forms isn't available in this deployment yet (`forms_unavailable`).", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64827,15 +72692,22 @@ } } } - } - }, - "delete": { - "operationId": "deleteForm", + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + } + }, + "/v1/scheduling/holiday_calendars/{id}": { + "get": { + "operationId": "getSchedulingHolidayCalendar", "tags": [ - "Forms" + "Scheduling holiday calendars" ], - "summary": "Delete a form", - "description": "Deletes the form with every submission and uploaded file. Its endpoint answers 404 from then on.", + "summary": "Retrieve a holiday calendar", + "description": "With when it was last read, and why not if it couldn't be.", "security": [ { "bearerAuth": [] @@ -64845,13 +72717,10 @@ { "schema": { "type": "string", - "description": "The form's id.", - "examples": [ - "form_01kd2m8n3p4q5r6s7t8v9w0x1y" - ] + "description": "The holiday calendar's id." }, "required": true, - "description": "The form's id.", + "description": "The holiday calendar's id.", "name": "id", "in": "path" }, @@ -64861,7 +72730,7 @@ ], "responses": { "200": { - "description": "The form was deleted.", + "description": "The calendar.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64885,7 +72754,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedForm" + "$ref": "#/components/schemas/SchedulingHolidayCalendar" } } } @@ -64954,7 +72823,7 @@ } }, "404": { - "description": "No form with this id exists in the API key's environment (`not_found`).", + "description": "Nothing with this id or uid exists in the environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65023,7 +72892,7 @@ } }, "503": { - "description": "Forms isn't available in this deployment yet (`forms_unavailable`).", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65044,16 +72913,14 @@ } } } - } - }, - "/v1/forms/{id}/submissions": { - "get": { - "operationId": "listFormSubmissions", + }, + "patch": { + "operationId": "updateSchedulingHolidayCalendar", "tags": [ - "Form submissions" + "Scheduling holiday calendars" ], - "summary": "List a form's submissions", - "description": "Newest first. `status` narrows them: `accepted` (delivered), `held` (Risk asked for a review), `spam` and `pending_confirmation` (waiting for the submitter's email link).", + "summary": "Update a holiday calendar", + "description": "Who it applies to, who covers and what happens to bookings. Takes effect shortly.", "security": [ { "bearerAuth": [] @@ -65063,56 +72930,30 @@ { "schema": { "type": "string", - "description": "The form's id.", - "examples": [ - "form_01kd2m8n3p4q5r6s7t8v9w0x1y" - ] + "description": "The holiday calendar's id." }, "required": true, - "description": "The form's id.", + "description": "The holiday calendar's id.", "name": "id", "in": "path" }, - { - "schema": { - "type": "string", - "description": "Only submissions in these statuses, comma-separated: `held,spam`." - }, - "required": false, - "description": "Only submissions in these statuses, comma-separated: `held,spam`.", - "name": "status", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 20." - }, - "required": false, - "description": "1-100, default 20.", - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "maxLength": 64, - "description": "Cursor: the `id` of the last item of the previous page." - }, - "required": false, - "description": "Cursor: the `id` of the last item of the previous page.", - "name": "after", - "in": "query" - }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSchedulingHolidayCalendarRequest" + } + } + } + }, "responses": { "200": { - "description": "A page of submissions.", + "description": "The calendar.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65136,13 +72977,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FormSubmissionList" + "$ref": "#/components/schemas/SchedulingHolidayCalendar" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65205,7 +73046,28 @@ } }, "404": { - "description": "No form with this id exists in the API key's environment (`not_found`).", + "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65226,7 +73088,7 @@ } }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65316,16 +73178,14 @@ } } } - } - }, - "/v1/forms/{id}/submissions/{submission_id}": { - "get": { - "operationId": "getFormSubmission", + }, + "delete": { + "operationId": "deleteSchedulingHolidayCalendar", "tags": [ - "Form submissions" + "Scheduling holiday calendars" ], - "summary": "Get a submission", - "description": "One submission of the form, with its data, files and Risk's assessment.", + "summary": "Delete a holiday calendar", + "description": "Hosts can be booked on its days again, shortly: past entries stay.", "security": [ { "bearerAuth": [] @@ -65335,36 +73195,20 @@ { "schema": { "type": "string", - "description": "The form's id.", - "examples": [ - "form_01kd2m8n3p4q5r6s7t8v9w0x1y" - ] + "description": "The holiday calendar's id." }, "required": true, - "description": "The form's id.", + "description": "The holiday calendar's id.", "name": "id", "in": "path" }, - { - "schema": { - "type": "string", - "description": "The submission's id.", - "examples": [ - "fsub_01kd2m9a0b1c2d3e4f5g6h7j8k" - ] - }, - "required": true, - "description": "The submission's id.", - "name": "submission_id", - "in": "path" - }, { "$ref": "#/components/parameters/CossVersion" } ], "responses": { "200": { - "description": "The submission.", + "description": "It's going.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65388,7 +73232,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FormSubmission" + "$ref": "#/components/schemas/DeletedSchedulingHolidayCalendar" } } } @@ -65457,7 +73301,7 @@ } }, "404": { - "description": "No such form, or no submission with this id in it (`not_found`).", + "description": "Nothing with this id or uid exists in the environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65547,14 +73391,16 @@ } } } - }, - "delete": { - "operationId": "deleteFormSubmission", + } + }, + "/v1/scheduling/holiday_calendars/{id}/holidays": { + "get": { + "operationId": "listSchedulingHolidays", "tags": [ - "Form submissions" + "Scheduling holiday calendars" ], - "summary": "Delete a submission", - "description": "Deletes the submission and its files.", + "summary": "List a calendar's holidays", + "description": "What's coming, with names, as last read. Empty until its first read.", "security": [ { "bearerAuth": [] @@ -65564,36 +73410,20 @@ { "schema": { "type": "string", - "description": "The form's id.", - "examples": [ - "form_01kd2m8n3p4q5r6s7t8v9w0x1y" - ] + "description": "The holiday calendar's id." }, "required": true, - "description": "The form's id.", + "description": "The holiday calendar's id.", "name": "id", "in": "path" }, - { - "schema": { - "type": "string", - "description": "The submission's id.", - "examples": [ - "fsub_01kd2m9a0b1c2d3e4f5g6h7j8k" - ] - }, - "required": true, - "description": "The submission's id.", - "name": "submission_id", - "in": "path" - }, { "$ref": "#/components/parameters/CossVersion" } ], "responses": { "200": { - "description": "The submission was deleted.", + "description": "The holidays.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65617,7 +73447,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedFormSubmission" + "$ref": "#/components/schemas/SchedulingHolidayList" } } } @@ -65686,7 +73516,7 @@ } }, "404": { - "description": "No such form, or no submission with this id in it (`not_found`).", + "description": "Nothing with this id or uid exists in the environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65778,53 +73608,32 @@ } } }, - "/v1/forms/{id}/submissions/{submission_id}/release": { + "/v1/scheduling/out_of_office": { "post": { - "operationId": "releaseFormSubmission", + "operationId": "createSchedulingOutOfOffice", "tags": [ - "Form submissions" + "Scheduling out of office" ], - "summary": "Release a submission", - "description": "Not spam: a `held` or `spam` submission becomes `accepted` and is delivered (webhooks, notifications, contacts), and Risk records its assessment as `legitimate`. Releasing an accepted submission returns it as is.", + "summary": "Create out of office entries", + "description": "Marks one host, a list of hosts or everyone matching some attributes as away, for part of a day or for whole dates. They aren't offered during it. Existing bookings stay as they are: list them with the entry's conflicts.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The form's id.", - "examples": [ - "form_01kd2m8n3p4q5r6s7t8v9w0x1y" - ] - }, - "required": true, - "description": "The form's id.", - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "string", - "description": "The submission's id.", - "examples": [ - "fsub_01kd2m9a0b1c2d3e4f5g6h7j8k" - ] - }, - "required": true, - "description": "The submission's id.", - "name": "submission_id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSchedulingOutOfOfficeRequest" + } + } } - ], + }, "responses": { - "200": { - "description": "The released submission.", + "201": { + "description": "An entry per host.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65848,13 +73657,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FormSubmission" + "$ref": "#/components/schemas/SchedulingOutOfOfficeList" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65917,7 +73726,28 @@ } }, "404": { - "description": "No such form, or no submission with this id in it (`not_found`).", + "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65938,7 +73768,7 @@ } }, "422": { - "description": "Only held and spam submissions can be released (`submission_not_releasable`).", + "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66027,61 +73857,92 @@ } } } - } - } - }, - "/v1/forms/{id}/submissions/{submission_id}/files/{file_id}": { + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, "get": { - "operationId": "getFormSubmissionFile", + "operationId": "listSchedulingOutOfOffice", "tags": [ - "Form submissions" + "Scheduling out of office" ], - "summary": "Download a file", - "description": "A file uploaded with the submission, as `application/octet-stream`: whatever its type, treat it as untrusted (a submitter chose it).", + "summary": "List out of office entries", + "description": "The environment's entries, newest first.", "security": [ { "bearerAuth": [] } ], "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "required": false, + "name": "limit", + "in": "query" + }, { "schema": { "type": "string", - "description": "The form's id.", - "examples": [ - "form_01kd2m8n3p4q5r6s7t8v9w0x1y" - ] + "description": "The last id of the previous page." }, - "required": true, - "description": "The form's id.", - "name": "id", - "in": "path" + "required": false, + "description": "The last id of the previous page.", + "name": "after", + "in": "query" }, { "schema": { "type": "string", - "description": "The submission's id.", + "description": "One host's entries, by id or uid." + }, + "required": false, + "description": "One host's entries, by id or uid.", + "name": "host", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Entries ending after this.", "examples": [ - "fsub_01kd2m9a0b1c2d3e4f5g6h7j8k" + "2027-01-04T15:00:00.000Z" ] }, - "required": true, - "description": "The submission's id.", - "name": "submission_id", - "in": "path" + "required": false, + "description": "Entries ending after this.", + "name": "start_after", + "in": "query" }, { "schema": { "type": "string", - "description": "The file's id (`files[].id` of the submission).", + "description": "Entries starting before this.", "examples": [ - "ffile_01kd2m9a0b1c2d3e4f5g6h7j8m" + "2027-01-04T15:00:00.000Z" ] }, - "required": true, - "description": "The file's id (`files[].id` of the submission).", - "name": "file_id", - "in": "path" + "required": false, + "description": "Entries starting before this.", + "name": "start_before", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "The entries one request made." + }, + "required": false, + "description": "The entries one request made.", + "name": "group", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -66089,7 +73950,7 @@ ], "responses": { "200": { - "description": "The file's bytes.", + "description": "A page of entries.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66111,10 +73972,9 @@ } }, "content": { - "application/octet-stream": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/SchedulingOutOfOfficeList" } } } @@ -66183,7 +74043,28 @@ } }, "404": { - "description": "No such form, submission or file (`not_found`).", + "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66252,7 +74133,7 @@ } }, "503": { - "description": "Forms isn't available in this deployment yet (`forms_unavailable`).", + "description": "The API key could not be verified right now; retry (`service_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66275,32 +74156,40 @@ } } }, - "/v1/scheduling/hosts": { - "post": { - "operationId": "createSchedulingHost", + "/v1/scheduling/out_of_office/{id}": { + "get": { + "operationId": "getSchedulingOutOfOffice", "tags": [ - "Scheduling hosts" + "Scheduling out of office" ], - "summary": "Create a host", - "description": "A person who can be booked: one of your users, such as a doctor or a tutor.", + "summary": "Retrieve an out of office entry", + "description": "One entry.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateHostRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The entry's id.", + "examples": [ + "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] + }, + "required": true, + "description": "The entry's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { - "201": { - "description": "The host.", + "200": { + "description": "The entry.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66324,13 +74213,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingHost" + "$ref": "#/components/schemas/SchedulingOutOfOffice" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66371,27 +74260,6 @@ } } }, - "402": { - "description": "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past the plan's limit (`plan_limit_reached`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -66413,29 +74281,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The request failed validation (`validation_error`, see `issues`), `time_zone` isn't an IANA name (`invalid_time_zone`), or another host has the `uid` (`uid_taken`).", + "404": { + "description": "Nothing with this id or uid exists in the environment (`not_found`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66524,54 +74371,51 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } }, - "get": { - "operationId": "listSchedulingHosts", + "patch": { + "operationId": "updateSchedulingOutOfOffice", "tags": [ - "Scheduling hosts" + "Scheduling out of office" ], - "summary": "List hosts", - "description": "The environment's hosts, newest first.", + "summary": "Update an out of office entry", + "description": "Changes what's given. An entry stays with its host.", "security": [ { "bearerAuth": [] } ], "parameters": [ - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 20 - }, - "required": false, - "name": "limit", - "in": "query" - }, { "schema": { "type": "string", - "description": "The last id of the previous page." + "description": "The entry's id.", + "examples": [ + "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] }, - "required": false, - "description": "The last id of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The entry's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSchedulingOutOfOfficeRequest" + } + } + } + }, "responses": { "200": { - "description": "A page of hosts.", + "description": "The entry.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66595,13 +74439,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingHostList" + "$ref": "#/components/schemas/SchedulingOutOfOffice" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66663,6 +74507,48 @@ } } }, + "404": { + "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "422": { "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", "headers": { @@ -66754,16 +74640,14 @@ } } } - } - }, - "/v1/scheduling/hosts/{host}": { - "get": { - "operationId": "getSchedulingHost", + }, + "delete": { + "operationId": "deleteSchedulingOutOfOffice", "tags": [ - "Scheduling hosts" + "Scheduling out of office" ], - "summary": "Retrieve a host", - "description": "A host by its id or your uid.", + "summary": "Delete an out of office entry", + "description": "The host can be booked in that time again, and any calendar event for it goes.", "security": [ { "bearerAuth": [] @@ -66773,14 +74657,14 @@ { "schema": { "type": "string", - "description": "The host's id or uid.", + "description": "The entry's id.", "examples": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" + "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The host's id or uid.", - "name": "host", + "description": "The entry's id.", + "name": "id", "in": "path" }, { @@ -66789,7 +74673,7 @@ ], "responses": { "200": { - "description": "The host.", + "description": "Deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66813,7 +74697,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingHost" + "$ref": "#/components/schemas/DeletedSchedulingOutOfOffice" } } } @@ -66972,14 +74856,16 @@ } } } - }, - "patch": { - "operationId": "updateSchedulingHost", + } + }, + "/v1/scheduling/out_of_office/by_external_id/{external_id}": { + "put": { + "operationId": "putSchedulingOutOfOffice", "tags": [ - "Scheduling hosts" + "Scheduling out of office" ], - "summary": "Update a host", - "description": "Changes what's given. A `disabled` host keeps its bookings but gets no new ones.", + "summary": "Create or replace an entry by your own id", + "description": "For syncing from a system of record such as an HR tool: send what it says, as often as you like, and there's one entry for it. Give every field you want to keep.", "security": [ { "bearerAuth": [] @@ -66989,14 +74875,14 @@ { "schema": { "type": "string", - "description": "The host's id or uid.", + "description": "Your own id for the entry.", "examples": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" + "hr_leave_4471" ] }, "required": true, - "description": "The host's id or uid.", - "name": "host", + "description": "Your own id for the entry.", + "name": "external_id", "in": "path" }, { @@ -67008,14 +74894,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateHostRequest" + "$ref": "#/components/schemas/PutSchedulingOutOfOfficeRequest" } } } }, "responses": { "200": { - "description": "The host.", + "description": "The entry, replaced.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -67039,7 +74925,37 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingHost" + "$ref": "#/components/schemas/SchedulingOutOfOffice" + } + } + } + }, + "201": { + "description": "The entry, new.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchedulingOutOfOffice" } } } @@ -67150,7 +75066,7 @@ } }, "422": { - "description": "The request failed validation (`validation_error`, see `issues`), `time_zone` isn't an IANA name (`invalid_time_zone`), or another host has the `uid` (`uid_taken`).", + "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -67240,14 +75156,16 @@ } } } - }, - "delete": { - "operationId": "deleteSchedulingHost", + } + }, + "/v1/scheduling/out_of_office/{id}/conflicts": { + "get": { + "operationId": "listSchedulingOutOfOfficeConflicts", "tags": [ - "Scheduling hosts" + "Scheduling out of office" ], - "summary": "Delete a host", - "description": "Removes the host and its schedules and calendars. Refused while bookings are still to come (`host_has_live_bookings`): cancel them, or set the host's status to `disabled`.", + "summary": "List an entry's conflicts", + "description": "Live bookings of the host that fall in the entry, soonest first. Entries never change them.", "security": [ { "bearerAuth": [] @@ -67257,23 +75175,34 @@ { "schema": { "type": "string", - "description": "The host's id or uid.", + "description": "The entry's id.", "examples": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" + "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The host's id or uid.", - "name": "host", + "description": "The entry's id.", + "name": "id", "in": "path" }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 100 + }, + "required": false, + "name": "limit", + "in": "query" + }, { "$ref": "#/components/parameters/CossVersion" } ], "responses": { "200": { - "description": "Deleted.", + "description": "The bookings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -67297,7 +75226,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSchedulingHost" + "$ref": "#/components/schemas/SchedulingOutOfOfficeConflictList" } } } @@ -73801,19 +81730,279 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConfirmHoldRequest" - } - } - } - }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConfirmHoldRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The booking.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Coss-Version": { + "description": "The API version this response is rendered in.", + "schema": { + "type": "string", + "examples": [ + "2026-10-10" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchedulingBooking" + } + } + } + }, + "400": { + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "The API key is missing, invalid, revoked or expired (`authentication_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The booking can't change that way (`booking_not_changeable`), the new time isn't available (`slot_unavailable`), or a hold expired (`hold_expired`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/v1/scheduling/holds/{id}": { + "delete": { + "operationId": "releaseSchedulingHold", + "tags": [ + "Scheduling bookings" + ], + "summary": "Release a hold", + "description": "Gives the slot back early.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The booking's id.", + "examples": [ + "book_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] + }, + "required": true, + "description": "The booking's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "responses": { "200": { - "description": "The booking.", + "description": "The cancelled booking.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -73843,7 +82032,7 @@ } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -73926,27 +82115,6 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "422": { "description": "The booking can't change that way (`booking_not_changeable`), the new time isn't available (`slot_unavailable`), or a hold expired (`hold_expired`).", "headers": { @@ -74040,14 +82208,14 @@ } } }, - "/v1/scheduling/holds/{id}": { - "delete": { - "operationId": "releaseSchedulingHold", + "/v1/scheduling/series": { + "post": { + "operationId": "createSchedulingSeries", "tags": [ - "Scheduling bookings" + "Scheduling series" ], - "summary": "Release a hold", - "description": "Gives the slot back early.", + "summary": "Create a recurring series", + "description": "Books up to 52 occurrences, keeping the attendee's local time across DST changes. Each one is an ordinary booking that can move on its own, and a pool keeps one host throughout.", "security": [ { "bearerAuth": [] @@ -74057,23 +82225,35 @@ { "schema": { "type": "string", - "description": "The booking's id.", + "minLength": 1, + "maxLength": 256, + "description": "Makes retries safe: a repeated request with the same key and body returns the original booking instead of creating another, for 24 hours. A refused request keeps no key. 1-256 characters.", "examples": [ - "book_01kd2m9a0b1c2d3e4f5g6h7j8m" + "welcome-user-123" ] }, - "required": true, - "description": "The booking's id.", - "name": "id", - "in": "path" + "required": false, + "description": "Makes retries safe: a repeated request with the same key and body returns the original booking instead of creating another, for 24 hours. A refused request keeps no key. 1-256 characters.", + "name": "Idempotency-Key", + "in": "header" }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSeriesRequest" + } + } + } + }, "responses": { - "200": { - "description": "The cancelled booking.", + "201": { + "description": "The series and its bookings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -74084,6 +82264,15 @@ ] } }, + "Idempotent-Replayed": { + "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", + "schema": { + "type": "string", + "enum": [ + "true" + ] + } + }, "Coss-Version": { "description": "The API version this response is rendered in.", "schema": { @@ -74097,13 +82286,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingBooking" + "$ref": "#/components/schemas/SchedulingSeries" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -74144,6 +82333,27 @@ } } }, + "402": { + "description": "In live environments: an overdue invoice or spend limit (`billing_hold`), the project's budget (`budget_exceeded`), the plan's allowance (`usage_limit_exceeded`) or what it includes (`plan_feature_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -74186,8 +82396,50 @@ } } }, + "409": { + "description": "A request with this key is still running (`idempotency_in_progress`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "422": { - "description": "The booking can't change that way (`booking_not_changeable`), the new time isn't available (`slot_unavailable`), or a hold expired (`hold_expired`).", + "description": "The time isn't available (`slot_unavailable`), `start`, `duration_minutes` or the occurrence count is out of bounds (`invalid_time`), the attendee's time zone isn't an IANA name (`invalid_time_zone`), the answers or attendee are invalid (`invalid_answers`, `invalid_attendee`), or the `Idempotency-Key` was used with a different request (`idempotency_key_reused`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -74279,39 +82531,19 @@ } } }, - "/v1/scheduling/series": { + "/v1/scheduling/series/preview": { "post": { - "operationId": "createSchedulingSeries", + "operationId": "previewSchedulingSeries", "tags": [ "Scheduling series" ], - "summary": "Create a recurring series", - "description": "Books up to 52 occurrences, keeping the attendee's local time across DST changes. Each one is an ordinary booking that can move on its own, and a pool keeps one host throughout.", + "summary": "Preview a recurring series", + "description": "Which occurrences would be free, without booking anything.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Makes retries safe: a repeated request with the same key and body returns the original booking instead of creating another, for 24 hours. A refused request keeps no key. 1-256 characters.", - "examples": [ - "welcome-user-123" - ] - }, - "required": false, - "description": "Makes retries safe: a repeated request with the same key and body returns the original booking instead of creating another, for 24 hours. A refused request keeps no key. 1-256 characters.", - "name": "Idempotency-Key", - "in": "header" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], "requestBody": { "required": true, "content": { @@ -74323,8 +82555,8 @@ } }, "responses": { - "201": { - "description": "The series and its bookings.", + "200": { + "description": "Each occurrence.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -74335,15 +82567,6 @@ ] } }, - "Idempotent-Replayed": { - "description": "`true` when this response replays an earlier request with the same `Idempotency-Key`.", - "schema": { - "type": "string", - "enum": [ - "true" - ] - } - }, "Coss-Version": { "description": "The API version this response is rendered in.", "schema": { @@ -74357,7 +82580,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingSeries" + "$ref": "#/components/schemas/SchedulingSeriesPreview" } } } @@ -74405,7 +82628,7 @@ } }, "402": { - "description": "In live environments: an overdue invoice or spend limit (`billing_hold`), the project's budget (`budget_exceeded`), the plan's allowance (`usage_limit_exceeded`) or what it includes (`plan_feature_unavailable`).", + "description": "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past the plan's limit (`plan_limit_reached`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -74467,27 +82690,6 @@ } } }, - "409": { - "description": "A request with this key is still running (`idempotency_in_progress`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "415": { "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { @@ -74510,7 +82712,7 @@ } }, "422": { - "description": "The time isn't available (`slot_unavailable`), `start`, `duration_minutes` or the occurrence count is out of bounds (`invalid_time`), the attendee's time zone isn't an IANA name (`invalid_time_zone`), the answers or attendee are invalid (`invalid_answers`, `invalid_attendee`), or the `Idempotency-Key` was used with a different request (`idempotency_key_reused`).", + "description": "The request failed validation (`validation_error`, see `issues`), or `start`, `end` or `duration_minutes` is invalid: `end` isn't after `start`, the window is longer than 62 days, or the duration isn't one of the event type's (`invalid_time`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -74599,35 +82801,58 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/scheduling/series/preview": { + "/v1/scheduling/series/{id}/cancel": { "post": { - "operationId": "previewSchedulingSeries", + "operationId": "cancelSchedulingSeries", "tags": [ "Scheduling series" ], - "summary": "Preview a recurring series", - "description": "Which occurrences would be free, without booking anything.", + "summary": "Cancel a recurring series", + "description": "Cancels the occurrences from `from_index` on, or all of them.", "security": [ { "bearerAuth": [] } ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The series' id.", + "examples": [ + "series_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] + }, + "required": true, + "description": "The series' id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateSeriesRequest" + "$ref": "#/components/schemas/CancelSeriesRequest" } } } }, "responses": { "200": { - "description": "Each occurrence.", + "description": "The cancelled bookings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -74651,7 +82876,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingSeriesPreview" + "$ref": "#/components/schemas/SchedulingCancelledSeries" } } } @@ -74698,27 +82923,6 @@ } } }, - "402": { - "description": "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past the plan's limit (`plan_limit_reached`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -74782,27 +82986,6 @@ } } }, - "422": { - "description": "The request failed validation (`validation_error`, see `issues`), or `start`, `end` or `duration_minutes` is invalid: `end` isn't after `start`, the window is longer than 62 days, or the duration isn't one of the event type's (`invalid_time`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -74872,22 +83055,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/scheduling/series/{id}/cancel": { - "post": { - "operationId": "cancelSchedulingSeries", + "/v1/scheduling/attendees/{uid}": { + "delete": { + "operationId": "eraseSchedulingAttendee", "tags": [ - "Scheduling series" + "Scheduling bookings" ], - "summary": "Cancel a recurring series", - "description": "Cancels the occurrences from `from_index` on, or all of them.", + "summary": "Erase an attendee", + "description": "Removes this person's details and answers from every booking now, and ends any booker session fixed to them. The bookings themselves stay.", "security": [ { "bearerAuth": [] @@ -74897,33 +83075,23 @@ { "schema": { "type": "string", - "description": "The series' id.", + "description": "Your id for the attendee.", "examples": [ - "series_01kd2m9a0b1c2d3e4f5g6h7j8m" + "patient_77" ] }, "required": true, - "description": "The series' id.", - "name": "id", + "description": "Your id for the attendee.", + "name": "uid", "in": "path" }, { "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CancelSeriesRequest" - } - } - } - }, "responses": { "200": { - "description": "The cancelled bookings.", + "description": "How many were erased.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -74947,13 +83115,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCancelledSeries" + "$ref": "#/components/schemas/SchedulingAttendeeErasure" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -75015,48 +83183,6 @@ } } }, - "404": { - "description": "Nothing with this id or uid exists in the environment (`not_found`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -75129,37 +83255,29 @@ } } }, - "/v1/scheduling/attendees/{uid}": { - "delete": { - "operationId": "eraseSchedulingAttendee", + "/v1/scheduling/attendees/erase": { + "post": { + "operationId": "eraseSchedulingAttendeeByEmail", "tags": [ "Scheduling bookings" ], - "summary": "Erase an attendee", - "description": "Removes this person's details and answers from every booking now, and ends any booker session fixed to them. The bookings themselves stay.", + "summary": "Erase an attendee or guest by email", + "description": "Removes this address's details and answers from every booking now, for someone you have no `uid` for, such as a guest. The bookings themselves stay.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "Your id for the attendee.", - "examples": [ - "patient_77" - ] - }, - "required": true, - "description": "Your id for the attendee.", - "name": "uid", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EraseSchedulingAttendeeByEmailRequest" + } + } } - ], + }, "responses": { "200": { "description": "How many were erased.", @@ -75186,13 +83304,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingAttendeeErasure" + "$ref": "#/components/schemas/SchedulingAttendeeErasureByEmail" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -75254,6 +83372,27 @@ } } }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -75323,35 +83462,58 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/scheduling/attendees/erase": { + "/v1/scheduling/hosts/{host}/calendar_connections/authorize": { "post": { - "operationId": "eraseSchedulingAttendeeByEmail", + "operationId": "authorizeSchedulingCalendar", "tags": [ - "Scheduling bookings" + "Scheduling calendars" ], - "summary": "Erase an attendee or guest by email", - "description": "Removes this address's details and answers from every booking now, for someone you have no `uid` for, such as a guest. The bookings themselves stay.", + "summary": "Start connecting a calendar", + "description": "A URL for the host to give consent at Google or Microsoft. Open it in a new window: providers refuse to be framed. Coss.com reads only busy times, and writes events with a neutral title unless the event type's `host_calendar_details` is `full`.", "security": [ { "bearerAuth": [] } ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The host's id or uid.", + "examples": [ + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] + }, + "required": true, + "description": "The host's id or uid.", + "name": "host", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EraseSchedulingAttendeeByEmailRequest" + "$ref": "#/components/schemas/AuthorizeCalendarRequest" } } } }, "responses": { "200": { - "description": "How many were erased.", + "description": "Where to send the host.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -75375,7 +83537,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingAttendeeErasureByEmail" + "$ref": "#/components/schemas/SchedulingCalendarAuthorization" } } } @@ -75443,6 +83605,27 @@ } } }, + "404": { + "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "415": { "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { @@ -75464,6 +83647,27 @@ } } }, + "422": { + "description": "The request failed validation (`validation_error`, see `issues`), or the calendar can't be connected: the provider isn't set up, the server's URL is refused, or a simulated calendar was asked for outside a sandbox environment (`calendar_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -75533,22 +83737,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/scheduling/hosts/{host}/calendar_connections/authorize": { + "/v1/scheduling/hosts/{host}/calendar_connections": { "post": { - "operationId": "authorizeSchedulingCalendar", + "operationId": "createSchedulingCalendarConnection", "tags": [ "Scheduling calendars" ], - "summary": "Start connecting a calendar", - "description": "A URL for the host to give consent at Google or Microsoft. Open it in a new window: providers refuse to be framed. Coss.com reads only busy times, and writes events with a neutral title unless the event type's `host_calendar_details` is `full`.", + "summary": "Connect a calendar with a password", + "description": "CalDAV (Apple and others) with an app password, or, in sandbox environments, a `simulated` calendar whose busy times you set yourself. Google and Microsoft use `authorize`.", "security": [ { "bearerAuth": [] @@ -75577,14 +83776,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AuthorizeCalendarRequest" + "$ref": "#/components/schemas/CreateCalendarConnectionRequest" } } } }, "responses": { - "200": { - "description": "Where to send the host.", + "201": { + "description": "The connection.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -75608,7 +83807,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarAuthorization" + "$ref": "#/components/schemas/SchedulingCalendarConnection" } } } @@ -75809,16 +84008,14 @@ } } } - } - }, - "/v1/scheduling/hosts/{host}/calendar_connections": { - "post": { - "operationId": "createSchedulingCalendarConnection", + }, + "get": { + "operationId": "listSchedulingCalendarConnections", "tags": [ "Scheduling calendars" ], - "summary": "Connect a calendar with a password", - "description": "CalDAV (Apple and others) with an app password, or, in sandbox environments, a `simulated` calendar whose busy times you set yourself. Google and Microsoft use `authorize`.", + "summary": "List a host's calendar connections", + "description": "With each connection's calendars and which of them count.", "security": [ { "bearerAuth": [] @@ -75842,19 +84039,9 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateCalendarConnectionRequest" - } - } - } - }, "responses": { - "201": { - "description": "The connection.", + "200": { + "description": "The connections.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -75878,13 +84065,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarConnection" + "$ref": "#/components/schemas/SchedulingCalendarConnectionList" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -75967,48 +84154,6 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The request failed validation (`validation_error`, see `issues`), or the calendar can't be connected: the provider isn't set up, the server's URL is refused, or a simulated calendar was asked for outside a sandbox environment (`calendar_unavailable`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -76079,14 +84224,16 @@ } } } - }, - "get": { - "operationId": "listSchedulingCalendarConnections", + } + }, + "/v1/scheduling/hosts/{host}/calendar_connections/{id}": { + "delete": { + "operationId": "deleteSchedulingCalendarConnection", "tags": [ "Scheduling calendars" ], - "summary": "List a host's calendar connections", - "description": "With each connection's calendars and which of them count.", + "summary": "Disconnect a calendar", + "description": "Revokes coss.com's access and deletes every busy time read through it. One a calendar delegation manages is refused (`calendar_managed`).", "security": [ { "bearerAuth": [] @@ -76106,13 +84253,26 @@ "name": "host", "in": "path" }, + { + "schema": { + "type": "string", + "description": "The connection's id.", + "examples": [ + "calconn_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] + }, + "required": true, + "description": "The connection's id.", + "name": "id", + "in": "path" + }, { "$ref": "#/components/parameters/CossVersion" } ], "responses": { "200": { - "description": "The connections.", + "description": "Disconnected.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76136,7 +84296,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarConnectionList" + "$ref": "#/components/schemas/DeletedSchedulingCalendarConnection" } } } @@ -76225,6 +84385,27 @@ } } }, + "422": { + "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -76297,14 +84478,14 @@ } } }, - "/v1/scheduling/hosts/{host}/calendar_connections/{id}": { - "delete": { - "operationId": "deleteSchedulingCalendarConnection", + "/v1/scheduling/hosts/{host}/calendars/{calendar_id}": { + "patch": { + "operationId": "updateSchedulingCalendar", "tags": [ "Scheduling calendars" ], - "summary": "Disconnect a calendar", - "description": "Revokes coss.com's access and deletes every busy time read through it. One a calendar delegation manages is refused (`calendar_managed`).", + "summary": "Choose how a calendar counts", + "description": "Whether its busy times block slots, and whether bookings are written to it.", "security": [ { "bearerAuth": [] @@ -76327,23 +84508,33 @@ { "schema": { "type": "string", - "description": "The connection's id.", + "description": "The calendar's id.", "examples": [ - "calconn_01kd2m9a0b1c2d3e4f5g6h7j8m" + "cal_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The connection's id.", - "name": "id", + "description": "The calendar's id.", + "name": "calendar_id", "in": "path" }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCalendarRequest" + } + } + } + }, "responses": { "200": { - "description": "Disconnected.", + "description": "The host's connections.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76367,13 +84558,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSchedulingCalendarConnection" + "$ref": "#/components/schemas/SchedulingCalendarConnectionList" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76456,8 +84647,8 @@ } } }, - "422": { - "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76549,63 +84740,32 @@ } } }, - "/v1/scheduling/hosts/{host}/calendars/{calendar_id}": { - "patch": { - "operationId": "updateSchedulingCalendar", + "/v1/scheduling/calendar_delegations": { + "post": { + "operationId": "createSchedulingCalendarDelegation", "tags": [ "Scheduling calendars" ], - "summary": "Choose how a calendar counts", - "description": "Whether its busy times block slots, and whether bookings are written to it.", + "summary": "Add a calendar delegation", + "description": "An admin authorizes once, and every host on the domain gets their calendar with no consent screen. It stays `pending`: authorize what it returns, then verify it. Credentials are never returned.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The host's id or uid.", - "examples": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" - ] - }, - "required": true, - "description": "The host's id or uid.", - "name": "host", - "in": "path" - }, - { - "schema": { - "type": "string", - "description": "The calendar's id.", - "examples": [ - "cal_01kd2m9a0b1c2d3e4f5g6h7j8m" - ] - }, - "required": true, - "description": "The calendar's id.", - "name": "calendar_id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateCalendarRequest" + "$ref": "#/components/schemas/CreateCalendarDelegationRequest" } } } }, "responses": { - "200": { - "description": "The host's connections.", + "201": { + "description": "The delegation.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76629,7 +84789,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarConnectionList" + "$ref": "#/components/schemas/SchedulingCalendarDelegation" } } } @@ -76676,8 +84836,8 @@ } } }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "402": { + "description": "The organization's plan doesn't include calendar delegations (`plan_feature_unavailable`) (`billing_error`). Never retried: it needs a plan change or a payment.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76697,8 +84857,8 @@ } } }, - "404": { - "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "403": { + "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76739,6 +84899,27 @@ } } }, + "422": { + "description": "The credentials or mailbox aren't valid (`invalid_calendar_delegation`), or the provider refused them (`calendar_delegation_refused`, and `message` says what to fix).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -76808,35 +84989,28 @@ } } } - } - } - }, - "/v1/scheduling/calendar_delegations": { - "post": { - "operationId": "createSchedulingCalendarDelegation", + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, + "get": { + "operationId": "listSchedulingCalendarDelegations", "tags": [ "Scheduling calendars" ], - "summary": "Add a calendar delegation", - "description": "An admin authorizes once, and every host on the domain gets their calendar with no consent screen. It stays `pending`: authorize what it returns, then verify it. Credentials are never returned.", + "summary": "List calendar delegations", + "description": "With how many hosts each has connected.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateCalendarDelegationRequest" - } - } - } - }, "responses": { - "201": { - "description": "The delegation.", + "200": { + "description": "The delegations.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76860,13 +85034,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarDelegation" + "$ref": "#/components/schemas/SchedulingCalendarDelegationList" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76907,27 +85081,6 @@ } } }, - "402": { - "description": "The organization's plan doesn't include calendar delegations (`plan_feature_unavailable`) (`billing_error`). Never retried: it needs a plan change or a payment.", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -76949,48 +85102,6 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The credentials or mailbox aren't valid (`invalid_calendar_delegation`), or the provider refused them (`calendar_delegation_refused`, and `message` says what to fix).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -77066,22 +85177,42 @@ "$ref": "#/components/parameters/CossVersion" } ] - }, + } + }, + "/v1/scheduling/calendar_delegations/{id}": { "get": { - "operationId": "listSchedulingCalendarDelegations", + "operationId": "getSchedulingCalendarDelegation", "tags": [ "Scheduling calendars" ], - "summary": "List calendar delegations", - "description": "With how many hosts each has connected.", + "summary": "Get a calendar delegation", + "description": "Its status, what to authorize, and how many hosts it has connected.", "security": [ { "bearerAuth": [] } ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The delegation's id.", + "examples": [ + "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] + }, + "required": true, + "description": "The delegation's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "responses": { "200": { - "description": "The delegations.", + "description": "The delegation.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -77105,7 +85236,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarDelegationList" + "$ref": "#/components/schemas/SchedulingCalendarDelegation" } } } @@ -77173,6 +85304,27 @@ } } }, + "404": { + "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -77242,22 +85394,15 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - } - }, - "/v1/scheduling/calendar_delegations/{id}": { - "get": { - "operationId": "getSchedulingCalendarDelegation", + } + }, + "patch": { + "operationId": "updateSchedulingCalendarDelegation", "tags": [ "Scheduling calendars" ], - "summary": "Get a calendar delegation", - "description": "Its status, what to authorize, and how many hosts it has connected.", + "summary": "Update a calendar delegation", + "description": "New credentials are tried on the verified mailbox before they replace the old ones, and end an outage if they work.", "security": [ { "bearerAuth": [] @@ -77281,6 +85426,16 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCalendarDelegationRequest" + } + } + } + }, "responses": { "200": { "description": "The delegation.", @@ -77313,7 +85468,7 @@ } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -77354,6 +85509,27 @@ } } }, + "402": { + "description": "The organization's plan doesn't include calendar delegations (`plan_feature_unavailable`) (`billing_error`). Never retried: it needs a plan change or a payment.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -77396,6 +85572,48 @@ } } }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The credentials or mailbox aren't valid (`invalid_calendar_delegation`), or the provider refused them (`calendar_delegation_refused`, and `message` says what to fix).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -77467,13 +85685,13 @@ } } }, - "patch": { - "operationId": "updateSchedulingCalendarDelegation", + "delete": { + "operationId": "deleteSchedulingCalendarDelegation", "tags": [ "Scheduling calendars" ], - "summary": "Update a calendar delegation", - "description": "New credentials are tried on the verified mailbox before they replace the old ones, and end an outage if they work.", + "summary": "Delete a calendar delegation", + "description": "Its hosts lose these calendars within minutes. They stay bookable with no conflict check, and events already written stay on their calendars, out of reach of later moves or cancellations.", "security": [ { "bearerAuth": [] @@ -77497,19 +85715,9 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateCalendarDelegationRequest" - } - } - } - }, "responses": { "200": { - "description": "The delegation.", + "description": "Being removed.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -77533,13 +85741,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarDelegation" + "$ref": "#/components/schemas/DeletedSchedulingCalendarDelegation" } } } }, "400": { - "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -77580,27 +85788,6 @@ } } }, - "402": { - "description": "The organization's plan doesn't include calendar delegations (`plan_feature_unavailable`) (`billing_error`). Never retried: it needs a plan change or a payment.", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -77643,48 +85830,6 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "422": { - "description": "The credentials or mailbox aren't valid (`invalid_calendar_delegation`), or the provider refused them (`calendar_delegation_refused`, and `message` says what to fix).", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -77755,14 +85900,16 @@ } } } - }, - "delete": { - "operationId": "deleteSchedulingCalendarDelegation", + } + }, + "/v1/scheduling/calendar_delegations/{id}/verify": { + "post": { + "operationId": "verifySchedulingCalendarDelegation", "tags": [ "Scheduling calendars" ], - "summary": "Delete a calendar delegation", - "description": "Its hosts lose these calendars within minutes. They stay bookable with no conflict check, and events already written stay on their calendars, out of reach of later moves or cancellations.", + "summary": "Verify a calendar delegation", + "description": "Tries one real mailbox. Passing makes it `active`, and its hosts start connecting. Google can take up to 24 hours to apply a new authorization, so a refusal may just need time.", "security": [ { "bearerAuth": [] @@ -77786,9 +85933,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyCalendarDelegationRequest" + } + } + } + }, "responses": { "200": { - "description": "Being removed.", + "description": "The delegation.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -77812,13 +85969,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSchedulingCalendarDelegation" + "$ref": "#/components/schemas/SchedulingCalendarDelegation" } } } }, "400": { - "description": "The `Coss-Version` header names an unknown API version (`invalid_api_version`).", + "description": "The body is not valid JSON (`invalid_json`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -77859,6 +86016,27 @@ } } }, + "402": { + "description": "The organization's plan doesn't include calendar delegations (`plan_feature_unavailable`) (`billing_error`). Never retried: it needs a plan change or a payment.", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -77901,6 +86079,48 @@ } } }, + "415": { + "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "The credentials or mailbox aren't valid (`invalid_calendar_delegation`), or the provider refused them (`calendar_delegation_refused`, and `message` says what to fix).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "429": { "description": "Too many requests with this API key (`rate_limited`), or too many requests with invalid keys from this address (`too_many_failed_authentications`). Retry after `Retry-After` seconds. Successful responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the limit is fully restored).", "headers": { @@ -77973,14 +86193,14 @@ } } }, - "/v1/scheduling/calendar_delegations/{id}/verify": { - "post": { - "operationId": "verifySchedulingCalendarDelegation", + "/v1/scheduling/hosts/{host}/simulated_busy": { + "put": { + "operationId": "setSchedulingSimulatedBusy", "tags": [ "Scheduling calendars" ], - "summary": "Verify a calendar delegation", - "description": "Tries one real mailbox. Passing makes it `active`, and its hosts start connecting. Google can take up to 24 hours to apply a new authorization, so a refusal may just need time.", + "summary": "Set a simulated calendar's busy times", + "description": "Sandbox environments only: test how bookings react to a busy calendar.", "security": [ { "bearerAuth": [] @@ -77990,14 +86210,14 @@ { "schema": { "type": "string", - "description": "The delegation's id.", + "description": "The host's id or uid.", "examples": [ - "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The delegation's id.", - "name": "id", + "description": "The host's id or uid.", + "name": "host", "in": "path" }, { @@ -78009,14 +86229,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerifyCalendarDelegationRequest" + "$ref": "#/components/schemas/SetSimulatedBusyRequest" } } } }, "responses": { - "200": { - "description": "The delegation.", + "204": { + "description": "Set.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -78036,13 +86256,6 @@ ] } } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SchedulingCalendarDelegation" - } - } } }, "400": { @@ -78087,27 +86300,6 @@ } } }, - "402": { - "description": "The organization's plan doesn't include calendar delegations (`plan_feature_unavailable`) (`billing_error`). Never retried: it needs a plan change or a payment.", - "headers": { - "X-Request-Id": { - "description": "Unique id of this request. Include it when contacting support.", - "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -78172,7 +86364,7 @@ } }, "422": { - "description": "The credentials or mailbox aren't valid (`invalid_calendar_delegation`), or the provider refused them (`calendar_delegation_refused`, and `message` says what to fix).", + "description": "The body failed validation (`validation_error`, see `issues`), or this isn't a sandbox environment: simulated calendars only exist there (`calendar_unavailable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -78264,50 +86456,32 @@ } } }, - "/v1/scheduling/hosts/{host}/simulated_busy": { - "put": { - "operationId": "setSchedulingSimulatedBusy", + "/v1/scheduling/manager_sessions": { + "post": { + "operationId": "createSchedulingManagerSession", "tags": [ - "Scheduling calendars" + "Scheduling hosts" ], - "summary": "Set a simulated calendar's busy times", - "description": "Sandbox environments only: test how bookings react to a busy calendar.", + "summary": "Create a manager session", + "description": "Lets a team lead manage cover for the hosts you name, or those matching an attribute filter whenever it's asked: time off, bookings that need someone else, and moving them. It never sees attendees' details. Send the `url` to their browser, or embed it with `coss/scheduling`.", "security": [ { "bearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The host's id or uid.", - "examples": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" - ] - }, - "required": true, - "description": "The host's id or uid.", - "name": "host", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SetSimulatedBusyRequest" + "$ref": "#/components/schemas/CreateSchedulingManagerSessionRequest" } } } }, "responses": { - "204": { - "description": "Set.", + "201": { + "description": "The session.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -78327,6 +86501,13 @@ ] } } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchedulingSession" + } + } } }, "400": { @@ -78435,7 +86616,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`, see `issues`), or this isn't a sandbox environment: simulated calendars only exist there (`calendar_unavailable`).", + "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -78524,7 +86705,12 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, "/v1/scheduling/hosts/{host}/sessions": { diff --git a/apps/api/src/middleware/request-log.ts b/apps/api/src/middleware/request-log.ts index f663dce2e..0bdd4b584 100644 --- a/apps/api/src/middleware/request-log.ts +++ b/apps/api/src/middleware/request-log.ts @@ -53,7 +53,7 @@ const FORM_SUBMISSIONS = /^\/v1\/forms\/[^/]+\/submissions(\/|$)/; * delegation's requests carry a service account's private key or an app's client secret. */ const SCHEDULING_PERSONAL = - /^\/v1\/scheduling\/((bookings|holds|series|attendees|booker_sessions|calendar_delegations)(\/|$)|hosts\/[^/]+\/calendar_connections)/; + /^\/v1\/scheduling\/((bookings|holds|series|attendees|booker_sessions|calendar_delegations|out_of_office|reassignments|holiday_calendars)(\/|$)|hosts\/[^/]+\/calendar_connections)/; /** * Responses that are capabilities: preference links change a contact's consent and need * `contact:update`, and Sign's session links (the token is in the URL's fragment too) sign as a @@ -62,6 +62,7 @@ const SCHEDULING_PERSONAL = const CAPABILITY_RESPONSES = [ /^\/v1\/scheduling\/hosts\/[^/]+\/sessions$/, /^\/v1\/scheduling\/booker_sessions$/, + /^\/v1\/scheduling\/manager_sessions$/, /^\/v1\/contacts\/[^/]+\/preference_links$/, /^\/v1\/sign\/requests\/[^/]+\/signers\/[^/]+\/sessions$/, /^\/v1\/sign\/templates\/[^/]+\/builder_sessions$/, diff --git a/apps/api/src/routes/scheduling-client.ts b/apps/api/src/routes/scheduling-client.ts index f7f038e60..d37f67ccc 100644 --- a/apps/api/src/routes/scheduling-client.ts +++ b/apps/api/src/routes/scheduling-client.ts @@ -5,41 +5,65 @@ * checked against the environment's allowed origins. Each session reaches only its own scope. * Outside `/v1` and the OpenAPI document: `coss/scheduling` is its client. */ +import { db } from "@coss/db"; import { type AuthenticatedSession, approveBooking, + assertManagedRun, attendeeEmailedOnConfirm, authenticateSchedulingSession, + awayNoticeOf, cancelBooking, + cancelReassignment, connectCaldav, connectSimulated, createBooking, + createOutOfOffice, + createReassignment, createSchedule, declineBooking, + deleteOutOfOffice, disconnectCalendar, fixedAttendee, getBooking, getEventType, getHost, getLogoRow, + getOutOfOffice, + getReassignment, getSettingsRow, listConnections, listEventTypeSlots, + listManagedBookings, + listOutOfOffice, + listOutOfOfficeConflicts, + listPossibleDelegates, + listReassignCandidates, + listReassignmentItems, + listReassignments, listRecentHostBookings, listRescheduleSlots, listSchedules, listUpcomingHostBookings, logoDataUrl, + managedHosts, markNoShow, + previewReassignment, + reassignBooking, rescheduleBooking, + resolveHost, type SchedulingDeps, SchedulingError, + schedulingSessionRefused, sessionActor, sessionAttributes, startCalendarAuthorization, toConnectionObject, + toManagedHost, + undoReassignment, updateCalendar, updateHost, + updateOutOfOffice, updateSchedule, } from "@coss/scheduling"; import { @@ -47,11 +71,21 @@ import { CancelBookingRequest, CreateBookingRequest, CreateCalendarConnectionRequest, + CreateReassignmentRequest, + HostOutOfOfficeRequest, + ManagerOutOfOfficeRequest, + OUT_OF_OFFICE_PRESET_REASONS, + OUT_OF_OFFICE_REASON_LABELS, + PreviewReassignmentRequest, + ReassignBookingRequest, RescheduleBookingRequest, + UndoReassignmentRequest, UpdateCalendarRequest, UpdateHostLocationsRequest, + UpdateOutOfOfficeRequest, UpsertScheduleRequest, } from "@coss/scheduling/schemas"; +import { memberStillHolds } from "@coss/tenancy"; import { type Context, Hono, type MiddlewareHandler } from "hono"; import { z } from "zod"; import { ApiError } from "../errors.ts"; @@ -114,6 +148,17 @@ function parseJson(text: string): unknown { const notFound = () => new SchedulingError("not_found", "That isn't available from this link."); +const HostReassignRequest = z.strictObject({ + to_host: z.string(), + note: z.string().max(500).optional(), +}); + +/** Moving bookings is its own step from a browser, with its own rules about who can. */ +const SessionOutOfOfficeChange = UpdateOutOfOfficeRequest.omit({ + existing_bookings: true, + reassign: true, +}); + /** What a browser booking may say: the event type and host come from the session. */ const ClientBookingRequest = CreateBookingRequest.omit({ event_type: true, @@ -226,6 +271,23 @@ export function createSchedulingClientRoutes(opts: { router.use(authenticated); + // A team page a dashboard member opened reaches every host, on their say-so, so it lasts only as + // long as they could still do that. Refused like any session that's over. + router.use("/manager/*", async (c, next) => { + const { session, env } = c.var.session; + if ( + session.createdByUserId && + !(await memberStillHolds({ + organizationId: env.organizationId, + userId: session.createdByUserId, + permissions: { scheduling: ["update"] }, + })) + ) { + throw schedulingSessionRefused(); + } + await next(); + }); + const hostOf = (c: Context): string => { const { session } = c.var.session; if (session.kind !== "host" || !session.hostId) throw notFound(); @@ -305,15 +367,31 @@ export function createSchedulingClientRoutes(opts: { } if (session.kind === "host") { const host = await getHost(env, session.hostId as string); + const settings = await getSettingsRow(env); return c.json({ ...base, host: { id: host.id, name: host.name, time_zone: host.time_zone }, zoom_available: Boolean(deps.calendars?.apps.zoom), + can_reassign: settings?.hostReassignment === "eligible", }); } return c.json(base); }); + /** Only when one person can be booked here: "they're away" can't be said of a pool. */ + const awayNoticeFor = async ( + env: AuthenticatedSession["env"], + eventType: string, + pinnedHost: string | null, + ) => { + const now = deps.now?.() ?? new Date(); + if (pinnedHost) return awayNoticeOf(env, pinnedHost, now); + const { hosts, kind } = await getEventType(env, eventType); + const [only] = hosts; + if (kind === "pool" || hosts.length !== 1 || !only) return null; + return awayNoticeOf(env, only.host, now); + }; + const slotWindow = (c: Context) => { const query = SlotQuery.safeParse({ start: c.req.query("start"), @@ -333,6 +411,7 @@ export function createSchedulingClientRoutes(opts: { const slotList = ( c: Context, slots: { startMs: number; endMs: number; seatsLeft?: number | null }[], + extra: Record = {}, ) => c.json({ object: "list" as const, @@ -342,26 +421,9 @@ export function createSchedulingClientRoutes(opts: { seats_left: slot.seatsLeft ?? null, })), has_more: false, + ...extra, }); - const slotsFor = async ( - c: Context, - eventType: string, - hostRefs: readonly string[] | undefined, - attributes?: Record, - ) => { - await limitSchedulingSlots(c.var.rateLimiter, c.var.session.env); - return slotList( - c, - await listEventTypeSlots(c.var.session.env, { - ...slotWindow(c), - eventTypeRef: eventType, - hostRefs, - attributes, - }), - ); - }; - /** Where this booking could move to: its own hosts, length and buffers. */ const rescheduleSlots = async (c: Context, id: string) => { await limitSchedulingSlots(c.var.rateLimiter, c.var.session.env); @@ -369,14 +431,19 @@ export function createSchedulingClientRoutes(opts: { return slotList(c, await listRescheduleSlots(c.var.session.env, id, { fromMs, toMs, nowMs })); }; - router.get("/slots", async (c) => - slotsFor( - c, - eventTypeOf(c), - c.var.session.session.hostId ? [c.var.session.session.hostId] : undefined, - sessionAttributes(c.var.session.session, deps), - ), - ); + router.get("/slots", async (c) => { + // The session's kind first: the wrong one is a 404, whatever the query says. + const eventTypeRef = eventTypeOf(c); + const { env, session } = c.var.session; + await limitSchedulingSlots(c.var.rateLimiter, env); + const slots = await listEventTypeSlots(env, { + ...slotWindow(c), + eventTypeRef, + hostRefs: session.hostId ? [session.hostId] : undefined, + attributes: sessionAttributes(session, deps), + }); + return slotList(c, slots, { away: await awayNoticeFor(env, eventTypeRef, session.hostId) }); + }); router.post("/bookings", async (c) => { const eventType = eventTypeOf(c); @@ -548,6 +615,85 @@ export function createSchedulingClientRoutes(opts: { return c.json({ object: "list" as const, data, has_more: false }); }); + // A host's own time away. Whose it is comes from the session, so it never needs saying. + router.get("/host/out_of_office", async (c) => { + const hostId = hostOf(c); + const { env } = c.var.session; + const page = await listOutOfOffice( + env, + { hostIds: [hostId], fromMs: (deps.now?.() ?? new Date()).getTime(), limit: 100 }, + deps, + ); + const settings = await getSettingsRow(env); + return c.json({ + object: "list" as const, + data: page.data, + has_more: page.hasMore, + reasons: [ + ...OUT_OF_OFFICE_PRESET_REASONS.map((key) => ({ + key, + label: OUT_OF_OFFICE_REASON_LABELS[key], + })), + ...(settings?.outOfOfficeReasons ?? []), + ], + }); + }); + + /** Who they can name to cover for them. Ids and names, and only of people they share work with. */ + const delegatesFor = async (env: AuthenticatedSession["env"], hostId: string) => + (await listPossibleDelegates(env, hostId)).map((host) => host.id); + + router.get("/host/delegates", async (c) => { + const data = await listPossibleDelegates(c.var.session.env, hostOf(c)); + return c.json({ object: "list" as const, data, has_more: false }); + }); + + router.post("/host/out_of_office", async (c) => { + const hostId = writableHost(c); + const { env } = c.var.session; + const [entry] = await createOutOfOffice( + env, + { ...(await parsed(c, HostOutOfOfficeRequest)), host: hostId }, + deps, + sessionActor(c.var.session.session), + "host", + { allowedDelegates: await delegatesFor(env, hostId) }, + ); + return c.json(entry, 201); + }); + + router.patch("/host/out_of_office/:id", async (c) => { + const hostId = writableHost(c); + const entry = await updateOutOfOffice( + c.var.session.env, + c.req.param("id"), + await parsed(c, SessionOutOfOfficeChange), + deps, + sessionActor(c.var.session.session), + { hostId }, + { allowedDelegates: await delegatesFor(c.var.session.env, hostId), startsNoRun: true }, + ); + return c.json(entry); + }); + + router.delete("/host/out_of_office/:id", async (c) => { + const hostId = writableHost(c); + await deleteOutOfOffice(c.var.session.env, c.req.param("id"), deps, { hostId }); + return c.body(null, 204); + }); + + router.get("/host/out_of_office/:id/conflicts", async (c) => { + const hostId = hostOf(c); + const page = await listOutOfOfficeConflicts( + c.var.session.env, + c.req.param("id"), + { limit: 200 }, + deps, + { hostId }, + ); + return c.json({ object: "list" as const, data: page.data, has_more: page.hasMore }); + }); + router.get("/host/locations", async (c) => { const host = await getHost(c.var.session.env, hostOf(c)); return c.json({ object: "list" as const, data: host.locations, has_more: false }); @@ -584,12 +730,293 @@ export function createSchedulingClientRoutes(opts: { ); return c.json({ object: "list" as const, - data: page.data, + // What this page shows and no more: not contact details, metadata or the other hosts. + data: page.data.map((booking) => ({ + id: booking.id, + status: booking.status, + title: booking.title, + start: booking.start, + end: booking.end, + no_show_at: booking.no_show_at, + group_session: booking.group_session, + attendees: booking.attendees.map((attendee) => ({ name: attendee.name })), + })), has_more: page.nextCursor !== null, next_cursor: page.nextCursor, }); }); + /** Off unless the environment says so, and then it looks like nothing's there. */ + const reassigningHost = async (c: Context) => { + const settings = await getSettingsRow(c.var.session.env); + if (settings?.hostReassignment !== "eligible") throw notFound(); + }; + + // Who's free and takes this event type, by name. Why anyone else can't isn't this host's to know. + router.get("/host/bookings/:id/reassign_candidates", async (c) => { + const hostId = hostOf(c); + await reassigningHost(c); + const id = c.req.param("id"); + await hostsOwnBooking(c, hostId, id); + const page = await listReassignCandidates(c.var.session.env, id, { fromHost: hostId }, deps); + const data = page.data + .filter((candidate) => candidate.member && candidate.available) + .map((candidate) => ({ host: candidate.host, name: candidate.name })); + return c.json({ object: "list" as const, data, has_more: false }); + }); + + router.post("/host/bookings/:id/reassign", async (c) => { + const hostId = writableHost(c); + await reassigningHost(c); + const id = c.req.param("id"); + const body = await parsed(c, HostReassignRequest); + const { env, session } = c.var.session; + const booking = await reassignBooking( + env, + id, + { + changes: [{ from_host: hostId, to_host: body.to_host }], + reason_code: "host_unavailable", + ...(body.note ? { note: body.note } : {}), + }, + deps, + sessionActor(session), + // Checked with the booking locked: it may have stopped being theirs since the page loaded. + { fromHosts: new Set([hostId]), namesOnly: true }, + ); + return c.json({ id: booking.id, status: booking.status }); + }); + + // A team page. Whatever it reaches is worked out on each request, since a filter's answer changes. + const managerOf = (c: Context) => { + const { session, env } = c.var.session; + if (session.kind !== "manager") throw notFound(); + return { session, env }; + }; + const writableManager = (c: Context) => { + const manager = managerOf(c); + if (manager.session.readOnly) { + throw new ApiError(403, { + type: "permission_error", + code: "read_only_session", + message: "This session can only read.", + }); + } + return manager; + }; + const teamOf = async (manager: ReturnType) => { + const hosts = await managedHosts(manager.env, manager.session); + return { hosts, ids: hosts.map((host) => host.id), set: new Set(hosts.map((host) => host.id)) }; + }; + const managedEntry = async (manager: ReturnType, id: string) => { + const entry = await getOutOfOffice(manager.env, id, deps); + const team = await teamOf(manager); + if (!team.set.has(entry.host)) throw notFound(); + return { entry, team }; + }; + const managedRun = async (manager: ReturnType, id: string) => { + const team = await teamOf(manager); + await assertManagedRun(manager.env, id, team.ids); + }; + + router.get("/manager/hosts", async (c) => { + const { hosts } = await teamOf(managerOf(c)); + return c.json({ object: "list" as const, data: hosts.map(toManagedHost), has_more: false }); + }); + + router.get("/manager/out_of_office", async (c) => { + const manager = managerOf(c); + const team = await teamOf(manager); + if (team.ids.length === 0) + return c.json({ object: "list" as const, data: [], has_more: false }); + const page = await listOutOfOffice( + manager.env, + { + hostIds: team.ids, + fromMs: (deps.now?.() ?? new Date()).getTime(), + limit: Math.min(Math.max(Number(c.req.query("limit")) || 100, 1), 100), + after: c.req.query("after"), + }, + deps, + ); + const settings = await getSettingsRow(manager.env); + return c.json({ + object: "list" as const, + data: page.data, + has_more: page.hasMore, + reasons: [ + ...OUT_OF_OFFICE_PRESET_REASONS.map((key) => ({ + key, + label: OUT_OF_OFFICE_REASON_LABELS[key], + })), + ...(settings?.outOfOfficeReasons ?? []), + ], + }); + }); + + router.post("/manager/out_of_office", async (c) => { + const manager = writableManager(c); + const team = await teamOf(manager); + const body = await parsed(c, ManagerOutOfOfficeRequest); + const hosts: string[] = []; + for (const ref of body.hosts) { + const host = await resolveHost(db, manager.env, ref); + if (!team.set.has(host.id)) throw notFound(); + hosts.push(host.id); + } + const data = await createOutOfOffice( + manager.env, + { ...body, hosts }, + deps, + sessionActor(manager.session), + manager.session.createdByUserId ? "dashboard" : "manager", + { allowedDelegates: team.ids }, + ); + return c.json({ object: "list" as const, data, has_more: false }, 201); + }); + + router.patch("/manager/out_of_office/:id", async (c) => { + const manager = writableManager(c); + const { entry, team } = await managedEntry(manager, c.req.param("id")); + const updated = await updateOutOfOffice( + manager.env, + entry.id, + await parsed(c, SessionOutOfOfficeChange), + deps, + sessionActor(manager.session), + { hostId: entry.host }, + { allowedDelegates: team.ids, startsNoRun: true }, + ); + return c.json(updated); + }); + + router.delete("/manager/out_of_office/:id", async (c) => { + const manager = writableManager(c); + const { entry } = await managedEntry(manager, c.req.param("id")); + await deleteOutOfOffice(manager.env, entry.id, deps, { hostId: entry.host }); + return c.body(null, 204); + }); + + router.get("/manager/out_of_office/:id/conflicts", async (c) => { + const manager = managerOf(c); + const { entry } = await managedEntry(manager, c.req.param("id")); + const page = await listOutOfOfficeConflicts(manager.env, entry.id, { limit: 100 }, deps, { + hostId: entry.host, + }); + return c.json({ object: "list" as const, data: page.data, has_more: page.hasMore }); + }); + + router.get("/manager/bookings", async (c) => { + const manager = managerOf(c); + const team = await teamOf(manager); + const page = await listManagedBookings(manager.env, team.ids, { + hostId: c.req.query("host"), + bookingId: c.req.query("booking"), + needsCover: c.req.query("needs_cover") === "true", + now: deps.now?.() ?? new Date(), + limit: 100, + }); + return c.json({ object: "list" as const, data: page.data, has_more: page.hasMore }); + }); + + router.get("/manager/bookings/:id/reassign_candidates", async (c) => { + const manager = managerOf(c); + const team = await teamOf(manager); + const id = c.req.param("id"); + const booking = await getBooking(manager.env, id, deps); + if (!booking.hosts.some((hostId) => team.set.has(hostId))) throw notFound(); + const page = await listReassignCandidates( + manager.env, + id, + { + hostIds: team.ids, + ...(c.req.query("from_host") ? { fromHost: c.req.query("from_host") } : {}), + }, + deps, + ); + return c.json({ object: "list" as const, data: page.data, has_more: page.hasMore }); + }); + + router.post("/manager/bookings/:id/reassign", async (c) => { + const manager = writableManager(c); + const team = await teamOf(manager); + const booking = await reassignBooking( + manager.env, + c.req.param("id"), + await parsed(c, ReassignBookingRequest), + deps, + sessionActor(manager.session), + { fromHosts: team.set, toHosts: team.set }, + ); + return c.json({ id: booking.id, status: booking.status, hosts: booking.hosts }); + }); + + router.post("/manager/reassignments/preview", async (c) => { + const manager = managerOf(c); + const team = await teamOf(manager); + return c.json( + await previewReassignment(manager.env, await parsed(c, PreviewReassignmentRequest), deps, { + onlyHosts: team.set, + }), + ); + }); + + router.post("/manager/reassignments", async (c) => { + const manager = writableManager(c); + const team = await teamOf(manager); + const { reassignment } = await createReassignment( + manager.env, + await parsed(c, CreateReassignmentRequest), + deps, + sessionActor(manager.session), + { onlyHosts: team.set, idempotencyKey: c.req.header("Idempotency-Key") }, + ); + return c.json(reassignment, 202); + }); + + router.get("/manager/reassignments", async (c) => { + const manager = managerOf(c); + const team = await teamOf(manager); + const page = await listReassignments(manager.env, { limit: 50, hostIds: team.ids }); + return c.json({ object: "list" as const, data: page.data, has_more: page.hasMore }); + }); + + router.get("/manager/reassignments/:id", async (c) => { + const manager = managerOf(c); + await managedRun(manager, c.req.param("id")); + return c.json(await getReassignment(manager.env, c.req.param("id"))); + }); + + router.get("/manager/reassignments/:id/items", async (c) => { + const manager = managerOf(c); + await managedRun(manager, c.req.param("id")); + const page = await listReassignmentItems(manager.env, c.req.param("id"), { limit: 100 }); + return c.json({ object: "list" as const, data: page.data, has_more: page.hasMore }); + }); + + router.post("/manager/reassignments/:id/cancel", async (c) => { + const manager = writableManager(c); + await managedRun(manager, c.req.param("id")); + return c.json(await cancelReassignment(manager.env, c.req.param("id"), deps)); + }); + + router.post("/manager/reassignments/:id/undo", async (c) => { + const manager = writableManager(c); + const team = await teamOf(manager); + await assertManagedRun(manager.env, c.req.param("id"), team.ids); + return c.json( + await undoReassignment( + manager.env, + c.req.param("id"), + await parsed(c, UndoReassignmentRequest), + deps, + sessionActor(manager.session), + { onlyHosts: team.set }, + ), + 202, + ); + }); + const hostsOwnBooking = async (c: Context, hostId: string, id: string) => { const booking = await getBooking(c.var.session.env, id, deps); if (!booking.hosts.includes(hostId)) throw notFound(); diff --git a/apps/api/src/routes/scheduling.ts b/apps/api/src/routes/scheduling.ts index 9d9aaaac7..3a2a85e71 100644 --- a/apps/api/src/routes/scheduling.ts +++ b/apps/api/src/routes/scheduling.ts @@ -3,11 +3,13 @@ * bookings, holds and series. Tenancy comes only from the API key. */ +import { db } from "@coss/db"; import { approveBooking, type BookingActor, type CalendarDeps, cancelBooking, + cancelReassignment, cancelSeries, confirmHold, connectCaldav, @@ -17,15 +19,21 @@ import { createCalendarDelegation, createEventType, createHold, + createHolidayCalendar, createHost, createHostSession, + createManagerSession, createManageSession, + createOutOfOffice, + createReassignment, createSchedule, createSeries, declineBooking, deleteCalendarDelegation, deleteHold, + deleteHolidayCalendar, deleteHost, + deleteOutOfOffice, deleteSchedule, disconnectCalendar, eraseAttendee, @@ -34,7 +42,10 @@ import { getBooking, getCalendarDelegation, getEventType, + getHolidayCalendar, getHost, + getOutOfOffice, + getReassignment, getSchedule, getSettings, getSettingsRow, @@ -45,23 +56,37 @@ import { listConnections, listEventTypeSlots, listEventTypes, + listHolidayCalendars, + listHolidays, listHosts, + listOutOfOffice, + listOutOfOfficeConflicts, + listReassignCandidates, + listReassignmentItems, + listReassignments, listSchedules, markNoShow, + previewReassignment, previewSeries, + putOutOfOffice, + reassignBooking, requireRouting, rescheduleBooking, + resolveHost, type SchedulingDeps, SchedulingError, searchAvailability, setSimulatedBusy, startCalendarAuthorization, toConnectionObject, + undoReassignment, updateBookingLocation, updateCalendar, updateCalendarDelegation, updateEventType, + updateHolidayCalendar, updateHost, + updateOutOfOffice, updateSchedule, updateSettings, verifyCalendarDelegation, @@ -83,16 +108,22 @@ import { CancelledSeries, CancelSeriesRequest, ConfirmHoldRequest, + ConflictsQuery, CreateBookingRequest, CreateCalendarConnectionRequest, CreateCalendarDelegationRequest, CreateEventTypeRequest, CreateHoldRequest, + CreateHolidayCalendarRequest, CreateHostRequest, + CreateOutOfOfficeRequest, + CreateReassignmentRequest, CreateSeriesRequest, DeletedCalendarConnection, DeletedCalendarDelegation, + DeletedHolidayCalendar, DeletedHost, + DeletedOutOfOffice, DeletedSchedule, EraseAttendeeByEmailRequest, ErasedAttendee, @@ -100,11 +131,27 @@ import { EventTypeList, EventTypeObject, ExplainSlotQuery, + HolidayCalendarList, + HolidayCalendarObject, + HolidayList, HostList, HostObject, ListBookingEventsQuery, ListBookingsQuery, + ListOutOfOfficeQuery, + OutOfOfficeConflictList, + OutOfOfficeList, + OutOfOfficeObject, PageQuery, + PreviewReassignmentRequest, + PutOutOfOfficeRequest, + ReassignBookingRequest, + ReassignCandidateList, + ReassignCandidatesQuery, + ReassignmentItemList, + ReassignmentList, + ReassignmentObject, + ReassignmentPreview, RescheduleBookingRequest, ScheduleList, ScheduleObject, @@ -115,17 +162,22 @@ import { SlotExplanation, SlotList, SlotsQuery, + UndoReassignmentRequest, UpdateBookingLocationRequest, UpdateCalendarDelegationRequest, UpdateCalendarRequest, UpdateEventTypeRequest, + UpdateHolidayCalendarRequest, UpdateHostRequest, + UpdateOutOfOfficeRequest, UpdateSettingsRequest, UpsertScheduleRequest, VerifyCalendarDelegationRequest, } from "@coss/scheduling/schemas"; +import { apiKeyHasPermissions } from "@coss/tenancy"; import { secretsKeyRing } from "@coss/webhooks"; import { createRoute, OpenAPIHono } from "@hono/zod-openapi"; +import type { Context } from "hono"; import { createMiddleware } from "hono/factory"; import { z } from "zod"; import { recordApiAudit } from "../audit.ts"; @@ -145,6 +197,23 @@ const TAG_BOOKINGS = "Scheduling bookings"; const TAG_SERIES = "Scheduling series"; const TAG_SETTINGS = "Scheduling settings"; const TAG_CALENDARS = "Scheduling calendars"; +const TAG_OUT_OF_OFFICE = "Scheduling out of office"; +const TAG_REASSIGNMENTS = "Scheduling reassignments"; +const TAG_HOLIDAY_CALENDARS = "Scheduling holiday calendars"; + +/** + * A key that can only create can't reassign, and something it creates that moves bookings is + * reassigning them, later and from the worker, where nothing would ask. + */ +async function requireMovingBookings(c: Context, existing: string | undefined) { + if (existing !== "reassign") return; + if (await apiKeyHasPermissions(c.var.auth.apiKeyId, { scheduling: ["update"] })) return; + throw new ApiError(403, { + type: "permission_error", + code: "insufficient_permissions", + message: "Moving existing bookings needs the scheduling:update permission.", + }); +} const read = () => [requireApiKey({ scheduling: ["read"] }), rateLimit("api.read")]; const writing = ( @@ -187,6 +256,12 @@ const DelegationParams = z.object({ id: param("The delegation's id.", "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m"), }); const AttendeeParams = z.object({ uid: param("Your id for the attendee.", "patient_77") }); +const OutOfOfficeParams = z.object({ + id: param("The entry's id.", "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m"), +}); +const ExternalOutOfOfficeParams = z.object({ + external_id: param("Your own id for the entry.", "hr_leave_4471"), +}); const bodyErrors = { 400: errorDoc("The body is not valid JSON (`invalid_json`)."), @@ -246,6 +321,14 @@ const BookingIdempotency = idempotencyHeaders( "booking instead of creating another, for 24 hours. A refused request keeps no key.", ); +const ReassignmentParams = z.object({ + id: z.string().meta({ description: "The reassignment's id." }), +}); +const ReassignmentIdempotency = idempotencyHeaders( + "Makes retries safe: a repeated request with the same key and body returns the original run " + + "instead of starting another, for 24 hours.", +); + const createdBooking = (description: string, schema: z.ZodType = BookingObject) => ({ description, headers: { "X-Request-Id": requestIdHeader, "Idempotent-Replayed": idempotentReplayedHeader }, @@ -1310,6 +1393,23 @@ const CreateHostSessionRequest = z }) .meta({ id: "CreateSchedulingHostSessionRequest", examples: [{ expires_in: 3600 }] }); +const CreateManagerSessionRequest = z + .strictObject({ + hosts: z.array(z.string()).min(1).max(1000).optional().meta({ + description: "The hosts this page can reach, by id or uid.", + }), + host_filter: z + .record(z.string(), z.union([z.string(), z.array(z.string())])) + .optional() + .meta({ description: "Everyone matching these attributes, asked again on every request." }), + expires_in: SessionExpiry, + read_only: z.boolean().optional(), + }) + .meta({ + id: "CreateSchedulingManagerSessionRequest", + examples: [{ host_filter: { team: "cardiology" }, expires_in: 3600 }], + }); + const CreateBookerSessionRequest = z .strictObject({ event_type: z.string().meta({ description: "The event type's id or uid." }), @@ -1358,6 +1458,31 @@ export const createHostSessionRoute = createRoute({ }, }); +export const createManagerSessionRoute = createRoute({ + method: "post", + path: "/scheduling/manager_sessions", + operationId: "createSchedulingManagerSession", + tags: [TAG_HOSTS], + summary: "Create a manager session", + description: + "Lets a team lead manage cover for the hosts you name, or those matching an attribute filter whenever it's asked: time off, bookings that need someone else, and moving them. It never sees attendees' details. Send the `url` to their browser, or embed it with `coss/scheduling`.", + security: [{ bearerAuth: [] }], + middleware: writing("update", "POST /v1/scheduling/manager_sessions"), + request: { + body: { + required: true, + content: { "application/json": { schema: CreateManagerSessionRequest } }, + }, + }, + responses: { + 201: jsonDoc("The session.", CreatedSessionBody), + ...commonErrors, + ...bodyErrors, + 404: notFound, + 422: invalid, + }, +}); + export const createBookerSessionRoute = createRoute({ method: "post", path: "/scheduling/booker_sessions", @@ -1403,6 +1528,449 @@ export const createManageSessionRoute = createRoute({ }, }); +export const reassignBookingRoute = createRoute({ + method: "post", + path: "/scheduling/bookings/{id}/reassign", + operationId: "reassignSchedulingBooking", + tags: [TAG_BOOKINGS], + summary: "Reassign a booking", + description: + "Gives a booking to another host, keeping its id and time. Works for every kind: a group session moves as a whole, from any of its seats, and one that's already started changes for the record, with nobody told. Whoever leaves and joins is told, and the attendee unless you say not to.", + security: [{ bearerAuth: [] }], + middleware: writing("update", "POST /v1/scheduling/bookings/{id}/reassign"), + request: { + params: BookingParams, + body: { required: true, content: { "application/json": { schema: ReassignBookingRequest } } }, + }, + responses: { + 200: jsonDoc("The booking.", BookingObject), + ...commonErrors, + ...bodyErrors, + 404: notFound, + 422: errorDoc( + "The request doesn't make sense (`invalid_reassignment`), that host can't take it " + + "(`reassign_target_unavailable`), nobody can (`no_reassign_candidate`), or the booking can't change (`booking_not_changeable`, `hold_expired`).", + ), + }, +}); + +export const listReassignCandidatesRoute = createRoute({ + method: "get", + path: "/scheduling/bookings/{id}/reassign_candidates", + operationId: "listSchedulingReassignCandidates", + tags: [TAG_BOOKINGS], + summary: "List who a booking could go to", + description: + "The environment's other active hosts: members of the event type first, in the order a pool would pick them, each saying whether they're free and why not. Things can change before you ask: reassigning decides again.", + security: [{ bearerAuth: [] }], + middleware: read(), + request: { params: BookingParams, query: ReassignCandidatesQuery }, + responses: { + 200: jsonDoc("The candidates.", ReassignCandidateList), + ...commonErrors, + 404: notFound, + 422: invalid, + }, +}); + +export const previewReassignmentRoute = createRoute({ + method: "post", + path: "/scheduling/reassignments/preview", + operationId: "previewSchedulingReassignment", + tags: [TAG_REASSIGNMENTS], + summary: "Preview a bulk reassignment", + description: + "Who each booking would go to, and why not when nobody can take it, without moving anything. It works down them in order, so two can't both be promised the same free hour. Change any of it and start the run with `assignments`. Covers the first 500. Things can change before you start it: the run decides again.", + security: [{ bearerAuth: [] }], + middleware: read(), + request: { + body: { + required: true, + content: { "application/json": { schema: PreviewReassignmentRequest } }, + }, + }, + responses: { + 200: jsonDoc("What would happen.", ReassignmentPreview), + ...commonErrors, + ...bodyErrors, + 404: notFound, + 422: errorDoc("The selection or strategy doesn't make sense (`invalid_reassignment`)."), + }, +}); + +export const createReassignmentRoute = createRoute({ + method: "post", + path: "/scheduling/reassignments", + operationId: "createSchedulingReassignment", + tags: [TAG_REASSIGNMENTS], + summary: "Start a bulk reassignment", + description: + "Moves up to 5,000 bookings in the background, each on its own, so one that can't move never holds up the rest. Nothing's ever cancelled: a booking nobody can take stays where it is, and its item says so. It answers at once: follow the run by its id, or wait for `scheduling.reassignment.completed`.", + security: [{ bearerAuth: [] }], + middleware: writing("update", "POST /v1/scheduling/reassignments"), + request: { + headers: ReassignmentIdempotency, + body: { + required: true, + content: { "application/json": { schema: CreateReassignmentRequest } }, + }, + }, + responses: { + 202: createdBooking("The run, waiting to start.", ReassignmentObject), + ...commonErrors, + ...bodyErrors, + 404: notFound, + 409: errorDoc("A request with this key is still running (`idempotency_in_progress`)."), + 422: errorDoc( + "The selection or strategy doesn't make sense, or covers too many bookings (`invalid_reassignment`).", + ), + }, +}); + +export const listReassignmentsRoute = createRoute({ + method: "get", + path: "/scheduling/reassignments", + operationId: "listSchedulingReassignments", + tags: [TAG_REASSIGNMENTS], + summary: "List bulk reassignments", + description: "Newest first, with how each is going.", + security: [{ bearerAuth: [] }], + middleware: read(), + request: { query: PageQuery }, + responses: { + 200: jsonDoc("The runs.", ReassignmentList), + ...commonErrors, + 422: invalid, + }, +}); + +export const getReassignmentRoute = createRoute({ + method: "get", + path: "/scheduling/reassignments/{id}", + operationId: "getSchedulingReassignment", + tags: [TAG_REASSIGNMENTS], + summary: "Retrieve a bulk reassignment", + description: "How it's going.", + security: [{ bearerAuth: [] }], + middleware: read(), + request: { params: ReassignmentParams }, + responses: { + 200: jsonDoc("The run.", ReassignmentObject), + ...commonErrors, + 404: notFound, + }, +}); + +export const listReassignmentItemsRoute = createRoute({ + method: "get", + path: "/scheduling/reassignments/{id}/items", + operationId: "listSchedulingReassignmentItems", + tags: [TAG_REASSIGNMENTS], + summary: "List a bulk reassignment's bookings", + description: + "Each booking it covers and what happened to it. A group session has one, for one of its seats. Empty until the worker's started on it.", + security: [{ bearerAuth: [] }], + middleware: read(), + request: { params: ReassignmentParams, query: PageQuery }, + responses: { + 200: jsonDoc("The items.", ReassignmentItemList), + ...commonErrors, + 404: notFound, + 422: invalid, + }, +}); + +export const cancelReassignmentRoute = createRoute({ + method: "post", + path: "/scheduling/reassignments/{id}/cancel", + operationId: "cancelSchedulingReassignment", + tags: [TAG_REASSIGNMENTS], + summary: "Cancel a bulk reassignment", + description: + "Stops what hasn't happened yet. What's moved stays moved: undo it for that. Cancelling one that's finished changes nothing.", + security: [{ bearerAuth: [] }], + middleware: writing("update", "POST /v1/scheduling/reassignments/{id}/cancel"), + request: { params: ReassignmentParams }, + responses: { + 200: jsonDoc("The run.", ReassignmentObject), + ...commonErrors, + 404: notFound, + }, +}); + +export const undoReassignmentRoute = createRoute({ + method: "post", + path: "/scheduling/reassignments/{id}/undo", + operationId: "undoSchedulingReassignment", + tags: [TAG_REASSIGNMENTS], + summary: "Undo a bulk reassignment", + description: + "Gives back what a finished run moved, as a new run. A booking that's moved on since stays where it went, and a former host who's no longer free keeps it from going back, unless you pass `overrides`.", + security: [{ bearerAuth: [] }], + middleware: writing("update", "POST /v1/scheduling/reassignments/{id}/undo"), + request: { + params: ReassignmentParams, + body: { + required: true, + content: { "application/json": { schema: UndoReassignmentRequest } }, + }, + }, + responses: { + 202: jsonDoc("The new run.", ReassignmentObject), + ...commonErrors, + ...bodyErrors, + 404: notFound, + 422: errorDoc("It's still running (`reassignment_not_finished`)."), + }, +}); + +const HolidayCalendarParams = z.object({ + id: z.string().meta({ description: "The holiday calendar's id." }), +}); + +export const createHolidayCalendarRoute = createRoute({ + method: "post", + path: "/scheduling/holiday_calendars", + operationId: "createSchedulingHolidayCalendar", + tags: [TAG_HOLIDAY_CALENDARS], + summary: "Create a holiday calendar", + description: + "Applies a country's public holidays, or your own feed, to some hosts as ordinary out of office entries, kept up to date by coss.com. A host who removes one keeps it removed.", + security: [{ bearerAuth: [] }], + middleware: writing("create", "POST /v1/scheduling/holiday_calendars"), + request: { + body: { + required: true, + content: { "application/json": { schema: CreateHolidayCalendarRequest } }, + }, + }, + responses: { + 201: jsonDoc("The calendar.", HolidayCalendarObject), + ...commonErrors, + ...bodyErrors, + 404: notFound, + 422: errorDoc("That isn't something to read (`invalid_holiday_calendar`)."), + }, +}); + +export const listHolidayCalendarsRoute = createRoute({ + method: "get", + path: "/scheduling/holiday_calendars", + operationId: "listSchedulingHolidayCalendars", + tags: [TAG_HOLIDAY_CALENDARS], + summary: "List holiday calendars", + description: "By name. A feed's URL is never shown again.", + security: [{ bearerAuth: [] }], + middleware: read(), + responses: { 200: jsonDoc("The calendars.", HolidayCalendarList), ...commonErrors }, +}); + +export const getHolidayCalendarRoute = createRoute({ + method: "get", + path: "/scheduling/holiday_calendars/{id}", + operationId: "getSchedulingHolidayCalendar", + tags: [TAG_HOLIDAY_CALENDARS], + summary: "Retrieve a holiday calendar", + description: "With when it was last read, and why not if it couldn't be.", + security: [{ bearerAuth: [] }], + middleware: read(), + request: { params: HolidayCalendarParams }, + responses: { + 200: jsonDoc("The calendar.", HolidayCalendarObject), + ...commonErrors, + 404: notFound, + }, +}); + +export const listHolidaysRoute = createRoute({ + method: "get", + path: "/scheduling/holiday_calendars/{id}/holidays", + operationId: "listSchedulingHolidays", + tags: [TAG_HOLIDAY_CALENDARS], + summary: "List a calendar's holidays", + description: "What's coming, with names, as last read. Empty until its first read.", + security: [{ bearerAuth: [] }], + middleware: read(), + request: { params: HolidayCalendarParams }, + responses: { 200: jsonDoc("The holidays.", HolidayList), ...commonErrors, 404: notFound }, +}); + +export const updateHolidayCalendarRoute = createRoute({ + method: "patch", + path: "/scheduling/holiday_calendars/{id}", + operationId: "updateSchedulingHolidayCalendar", + tags: [TAG_HOLIDAY_CALENDARS], + summary: "Update a holiday calendar", + description: "Who it applies to, who covers and what happens to bookings. Takes effect shortly.", + security: [{ bearerAuth: [] }], + middleware: writing("update", "PATCH /v1/scheduling/holiday_calendars/{id}"), + request: { + params: HolidayCalendarParams, + body: { + required: true, + content: { "application/json": { schema: UpdateHolidayCalendarRequest } }, + }, + }, + responses: { + 200: jsonDoc("The calendar.", HolidayCalendarObject), + ...commonErrors, + ...bodyErrors, + 404: notFound, + 422: invalid, + }, +}); + +export const deleteHolidayCalendarRoute = createRoute({ + method: "delete", + path: "/scheduling/holiday_calendars/{id}", + operationId: "deleteSchedulingHolidayCalendar", + tags: [TAG_HOLIDAY_CALENDARS], + summary: "Delete a holiday calendar", + description: "Hosts can be booked on its days again, shortly: past entries stay.", + security: [{ bearerAuth: [] }], + middleware: writing("delete", "DELETE /v1/scheduling/holiday_calendars/{id}"), + request: { params: HolidayCalendarParams }, + responses: { + 200: jsonDoc("It's going.", DeletedHolidayCalendar), + ...commonErrors, + 404: notFound, + }, +}); + +export const createOutOfOfficeRoute = createRoute({ + method: "post", + path: "/scheduling/out_of_office", + operationId: "createSchedulingOutOfOffice", + tags: [TAG_OUT_OF_OFFICE], + summary: "Create out of office entries", + description: + "Marks one host, a list of hosts or everyone matching some attributes as away, for part of a " + + "day or for whole dates. They aren't offered during it. Existing bookings stay as they are: " + + "list them with the entry's conflicts.", + security: [{ bearerAuth: [] }], + middleware: writing("create", "POST /v1/scheduling/out_of_office"), + request: { + body: { required: true, content: { "application/json": { schema: CreateOutOfOfficeRequest } } }, + }, + responses: { + 201: jsonDoc("An entry per host.", OutOfOfficeList), + ...commonErrors, + ...bodyErrors, + 404: notFound, + 422: invalid, + }, +}); + +export const listOutOfOfficeRoute = createRoute({ + method: "get", + path: "/scheduling/out_of_office", + operationId: "listSchedulingOutOfOffice", + tags: [TAG_OUT_OF_OFFICE], + summary: "List out of office entries", + description: "The environment's entries, newest first.", + security: [{ bearerAuth: [] }], + middleware: read(), + request: { query: ListOutOfOfficeQuery }, + responses: { + 200: jsonDoc("A page of entries.", OutOfOfficeList), + ...commonErrors, + 404: notFound, + 422: invalid, + }, +}); + +export const getOutOfOfficeRoute = createRoute({ + method: "get", + path: "/scheduling/out_of_office/{id}", + operationId: "getSchedulingOutOfOffice", + tags: [TAG_OUT_OF_OFFICE], + summary: "Retrieve an out of office entry", + description: "One entry.", + security: [{ bearerAuth: [] }], + middleware: read(), + request: { params: OutOfOfficeParams }, + responses: { 200: jsonDoc("The entry.", OutOfOfficeObject), ...commonErrors, 404: notFound }, +}); + +export const updateOutOfOfficeRoute = createRoute({ + method: "patch", + path: "/scheduling/out_of_office/{id}", + operationId: "updateSchedulingOutOfOffice", + tags: [TAG_OUT_OF_OFFICE], + summary: "Update an out of office entry", + description: "Changes what's given. An entry stays with its host.", + security: [{ bearerAuth: [] }], + middleware: writing("update", "PATCH /v1/scheduling/out_of_office/{id}"), + request: { + params: OutOfOfficeParams, + body: { required: true, content: { "application/json": { schema: UpdateOutOfOfficeRequest } } }, + }, + responses: { + 200: jsonDoc("The entry.", OutOfOfficeObject), + ...commonErrors, + ...bodyErrors, + 404: notFound, + 422: invalid, + }, +}); + +export const putOutOfOfficeRoute = createRoute({ + method: "put", + path: "/scheduling/out_of_office/by_external_id/{external_id}", + operationId: "putSchedulingOutOfOffice", + tags: [TAG_OUT_OF_OFFICE], + summary: "Create or replace an entry by your own id", + description: + "For syncing from a system of record such as an HR tool: send what it says, as often as you " + + "like, and there's one entry for it. Give every field you want to keep.", + security: [{ bearerAuth: [] }], + middleware: writing("update", "PUT /v1/scheduling/out_of_office/by_external_id/{external_id}"), + request: { + params: ExternalOutOfOfficeParams, + body: { required: true, content: { "application/json": { schema: PutOutOfOfficeRequest } } }, + }, + responses: { + 200: jsonDoc("The entry, replaced.", OutOfOfficeObject), + 201: jsonDoc("The entry, new.", OutOfOfficeObject), + ...commonErrors, + ...bodyErrors, + 404: notFound, + 422: invalid, + }, +}); + +export const deleteOutOfOfficeRoute = createRoute({ + method: "delete", + path: "/scheduling/out_of_office/{id}", + operationId: "deleteSchedulingOutOfOffice", + tags: [TAG_OUT_OF_OFFICE], + summary: "Delete an out of office entry", + description: "The host can be booked in that time again, and any calendar event for it goes.", + security: [{ bearerAuth: [] }], + middleware: acting("delete"), + request: { params: OutOfOfficeParams }, + responses: { 200: jsonDoc("Deleted.", DeletedOutOfOffice), ...commonErrors, 404: notFound }, +}); + +export const listOutOfOfficeConflictsRoute = createRoute({ + method: "get", + path: "/scheduling/out_of_office/{id}/conflicts", + operationId: "listSchedulingOutOfOfficeConflicts", + tags: [TAG_OUT_OF_OFFICE], + summary: "List an entry's conflicts", + description: + "Live bookings of the host that fall in the entry, soonest first. Entries never change them.", + security: [{ bearerAuth: [] }], + middleware: read(), + request: { params: OutOfOfficeParams, query: ConflictsQuery }, + responses: { + 200: jsonDoc("The bookings.", OutOfOfficeConflictList), + ...commonErrors, + 404: notFound, + 422: invalid, + }, +}); + export const getSettingsRoute = createRoute({ method: "get", path: "/scheduling/settings", @@ -1550,6 +2118,247 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA return c.json({ object: "scheduling.host" as const, id, deleted: true as const }, 200); }); + router.openapi(reassignBookingRoute, async (c) => { + const booking = await reassignBooking( + c.var.auth, + c.req.valid("param").id, + c.req.valid("json"), + deps, + actorOf(c.var.auth), + ); + await recordApiAudit(c, { + action: "scheduling_booking.reassigned", + target: { type: "scheduling_booking", id: booking.id }, + }); + return c.json(booking, 200); + }); + + router.openapi(listReassignCandidatesRoute, async (c) => { + const query = c.req.valid("query"); + const fromHost = query.from_host + ? (await resolveHost(db, c.var.auth, query.from_host)).id + : undefined; + const page = await listReassignCandidates( + c.var.auth, + c.req.valid("param").id, + { fromHost }, + deps, + ); + return c.json({ object: "list" as const, data: page.data, has_more: page.hasMore }, 200); + }); + + router.openapi(previewReassignmentRoute, async (c) => + c.json(await previewReassignment(c.var.auth, c.req.valid("json"), deps), 200), + ); + + router.openapi( + createReassignmentRoute, + async (c) => { + const { reassignment, replayed } = await createReassignment( + c.var.auth, + c.req.valid("json"), + deps, + actorOf(c.var.auth), + { idempotencyKey: c.req.valid("header")["Idempotency-Key"] }, + ); + if (replayed) c.header("Idempotent-Replayed", "true"); + else { + await recordApiAudit(c, { + action: "scheduling_reassignment.created", + target: { type: "scheduling_reassignment", id: reassignment.id }, + }); + } + return c.json(reassignment, 202); + }, + (result) => { + idempotencyHook(result); + }, + ); + + router.openapi(listReassignmentsRoute, async (c) => { + const page = await listReassignments(c.var.auth, c.req.valid("query")); + return c.json({ object: "list" as const, data: page.data, has_more: page.hasMore }, 200); + }); + + router.openapi(getReassignmentRoute, async (c) => + c.json(await getReassignment(c.var.auth, c.req.valid("param").id), 200), + ); + + router.openapi(listReassignmentItemsRoute, async (c) => { + const page = await listReassignmentItems( + c.var.auth, + c.req.valid("param").id, + c.req.valid("query"), + ); + return c.json({ object: "list" as const, data: page.data, has_more: page.hasMore }, 200); + }); + + router.openapi(cancelReassignmentRoute, async (c) => { + const reassignment = await cancelReassignment(c.var.auth, c.req.valid("param").id, deps); + await recordApiAudit(c, { + action: "scheduling_reassignment.cancelled", + target: { type: "scheduling_reassignment", id: reassignment.id }, + }); + return c.json(reassignment, 200); + }); + + router.openapi(undoReassignmentRoute, async (c) => { + const reassignment = await undoReassignment( + c.var.auth, + c.req.valid("param").id, + c.req.valid("json"), + deps, + actorOf(c.var.auth), + ); + await recordApiAudit(c, { + action: "scheduling_reassignment.undone", + target: { type: "scheduling_reassignment", id: reassignment.id }, + }); + return c.json(reassignment, 202); + }); + + router.openapi(createHolidayCalendarRoute, async (c) => { + await requireMovingBookings(c, c.req.valid("json").existing_bookings); + const calendar = await createHolidayCalendar(c.var.auth, c.req.valid("json"), deps); + await recordApiAudit(c, { + action: "scheduling_holiday_calendar.created", + target: { type: "scheduling_holiday_calendar", id: calendar.id }, + }); + return c.json(calendar, 201); + }); + + router.openapi(listHolidayCalendarsRoute, async (c) => { + const page = await listHolidayCalendars(c.var.auth); + return c.json({ object: "list" as const, data: page.data, has_more: page.hasMore }, 200); + }); + + router.openapi(getHolidayCalendarRoute, async (c) => + c.json(await getHolidayCalendar(c.var.auth, c.req.valid("param").id), 200), + ); + + router.openapi(listHolidaysRoute, async (c) => + c.json( + { + object: "list" as const, + data: await listHolidays(c.var.auth, c.req.valid("param").id, deps), + has_more: false, + }, + 200, + ), + ); + + router.openapi(updateHolidayCalendarRoute, async (c) => { + const calendar = await updateHolidayCalendar( + c.var.auth, + c.req.valid("param").id, + c.req.valid("json"), + deps, + ); + await recordApiAudit(c, { + action: "scheduling_holiday_calendar.updated", + target: { type: "scheduling_holiday_calendar", id: calendar.id }, + }); + return c.json(calendar, 200); + }); + + router.openapi(deleteHolidayCalendarRoute, async (c) => { + const removed = await deleteHolidayCalendar(c.var.auth, c.req.valid("param").id, deps); + await recordApiAudit(c, { + action: "scheduling_holiday_calendar.deleted", + target: { type: "scheduling_holiday_calendar", id: removed.id }, + }); + return c.json(removed, 200); + }); + + router.openapi(createOutOfOfficeRoute, async (c) => { + await requireMovingBookings(c, c.req.valid("json").existing_bookings); + const data = await createOutOfOffice( + c.var.auth, + c.req.valid("json"), + deps, + actorOf(c.var.auth), + "api", + ); + for (const entry of data) { + await recordApiAudit(c, { + action: "scheduling_out_of_office.created", + target: { type: "scheduling_out_of_office", id: entry.id }, + }); + } + return c.json({ object: "list" as const, data, has_more: false }, 201); + }); + + router.openapi(listOutOfOfficeRoute, async (c) => { + const query = c.req.valid("query"); + const host = query.host ? await resolveHost(db, c.var.auth, query.host) : null; + const page = await listOutOfOffice( + c.var.auth, + { + hostIds: host ? [host.id] : undefined, + fromMs: query.start_after ? instant(query.start_after, "start_after") : undefined, + toMs: query.start_before ? instant(query.start_before, "start_before") : undefined, + group: query.group, + limit: query.limit, + after: query.after, + }, + deps, + ); + return c.json({ object: "list" as const, data: page.data, has_more: page.hasMore }, 200); + }); + + router.openapi(getOutOfOfficeRoute, async (c) => + c.json(await getOutOfOffice(c.var.auth, c.req.valid("param").id, deps), 200), + ); + + router.openapi(updateOutOfOfficeRoute, async (c) => { + const entry = await updateOutOfOffice( + c.var.auth, + c.req.valid("param").id, + c.req.valid("json"), + deps, + actorOf(c.var.auth), + ); + await recordApiAudit(c, { + action: "scheduling_out_of_office.updated", + target: { type: "scheduling_out_of_office", id: entry.id }, + }); + return c.json(entry, 200); + }); + + router.openapi(putOutOfOfficeRoute, async (c) => { + const { entry, created } = await putOutOfOffice( + c.var.auth, + c.req.valid("param").external_id, + c.req.valid("json"), + deps, + actorOf(c.var.auth), + ); + await recordApiAudit(c, { + action: created ? "scheduling_out_of_office.created" : "scheduling_out_of_office.updated", + target: { type: "scheduling_out_of_office", id: entry.id }, + }); + return created ? c.json(entry, 201) : c.json(entry, 200); + }); + + router.openapi(deleteOutOfOfficeRoute, async (c) => { + const { id } = await deleteOutOfOffice(c.var.auth, c.req.valid("param").id, deps); + await recordApiAudit(c, { + action: "scheduling_out_of_office.deleted", + target: { type: "scheduling_out_of_office", id }, + }); + return c.json({ object: "scheduling.out_of_office" as const, id, deleted: true as const }, 200); + }); + + router.openapi(listOutOfOfficeConflictsRoute, async (c) => { + const page = await listOutOfOfficeConflicts( + c.var.auth, + c.req.valid("param").id, + c.req.valid("query"), + deps, + ); + return c.json({ object: "list" as const, data: page.data, has_more: page.hasMore }, 200); + }); + router.openapi(createScheduleRoute, async (c) => c.json(await createSchedule(c.var.auth, c.req.valid("param").host, c.req.valid("json")), 201), ); @@ -2126,6 +2935,20 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA expires_at: session.expiresAt.toISOString(), }); + router.openapi(createManagerSessionRoute, async (c) => { + const body = c.req.valid("json"); + const session = await createManagerSession(c.var.auth, { + plans: deps.plans, + hosts: body.hosts, + hostFilter: body.host_filter, + expiresIn: body.expires_in, + readOnly: body.read_only, + apiKeyId: c.var.auth.apiKeyId, + publicUrl: deps.publicUrl ?? null, + }); + return c.json(sessionBody(session), 201); + }); + router.openapi(createHostSessionRoute, async (c) => { const body = c.req.valid("json"); const session = await createHostSession(c.var.auth, { diff --git a/apps/api/src/webhook-events.ts b/apps/api/src/webhook-events.ts index e12592bcd..a5291a830 100644 --- a/apps/api/src/webhook-events.ts +++ b/apps/api/src/webhook-events.ts @@ -14,6 +14,8 @@ import { SchedulingBookingWebhookEvent, SchedulingCalendarDelegationWebhookEvent, SchedulingCalendarWebhookEvent, + SchedulingOutOfOfficeWebhookEvent, + SchedulingReassignmentWebhookEvent, } from "@coss/scheduling/schemas"; import { SignRequestWebhookEvent } from "@coss/sign/schemas"; import { ReceivedSmsWebhookEvent, SmsWebhookEvent } from "@coss/sms/schemas"; @@ -43,6 +45,8 @@ const WEBHOOK_EVENTS = { SchedulingBookingWebhookEvent, SchedulingCalendarDelegationWebhookEvent, SchedulingCalendarWebhookEvent, + SchedulingOutOfOfficeWebhookEvent, + SchedulingReassignmentWebhookEvent, }; export function registerWebhookEvents(app: OpenAPIHono): void { diff --git a/apps/api/test/agent-surface-exemptions.ts b/apps/api/test/agent-surface-exemptions.ts index 16221e3ba..548fa9294 100644 --- a/apps/api/test/agent-surface-exemptions.ts +++ b/apps/api/test/agent-surface-exemptions.ts @@ -76,6 +76,15 @@ export const AGENT_SURFACE_EXEMPTIONS: Readonly> = { "DELETE /v1/scheduling/hosts/{host}/calendar_connections/{id}": REMOVES, "PATCH /v1/scheduling/hosts/{host}/calendars/{calendar_id}": CHANGE, "PUT /v1/scheduling/hosts/{host}/simulated_busy": CHANGE, + "GET /v1/scheduling/out_of_office/{id}": READ, + "PATCH /v1/scheduling/out_of_office/{id}": CHANGE, + "PUT /v1/scheduling/out_of_office/by_external_id/{external_id}": CHANGE, + "POST /v1/scheduling/holiday_calendars": CHANGE, + "GET /v1/scheduling/holiday_calendars": READ, + "GET /v1/scheduling/holiday_calendars/{id}": READ, + "PATCH /v1/scheduling/holiday_calendars/{id}": CHANGE, + "DELETE /v1/scheduling/holiday_calendars/{id}": REMOVES, + "GET /v1/scheduling/holiday_calendars/{id}/holidays": READ, "PATCH /v1/tracking_settings/default": CHANGE, "DELETE /v1/tracking_settings/default": REMOVES, "PATCH /v1/tracking_settings/categories/{category}": CHANGE, diff --git a/apps/api/test/app.test.ts b/apps/api/test/app.test.ts index f5a7dac8b..ad0bfb347 100644 --- a/apps/api/test/app.test.ts +++ b/apps/api/test/app.test.ts @@ -292,6 +292,8 @@ describe("GET /openapi.json", () => { "/v1/scheduling/bookings/{id}/manage_sessions", "/v1/scheduling/bookings/{id}/no_show", "/v1/scheduling/bookings/{id}/notifications", + "/v1/scheduling/bookings/{id}/reassign", + "/v1/scheduling/bookings/{id}/reassign_candidates", "/v1/scheduling/bookings/{id}/reschedule", "/v1/scheduling/calendar_delegations", "/v1/scheduling/calendar_delegations/{id}", @@ -303,6 +305,9 @@ describe("GET /openapi.json", () => { "/v1/scheduling/holds", "/v1/scheduling/holds/{id}", "/v1/scheduling/holds/{id}/confirm", + "/v1/scheduling/holiday_calendars", + "/v1/scheduling/holiday_calendars/{id}", + "/v1/scheduling/holiday_calendars/{id}/holidays", "/v1/scheduling/hosts", "/v1/scheduling/hosts/{host}", "/v1/scheduling/hosts/{host}/calendar_connections", @@ -313,6 +318,17 @@ describe("GET /openapi.json", () => { "/v1/scheduling/hosts/{host}/schedules/{schedule_id}", "/v1/scheduling/hosts/{host}/sessions", "/v1/scheduling/hosts/{host}/simulated_busy", + "/v1/scheduling/manager_sessions", + "/v1/scheduling/out_of_office", + "/v1/scheduling/out_of_office/by_external_id/{external_id}", + "/v1/scheduling/out_of_office/{id}", + "/v1/scheduling/out_of_office/{id}/conflicts", + "/v1/scheduling/reassignments", + "/v1/scheduling/reassignments/preview", + "/v1/scheduling/reassignments/{id}", + "/v1/scheduling/reassignments/{id}/cancel", + "/v1/scheduling/reassignments/{id}/items", + "/v1/scheduling/reassignments/{id}/undo", "/v1/scheduling/series", "/v1/scheduling/series/preview", "/v1/scheduling/series/{id}/cancel", diff --git a/apps/api/test/cli-contract.test.ts b/apps/api/test/cli-contract.test.ts index 13276c7e3..a78578dd1 100644 --- a/apps/api/test/cli-contract.test.ts +++ b/apps/api/test/cli-contract.test.ts @@ -819,6 +819,28 @@ describe("coss-cli against the API", () => { expect(await shown.code).toBe(0); expect(shown.cli.stdout()).toContain("confirmed"); expect(await coss(["scheduling", "calendars", "ada"]).code).toBe(0); + await client.scheduling.hosts.create({ uid: "grace", name: "Grace", time_zone: "UTC" }); + await client.scheduling.schedules.create("grace", { + rules: [{ days: [0, 1, 2, 3, 4, 5, 6], start: "00:00", end: "24:00" }], + }); + const candidates = coss(["scheduling", "candidates", id, "--json"]); + expect(await candidates.code).toBe(0); + expect(lines(candidates.cli.stdout())[0]).toMatchObject({ uid: "grace", member: false }); + expect(await coss(["scheduling", "reassign", id]).code).toBe(2); + expect(await coss(["scheduling", "reassign", id, "--to", "grace"]).code).toBe(1); + const moved = coss([ + "scheduling", + "reassign", + id, + "--to", + "grace", + "--any-host", + "--reason", + "out_of_office", + "--json", + ]); + expect(await moved.code).toBe(0); + expect(await coss(["scheduling", "ooo", "--host", "grace"]).code).toBe(0); expect(await coss(["scheduling", "cancel", id]).code).toBe(0); }); diff --git a/apps/api/test/contract-types.ts b/apps/api/test/contract-types.ts index 5de676183..705500861 100644 --- a/apps/api/test/contract-types.ts +++ b/apps/api/test/contract-types.ts @@ -230,19 +230,35 @@ import type { CreateCalendarDelegationRequest, CreateEventTypeRequest, CreateHoldRequest, + CreateHolidayCalendarRequest, CreateHostRequest, + CreateOutOfOfficeRequest, + CreateReassignmentRequest, CreateSeriesRequest, EraseAttendeeByEmailRequest, ErasedAttendee, ErasedAttendeeByEmail, EventTypeObject, ExplainSlotQuery, + HolidayCalendarObject, HostObject, + ListOutOfOfficeQuery, + OutOfOfficeConflict, + OutOfOfficeObject, + PreviewReassignmentRequest, + PutOutOfOfficeRequest, + ReassignBookingRequest, + ReassignCandidate, + ReassignmentItemObject, + ReassignmentObject, + ReassignmentPreview, RescheduleBookingRequest, ScheduleObject, SchedulingBookingWebhookEvent, SchedulingCalendarDelegationWebhookEvent, SchedulingCalendarWebhookEvent, + SchedulingOutOfOfficeWebhookEvent, + SchedulingReassignmentWebhookEvent, SeriesPreview, SeriesResult, SetSimulatedBusyRequest, @@ -250,11 +266,14 @@ import type { SlotExplanation, SlotList, SlotsQuery, + UndoReassignmentRequest, UpdateBookingLocationRequest, UpdateCalendarDelegationRequest, UpdateCalendarRequest, UpdateEventTypeRequest, + UpdateHolidayCalendarRequest, UpdateHostRequest, + UpdateOutOfOfficeRequest, UpdateSettingsRequest, UpsertScheduleRequest, VerifyCalendarDelegationRequest, @@ -319,7 +338,10 @@ import type { CreateSchedulingCalendarDelegationParams, CreateSchedulingEventTypeParams, CreateSchedulingHoldParams, + CreateSchedulingHolidayCalendarParams, CreateSchedulingHostParams, + CreateSchedulingOutOfOfficeParams, + CreateSchedulingReassignmentParams, CreateSchedulingSeriesParams, CreateSegmentParams, CreateSubaccountParams, @@ -353,6 +375,7 @@ import type { ListReceivedEmailsParams, ListRiskAssessmentsParams, ListRiskListEntriesParams, + ListSchedulingOutOfOfficeParams, ListSchedulingSlotsParams, ListSubaccountsParams, ListSuppressionsParams, @@ -365,7 +388,10 @@ import type { LogDrainLine, LogDrainSource, PollWebhookListenerParams, + PreviewSchedulingReassignmentParams, PublishEmailTemplateParams, + PutSchedulingOutOfOfficeParams, + ReassignSchedulingBookingParams, RecoverWebhookEndpointParams, RenderEmailTemplateParams, RescheduleEmailParams, @@ -383,7 +409,14 @@ import type { SchedulingCalendarDelegation, SchedulingCancelledSeries, SchedulingEventType, + SchedulingHolidayCalendar, SchedulingHost, + SchedulingOutOfOffice, + SchedulingOutOfOfficeConflict, + SchedulingReassignCandidate, + SchedulingReassignment, + SchedulingReassignmentItem, + SchedulingReassignmentPreview, SchedulingSchedule, SchedulingSeries, SchedulingSeriesPreview, @@ -502,6 +535,7 @@ import type { SubaccountStatsParams, TestBroadcastParams, TrackingOptions, + UndoSchedulingReassignmentParams, UpdateBroadcastParams, UpdateContactParams, UpdateContactPropertyParams, @@ -518,7 +552,9 @@ import type { UpdateSchedulingCalendarDelegationParams, UpdateSchedulingCalendarParams, UpdateSchedulingEventTypeParams, + UpdateSchedulingHolidayCalendarParams, UpdateSchedulingHostParams, + UpdateSchedulingOutOfOfficeParams, UpdateSchedulingSettingsParams, UpdateSegmentParams, UpdateStorageSettingsParams, @@ -780,7 +816,9 @@ export type Contract = [ | z.output | z.output | z.output - | z.output, + | z.output + | z.output + | z.output, WebhookEvent > >, @@ -1081,6 +1119,56 @@ export type SubaccountContract = [ Assert, CreateSchedulingHostParams>>, Assert, UpdateSchedulingHostParams>>, Assert, SchedulingHost>>, + Assert< + MutuallyAssignable, CreateSchedulingOutOfOfficeParams> + >, + Assert, PutSchedulingOutOfOfficeParams>>, + Assert< + MutuallyAssignable, UpdateSchedulingOutOfOfficeParams> + >, + Assert, SchedulingOutOfOffice>>, + Assert< + MutuallyAssignable, ReassignSchedulingBookingParams> + >, + Assert, SchedulingReassignCandidate>>, + Assert, SchedulingOutOfOfficeConflict>>, + Assert< + MutuallyAssignable< + z.input, + PreviewSchedulingReassignmentParams + > + >, + Assert< + MutuallyAssignable< + z.input, + CreateSchedulingReassignmentParams + > + >, + Assert< + MutuallyAssignable, UndoSchedulingReassignmentParams> + >, + Assert, SchedulingReassignment>>, + Assert< + MutuallyAssignable< + z.input, + CreateSchedulingHolidayCalendarParams + > + >, + Assert< + MutuallyAssignable< + z.input, + UpdateSchedulingHolidayCalendarParams + > + >, + Assert, SchedulingHolidayCalendar>>, + Assert, SchedulingReassignmentItem>>, + Assert, SchedulingReassignmentPreview>>, + Assert< + MutuallyAssignable< + Omit, "limit">, + Omit + > + >, Assert, UpsertSchedulingScheduleParams>>, Assert, SchedulingSchedule>>, Assert< diff --git a/apps/api/test/mcp-contract.test.ts b/apps/api/test/mcp-contract.test.ts index 47bbbaadb..700fccfc3 100644 --- a/apps/api/test/mcp-contract.test.ts +++ b/apps/api/test/mcp-contract.test.ts @@ -464,6 +464,47 @@ describe("coss-mcp against the API", () => { false, ); expect((await call("create_scheduling_session", { event_type: "intro" })).isError).toBe(false); + + await call("save_scheduling_host", { uid: "grace", name: "Grace", time_zone: "UTC" }); + await call("save_scheduling_schedule", { + host: "grace", + rules: [{ days: [0, 1, 2, 3, 4, 5, 6], start: "00:00", end: "24:00" }], + }); + const candidates = await call("list_scheduling_reassign_candidates", { + booking_id: booked.data.id, + }); + expect(candidates.data.candidates[0]).toMatchObject({ uid: "grace", member: false }); + const refused = await call("reassign_scheduling_booking", { + booking_id: booked.data.id, + to_host: "grace", + reason_code: "other", + }); + expect(refused.isError).toBe(true); + const reassigned = await call("reassign_scheduling_booking", { + booking_id: booked.data.id, + to_host: "grace", + reason_code: "out_of_office", + overrides: ["any_host"], + }); + expect(reassigned.isError).toBe(false); + + const away = await call("create_scheduling_out_of_office", { + host: "grace", + start: slot.start, + end: new Date(Date.parse(later.end) + 3_600_000).toISOString(), + reason: "vacation", + }); + expect(away.isError).toBe(false); + const [entry] = away.data.entries; + expect(entry).toMatchObject({ reason: "vacation" }); + expect( + (await call("list_scheduling_out_of_office", { host: "grace" })).data.entries, + ).toHaveLength(1); + const conflicts = await call("list_scheduling_out_of_office_conflicts", { id: entry.id }); + expect(conflicts.data.bookings.map((row: { booking: string }) => row.booking)).toEqual([ + booked.data.id, + ]); + expect((await call("delete_scheduling_out_of_office", { id: entry.id })).isError).toBe(false); }); test("Hooks and Risk: an endpoint tested, a portal link, lists and checks", async () => { diff --git a/apps/api/test/request-log.test.ts b/apps/api/test/request-log.test.ts index 93c081f6d..8334d7585 100644 --- a/apps/api/test/request-log.test.ts +++ b/apps/api/test/request-log.test.ts @@ -103,6 +103,54 @@ for (const status of [201, 422]) { }); } +// Why someone's away can be medical, notes are encrypted everywhere else, and a feed's address or a +// session's link is a credential. +for (const [path, secret] of [ + ["/v1/scheduling/out_of_office", "Recovering from surgery"], + ["/v1/scheduling/out_of_office/ooo_1", "Recovering from surgery"], + ["/v1/scheduling/out_of_office/by_external_id/leave_1", "Recovering from surgery"], + ["/v1/scheduling/reassignments", "Recovering from surgery"], + ["/v1/scheduling/reassignments/preview", "Recovering from surgery"], + ["/v1/scheduling/holiday_calendars", "https://example.com/feed.ics?token=secret"], +] as const) { + test(`${path} keeps no body either way`, async () => { + const { app, writer } = loggedApp(() => Response.json({})); + app.post(path, async (c) => { + const body = await c.req.json(); + return Response.json({ object: "scheduling.thing", id: "x_1", ...body }, { status: 201 }); + }); + await app.request(path, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ notes: secret, note: secret, ics_url: secret }), + }); + const [line] = await waitForLogLines(writer, 1); + expect(JSON.stringify(line)).not.toContain(secret); + }); +} + +test("a manager session's link stays out of its response's log line", async () => { + const { app, writer } = loggedApp(() => Response.json({})); + const path = "/v1/scheduling/manager_sessions"; + app.post(path, () => + Response.json( + { + id: "schs_1", + token: "cossch_secret", + url: "https://book.example.com/t/schs_1#cossch_secret", + }, + { status: 201 }, + ), + ); + await app.request(path, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ hosts: ["host_1"] }), + }); + const [line] = await waitForLogLines(writer, 1); + expect(JSON.stringify(line)).not.toContain("cossch_secret"); +}); + for (const path of ["/v1/risk/email/check", "/v1/risk/ip/check", "/v1/risk/content/classify"]) { for (const status of [200, 422, 500]) { test(`${path} omits both bodies at status ${status}`, async () => { diff --git a/apps/api/test/scheduling.test.ts b/apps/api/test/scheduling.test.ts index 60cbe4ae1..b07022024 100644 --- a/apps/api/test/scheduling.test.ts +++ b/apps/api/test/scheduling.test.ts @@ -6,19 +6,33 @@ import { memoryLogWriter } from "@coss/logs/testing"; import { memoryLimiter } from "@coss/ratelimit/testing"; import { createBookerSession, + createDashboardSession, createEventType, createHostSession, createManageSession, getBookingOverview, listBookableEventTypes, listEventTypes, + runDueReassignments, } from "@coss/scheduling"; import { testPng, testSchedulingDeps } from "@coss/scheduling/testing"; -import { createTestTenant, type TestTenant } from "@coss/tenancy/testing"; +import { createEnvironmentApiKey } from "@coss/tenancy"; +import { + addMember, + createTestTenant, + createTestUser, + type TestTenant, +} from "@coss/tenancy/testing"; import { Hono } from "hono"; import { createSchedulingCallbackRoutes } from "../src/routes/scheduling-callbacks.ts"; import type { AppEnv } from "../src/types.ts"; -import { createTestApp, errorOf, type TestApp, waitForLogLines } from "./helpers.ts"; +import { + createTestApp, + errorOf, + setKeyPermissions, + type TestApp, + waitForLogLines, +} from "./helpers.ts"; const MONDAY = "2031-01-06"; const at = (time: string) => `${MONDAY}T${time}:00.000Z`; @@ -1416,3 +1430,1020 @@ describe("Zoom's events", () => { expect((await post(body, signature, timestamp)).status).toBe(200); }); }); + +describe("out of office", () => { + async function booker() { + const tenant = await createTestTenant(); + const { app } = createTestApp(); + const key = tenant.sandboxKey; + const send = (path: string, method: string, body?: unknown, auth = key) => + app.request(`/v1/scheduling${path}`, { + method, + headers: { authorization: `Bearer ${auth}`, "content-type": "application/json" }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }); + const host = (await ( + await send("/hosts", "POST", { name: "Dr. Reyes", time_zone: "UTC", uid: "doctor_1" }) + ).json()) as { id: string }; + await send("/hosts/doctor_1/schedules", "POST", { + rules: [{ days: [0, 1, 2, 3, 4, 5, 6], start: "09:00", end: "17:00" }], + }); + const eventType = (await ( + await send("/event_types", "POST", { + title: "Consultation", + duration_minutes: 30, + hosts: [{ host: "doctor_1" }], + }) + ).json()) as { id: string }; + const tokenFor = async (path: string, body: unknown) => + ((await (await send(path, "POST", body)).json()) as { token: string }).token; + const client = + (token: string) => + (path: string, method = "GET", body?: unknown) => + app.request(`/scheduling/client${path}`, { + method, + headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }); + return { app, tenant, send, host, eventType, tokenFor, client }; + } + + const FAR = { start: "2031-01-06T00:00:00.000Z", end: "2031-01-07T00:00:00.000Z" }; + + test("needs the right permissions, and a bad window is a 422", async () => { + const { send } = await booker(); + const refused = await send("/out_of_office", "POST", { host: "doctor_1" }); + expect(refused.status).toBe(422); + expect((await errorOf(refused)).code).toBe("invalid_time"); + const anonymous = await send("/out_of_office", "GET", undefined, "coss_test_nope"); + expect(anonymous.status).toBe(401); + }); + + test("a host session manages only its own time, and can't name another host", async () => { + const { send, host, tokenFor, client } = await booker(); + const other = (await ( + await send("/hosts", "POST", { name: "Dr. Chen", time_zone: "UTC" }) + ).json()) as { id: string }; + const mine = client(await tokenFor(`/hosts/${host.id}/sessions`, {})); + const theirs = client(await tokenFor(`/hosts/${other.id}/sessions`, {})); + + const created = await mine("/host/out_of_office", "POST", { + start_date: "2031-01-06", + end_date: "2031-01-06", + reason: "vacation", + }); + expect(created.status).toBe(201); + const entry = (await created.json()) as { id: string; host: string; source: string }; + expect(entry.host).toBe(host.id); + expect(entry.source).toBe("host"); + + const naming = await mine("/host/out_of_office", "POST", { + host: other.id, + start_date: "2031-01-06", + end_date: "2031-01-06", + }); + expect(naming.status).toBe(422); + + const list = (await (await mine("/host/out_of_office")).json()) as { + data: { id: string }[]; + reasons: { key: string }[]; + }; + expect(list.data.map((row) => row.id)).toEqual([entry.id]); + expect(list.reasons.map((reason) => reason.key)).toContain("vacation"); + expect( + ((await (await theirs("/host/out_of_office")).json()) as { data: unknown[] }).data, + ).toEqual([]); + expect((await theirs(`/host/out_of_office/${entry.id}`, "DELETE")).status).toBe(404); + expect((await theirs(`/host/out_of_office/${entry.id}`, "PATCH", { notes: "x" })).status).toBe( + 404, + ); + expect((await theirs(`/host/out_of_office/${entry.id}/conflicts`)).status).toBe(404); + expect((await mine(`/host/out_of_office/${entry.id}`, "DELETE")).status).toBe(204); + }); + + test("a host names who covers for them, only from those they share work with", async () => { + const { send, host, tokenFor, client } = await booker(); + const peer = (await ( + await send("/hosts", "POST", { name: "Dr. Peer", time_zone: "UTC" }) + ).json()) as { id: string }; + const stranger = (await ( + await send("/hosts", "POST", { name: "Dr. Stranger", time_zone: "UTC" }) + ).json()) as { id: string }; + await send("/event_types", "POST", { + title: "Shared", + kind: "pool", + duration_minutes: 30, + hosts: [{ host: host.id }, { host: peer.id }], + }); + const mine = client(await tokenFor(`/hosts/${host.id}/sessions`, {})); + const listed = (await (await mine("/host/delegates")).json()) as { + data: Record[]; + }; + // Only what it takes to choose: no attributes, emails or anything else about them. + expect(listed.data).toEqual([{ id: peer.id, name: "Dr. Peer" }]); + + const when = { start_date: "2031-01-06", end_date: "2031-01-06" }; + const named = await mine("/host/out_of_office", "POST", { ...when, delegates: [peer.id] }); + expect(named.status).toBe(201); + const entry = (await named.json()) as { id: string; delegates: string[] }; + expect(entry.delegates).toEqual([peer.id]); + const refused = await mine("/host/out_of_office", "POST", { + ...when, + delegates: [stranger.id], + }); + expect(refused.status).toBe(422); + const changing = await mine(`/host/out_of_office/${entry.id}`, "PATCH", { + delegates: [stranger.id], + }); + expect(changing.status).toBe(422); + }); + + describe("a team page", () => { + async function team(extra: Record = {}) { + const base = await booker(); + const { send, host, eventType } = base; + const rules = [{ days: [0, 1, 2, 3, 4, 5, 6], start: "09:00", end: "17:00" }]; + const add = async (name: string, attributes?: Record) => { + const created = (await ( + await send("/hosts", "POST", { name, time_zone: "UTC", attributes }) + ).json()) as { id: string }; + await send(`/hosts/${created.id}/schedules`, "POST", { rules }); + return created; + }; + const peer = await add("Dr. Peer", { team: "heart" }); + const outsider = await add("Dr. Outsider", { team: "skin" }); + const pool = (await ( + await send("/event_types", "POST", { + title: "Shared", + kind: "pool", + duration_minutes: 30, + hosts: [{ host: host.id }, { host: peer.id }, { host: outsider.id }], + }) + ).json()) as { id: string }; + const tokenRes = await send("/manager_sessions", "POST", { + hosts: [host.id, peer.id], + ...extra, + }); + expect(tokenRes.status).toBe(201); + const session = (await tokenRes.json()) as { token: string; url: string | null }; + const lead = base.client(session.token); + const bookFor = async (hostId: string, start: string) => + (await ( + await send("/bookings", "POST", { + event_type: pool.id, + start, + host: hostId, + attendee: { + uid: `p-${start}-${hostId}`, + name: "Private Person", + email: "private@example.com", + time_zone: "UTC", + }, + }) + ).json()) as { id: string }; + return { ...base, host, eventType, peer, outsider, pool, lead, bookFor, session }; + } + const START = "2031-01-06T10:00:00.000Z"; + const WHEN = { start_date: "2031-01-20", end_date: "2031-01-20" }; + + test("the dashboard's team page reaches every host, as the member who opened it", async () => { + const { tenant, send, peer, outsider, bookFor, host } = await team(); + const env = { + organizationId: tenant.organization.id, + projectId: tenant.project.id, + environmentId: tenant.development.id, + mode: "sandbox" as const, + }; + const session = await createDashboardSession(env, { + createdByUserId: tenant.owner.id, + publicUrl: "https://book.example.com", + }); + expect(session.url).toContain(`/t/${session.id}#`); + const { app } = createTestApp(); + const asMember = (method: string, path: string, body?: unknown) => + app.request(`/scheduling/client/manager${path}`, { + method, + headers: { + authorization: `Bearer ${session.token}`, + "content-type": "application/json", + }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }); + const hosts = (await (await asMember("GET", "/hosts")).json()) as { data: { id: string }[] }; + expect(hosts.data.map((found) => found.id).sort()).toEqual( + [host.id, peer.id, outsider.id].sort(), + ); + + // A link to one booking finds it, whatever else is coming up. + const booked = await bookFor(host.id, START); + const later = await bookFor(host.id, "2031-01-07T10:00:00.000Z"); + const one = (await (await asMember("GET", `/bookings?booking=${later.id}`)).json()) as { + data: { id: string }[]; + }; + expect(one.data.map((found) => found.id)).toEqual([later.id]); + + // Who's free is worked out here, and whoever's chosen is whoever's asked for. + const candidates = (await ( + await asMember("GET", `/bookings/${booked.id}/reassign_candidates`) + ).json()) as { data: { host: string; available: boolean }[] }; + expect(candidates.data.map((found) => found.host).sort()).toEqual( + [peer.id, outsider.id].sort(), + ); + const moved = await asMember("POST", `/bookings/${booked.id}/reassign`, { + changes: [{ to_host: outsider.id }], + reason_code: "other", + note: "They asked", + notify: { attendee: false }, + }); + expect(moved.status).toBe(200); + expect(((await moved.json()) as { hosts: string[] }).hosts).toEqual([outsider.id]); + + // Time away, on the member's say-so, in whole days and in hours. + const during = await bookFor(host.id, "2031-01-20T10:00:00.000Z"); + const days = await asMember("POST", "/out_of_office", { + hosts: [host.id], + ...WHEN, + reason: "vacation", + public_message: "Back soon", + notes: "Private", + show_on_calendar: false, + delegates: [peer.id], + }); + expect(days.status).toBe(201); + const [entry] = ((await days.json()) as { data: { id: string; source: string }[] }).data; + expect(entry?.source).toBe("dashboard"); + const hours = await asMember("POST", "/out_of_office", { + hosts: [peer.id], + start: "2031-02-03T09:00:00.000Z", + end: "2031-02-03T12:00:00.000Z", + }); + expect(hours.status).toBe(201); + + // Moving what's in someone's time away, as the page does. + const preview = await asMember("POST", "/reassignments/preview", { + selection: { out_of_office: entry?.id }, + strategy: {}, + }); + expect(preview.status).toBe(200); + const items = ( + (await preview.json()) as { + items: { booking: string; to_host: string | null; from_host: string | null }[]; + } + ).items; + expect(items.map((item) => item.booking)).toEqual([during.id]); + expect(items[0]?.to_host).not.toBeNull(); + const run = await asMember("POST", "/reassignments", { + selection: { out_of_office: entry?.id }, + strategy: { + assignments: [ + { booking: during.id, to_host: items[0]?.to_host, from_host: items[0]?.from_host }, + ], + }, + reason_code: "out_of_office", + notify: { attendees: false }, + }); + expect(run.status).toBe(202); + await runDueReassignments(testSchedulingDeps(), { + scope: { organizationIds: [tenant.organization.id] }, + }); + const after = await send(`/bookings/${during.id}`, "GET"); + expect(((await after.json()) as { hosts: string[] }).hosts).toEqual([ + items[0]?.to_host as string, + ]); + }); + + test("the dashboard's team page lasts only as long as the member can", async () => { + const { tenant, app, host } = await team(); + const env = { + organizationId: tenant.organization.id, + projectId: tenant.project.id, + environmentId: tenant.development.id, + mode: "sandbox" as const, + }; + const admin = await createTestUser(); + await addMember({ + organizationId: tenant.organization.id, + userId: admin.id, + role: "admin", + }); + const session = await createDashboardSession(env, { + createdByUserId: admin.id, + publicUrl: null, + }); + const hosts = () => + app.request("/scheduling/client/manager/hosts", { + headers: { authorization: `Bearer ${session.token}` }, + }); + expect(((await (await hosts()).json()) as { data: { id: string }[] }).data).toContainEqual( + expect.objectContaining({ id: host.id }), + ); + const ids = [admin.id, tenant.organization.id]; + const setRole = (role: string) => + pool.query("update member set role = $1 where user_id = $2 and organization_id = $3", [ + role, + ...ids, + ]); + await setRole("member"); + expect((await hosts()).status).toBe(404); + await setRole("admin"); + expect((await hosts()).status).toBe(200); + const ban = (until: Date | null) => + pool.query('update "user" set banned = true, ban_expires = $1 where id = $2', [ + until, + admin.id, + ]); + await ban(null); + expect((await hosts()).status).toBe(404); + await ban(new Date(Date.now() + 60_000)); + expect((await hosts()).status).toBe(404); + await ban(new Date(Date.now() - 60_000)); + expect((await hosts()).status).toBe(200); + await pool.query("delete from member where user_id = $1 and organization_id = $2", ids); + expect((await hosts()).status).toBe(404); + }); + + test("an empty scope is refused, and so is giving both", async () => { + const { send, host } = await booker(); + const empty = await send("/manager_sessions", "POST", { host_filter: {} }); + expect(empty.status).toBe(422); + expect((await errorOf(empty)).code).toBe("invalid_session"); + expect((await send("/manager_sessions", "POST", { hosts: [] })).status).toBe(422); + expect((await send("/manager_sessions", "POST", {})).status).toBe(422); + const both = await send("/manager_sessions", "POST", { + hosts: [host.id], + host_filter: { team: "x" }, + }); + expect(both.status).toBe(422); + }); + + test("its url is its own kind of page", async () => { + const { session } = await team(); + if (session.url) expect(session.url).toContain("/t/schs_"); + }); + + test("sees its own people, and only what it takes to choose them", async () => { + const { lead, host, peer, outsider } = await team(); + const { data } = (await (await lead("/manager/hosts")).json()) as { + data: Record[]; + }; + expect(data.map((row) => row.id)).toEqual(expect.arrayContaining([host.id, peer.id])); + expect(data.map((row) => row.id)).not.toContain(outsider.id); + expect(Object.keys(data[0] as object).sort()).toEqual( + ["id", "name", "object", "status", "time_zone", "uid"].sort(), + ); + }); + + test("a filter's asked again on every request", async () => { + const { send, booker: _unused, client, ...rest } = { ...(await team()), booker: null }; + const { outsider } = rest; + const session = (await ( + await send("/manager_sessions", "POST", { host_filter: { team: "skin" } }) + ).json()) as { token: string }; + const lead = client(session.token); + const ids = async () => + ((await (await lead("/manager/hosts")).json()) as { data: { id: string }[] }).data.map( + (row) => row.id, + ); + expect(await ids()).toEqual([outsider.id]); + await send(`/hosts/${outsider.id}`, "PATCH", { attributes: { team: "heart" } }); + expect(await ids()).toEqual([]); + }); + + test("another kind of session can't use it, nor it theirs", async () => { + const { lead, client, host, tokenFor } = await team(); + const hostPage = client(await tokenFor(`/hosts/${host.id}/sessions`, {})); + expect((await hostPage("/manager/hosts")).status).toBe(404); + expect((await lead("/host/out_of_office")).status).toBe(404); + }); + + test("bookings carry no attendee", async () => { + const { lead, host, bookFor } = await team(); + const booking = await bookFor(host.id, START); + const response = await lead("/manager/bookings"); + const text = await response.text(); + expect(text).toContain(booking.id); + expect(text).not.toContain("Private Person"); + expect(text).not.toContain("private@example.com"); + expect(text).not.toContain("attendee"); + }); + + test("what needs cover: someone's away during it", async () => { + const { lead, host, peer, bookFor, send } = await team(); + const away = await bookFor(host.id, START); + const fine = await bookFor(peer.id, START); + await send("/out_of_office", "POST", { + host: host.id, + start: START, + end: "2031-01-06T12:00:00.000Z", + }); + const { data } = (await (await lead("/manager/bookings?needs_cover=true")).json()) as { + data: { id: string; host_away: boolean }[]; + }; + expect(data.map((row) => row.id)).toContain(away.id); + expect(data.map((row) => row.id)).not.toContain(fine.id); + expect(data.find((row) => row.id === away.id)?.host_away).toBe(true); + }); + + test("someone else's booking doesn't exist", async () => { + const { lead, outsider, peer, bookFor } = await team(); + const theirs = await bookFor(outsider.id, START); + expect((await lead(`/manager/bookings/${theirs.id}/reassign_candidates`)).status).toBe(404); + const taking = await lead(`/manager/bookings/${theirs.id}/reassign`, "POST", { + changes: [{ to_host: peer.id }], + reason_code: "other", + }); + expect(taking.status).toBe(404); + }); + + test("moves a booking among its own people, and no further", async () => { + const { lead, host, peer, outsider, bookFor, send } = await team(); + const mine = await bookFor(host.id, START); + const candidates = (await ( + await lead(`/manager/bookings/${mine.id}/reassign_candidates`) + ).json()) as { data: { host: string }[] }; + expect(candidates.data.map((row) => row.host)).toEqual([peer.id]); + const outward = await lead(`/manager/bookings/${mine.id}/reassign`, "POST", { + changes: [{ to_host: outsider.id }], + reason_code: "other", + }); + expect(outward.status).toBe(422); + const moved = await lead(`/manager/bookings/${mine.id}/reassign`, "POST", { + changes: [{ to_host: peer.id }], + reason_code: "other", + }); + expect(moved.status).toBe(200); + expect(await moved.text()).not.toContain("private@example.com"); + const after = (await (await send(`/bookings/${mine.id}`, "GET")).json()) as { + hosts: string[]; + }; + expect(after.hosts).toEqual([peer.id]); + }); + + test("auto stays on the team", async () => { + const { lead, host, peer, bookFor } = await team(); + const mine = await bookFor(host.id, START); + const moved = await lead(`/manager/bookings/${mine.id}/reassign`, "POST", { + changes: [{ auto: true }], + reason_code: "other", + }); + expect(moved.status).toBe(200); + expect(((await moved.json()) as { hosts: string[] }).hosts).toEqual([peer.id]); + }); + + test("time off for its own people, naming only them", async () => { + const { lead, host, peer, outsider, client, send } = await team(); + const created = await lead("/manager/out_of_office", "POST", { + ...WHEN, + hosts: [host.id], + delegates: [peer.id], + }); + expect(created.status).toBe(201); + const { data } = (await created.json()) as { + data: { id: string; source: string; delegates: string[] }[]; + }; + expect(data[0]).toMatchObject({ source: "manager", delegates: [peer.id] }); + + const notMine = await lead("/manager/out_of_office", "POST", { + ...WHEN, + hosts: [outsider.id], + }); + expect(notMine.status).toBe(404); + const delegate = await lead("/manager/out_of_office", "POST", { + ...WHEN, + hosts: [host.id], + delegates: [outsider.id], + }); + expect(delegate.status).toBe(422); + + const theirs = (await ( + await send("/out_of_office", "POST", { ...WHEN, host: outsider.id }) + ).json()) as { data: { id: string }[] }; + const theirId = theirs.data[0]?.id as string; + expect((await lead(`/manager/out_of_office/${theirId}`, "DELETE")).status).toBe(404); + expect((await lead(`/manager/out_of_office/${theirId}/conflicts`)).status).toBe(404); + const list = (await (await lead("/manager/out_of_office")).json()) as { + data: { id: string }[]; + }; + expect(list.data.map((row) => row.id)).not.toContain(theirId); + expect(client).toBeTruthy(); + expect((await lead(`/manager/out_of_office/${data[0]?.id}`, "DELETE")).status).toBe(204); + }); + + test("can't start moving bookings from an entry", async () => { + const { lead, host } = await team(); + const asked = await lead("/manager/out_of_office", "POST", { + ...WHEN, + hosts: [host.id], + existing_bookings: "reassign", + }); + expect(asked.status).toBe(422); + }); + + test("time away comes a page at a time", async () => { + const { lead, host, peer } = await team(); + for (const hostId of [host.id, peer.id]) { + const made = await lead("/manager/out_of_office", "POST", { ...WHEN, hosts: [hostId] }); + expect(made.status).toBe(201); + } + const page = async (query: string) => + (await (await lead(`/manager/out_of_office${query}`, "GET")).json()) as { + data: { id: string }[]; + has_more: boolean; + }; + const first = await page("?limit=1"); + expect(first.data.length).toBe(1); + expect(first.has_more).toBe(true); + const second = await page(`?limit=1&after=${first.data[0]?.id}`); + expect(second.data.length).toBe(1); + expect(second.data[0]?.id).not.toBe(first.data[0]?.id); + expect(second.has_more).toBe(false); + }); + + test("moving an entry that moves bookings doesn't start a run", async () => { + const { lead, send, host, outsider, bookFor } = await team(); + const created = (await ( + await send("/out_of_office", "POST", { + host: host.id, + start_date: "2031-03-03", + end_date: "2031-03-03", + existing_bookings: "reassign", + reassign: { to_host: outsider.id }, + }) + ).json()) as { data: { id: string }[] }; + await bookFor(host.id, START); + const moved = await lead(`/manager/out_of_office/${created.data[0]?.id}`, "PATCH", { + start_date: "2031-01-06", + end_date: "2031-01-06", + }); + expect(moved.status).toBe(200); + const runs = (await (await send("/reassignments", "GET")).json()) as { data: unknown[] }; + expect(runs.data).toEqual([]); + }); + + test("a run stays on the team, and says who each goes to", async () => { + const { lead, host, peer, outsider, bookFor } = await team(); + const mine = await bookFor(host.id, START); + const theirs = await bookFor(outsider.id, START); + const run = (body: unknown) => lead("/manager/reassignments", "POST", body); + const fromOutside = await run({ + selection: { host: outsider.id }, + strategy: { to_host: peer.id }, + reason_code: "other", + }); + expect(fromOutside.status).toBe(422); + const toOutside = await run({ + selection: { host: host.id }, + strategy: { to_host: outsider.id }, + reason_code: "other", + }); + expect(toOutside.status).toBe(422); + const anyone = await run({ + selection: { host: host.id }, + strategy: { preferred_hosts: [peer.id] }, + reason_code: "other", + }); + expect(anyone.status).toBe(422); + const byId = await run({ + selection: { booking_ids: [theirs.id] }, + strategy: { to_host: peer.id }, + reason_code: "other", + }); + expect(byId.status).toBe(422); + + const preview = await lead("/manager/reassignments/preview", "POST", { + selection: { host: host.id }, + strategy: { preferred_hosts: [outsider.id] }, + }); + expect(preview.status).toBe(422); + const fine = await lead("/manager/reassignments/preview", "POST", { + selection: { host: host.id }, + strategy: { preferred_hosts: [peer.id] }, + }); + const { items } = (await fine.json()) as { items: { booking: string; to_host: string }[] }; + expect(items[0]).toMatchObject({ booking: mine.id, to_host: peer.id }); + expect(await (await lead("/manager/bookings")).text()).not.toContain("private@example.com"); + + const started = await run({ + selection: { host: host.id }, + strategy: { assignments: [{ booking: mine.id, to_host: peer.id, from_host: host.id }] }, + reason_code: "other", + }); + expect(started.status).toBe(202); + const { id } = (await started.json()) as { id: string }; + expect((await lead(`/manager/reassignments/${id}`)).status).toBe(200); + const list = (await (await lead("/manager/reassignments")).json()) as { + data: { id: string }[]; + }; + expect(list.data.map((row) => row.id)).toEqual([id]); + }); + + test("can't name someone off the team as who leaves a booking", async () => { + const { lead, host, peer, outsider, send } = await team(); + const panel = (await ( + await send("/event_types", "POST", { + title: "Panel", + kind: "collective", + duration_minutes: 30, + hosts: [ + { host: host.id, role: "required" }, + { host: outsider.id, role: "required" }, + ], + }) + ).json()) as { id: string }; + const booking = (await ( + await send("/bookings", "POST", { + event_type: panel.id, + start: START, + attendee: { uid: "p", name: "Sam", email: "sam@example.com", time_zone: "UTC" }, + }) + ).json()) as { id: string }; + const refused = await lead("/manager/reassignments", "POST", { + selection: { host: host.id }, + strategy: { + assignments: [{ booking: booking.id, to_host: peer.id, from_host: outsider.id }], + }, + reason_code: "other", + overrides: ["any_host"], + }); + expect(refused.status).toBe(422); + }); + + test("undoing only gives back between its own people", async () => { + const { tenant, lead, host, peer, outsider, bookFor, send } = await team(); + const mine = await bookFor(host.id, START); + const later = await bookFor(host.id, "2031-01-06T11:00:00.000Z"); + // Someone with an API key moved them, one on the team and one off it. + const made = (await ( + await send("/reassignments", "POST", { + selection: { host: host.id }, + strategy: { + assignments: [ + { booking: mine.id, to_host: peer.id, from_host: host.id }, + { booking: later.id, to_host: outsider.id, from_host: host.id }, + ], + }, + reason_code: "other", + }) + ).json()) as { id: string }; + const testDeps = testSchedulingDeps(); + const scope = { organizationIds: [tenant.organization.id] }; + await runDueReassignments(testDeps, { scope }); + const undone = await lead(`/manager/reassignments/${made.id}/undo`, "POST", {}); + expect(undone.status).toBe(202); + await runDueReassignments(testDeps, { scope }); + const hostsOf = async (id: string) => + ((await (await send(`/bookings/${id}`, "GET")).json()) as { hosts: string[] }).hosts; + expect(await hostsOf(mine.id)).toEqual([host.id]); + expect(await hostsOf(later.id)).toEqual([outsider.id]); + + // Undoing that is still this team's, and so is what comes of it. + const undoneRun = (await undone.json()) as { id: string }; + const again = await lead(`/manager/reassignments/${undoneRun.id}/undo`, "POST", {}); + expect(again.status).toBe(202); + const againRun = (await again.json()) as { id: string }; + await runDueReassignments(testDeps, { scope }); + expect(await hostsOf(mine.id)).toEqual([peer.id]); + expect((await lead(`/manager/reassignments/${againRun.id}`, "GET")).status).toBe(200); + const runs = (await (await lead("/manager/reassignments", "GET")).json()) as { + data: { id: string }[]; + }; + expect(runs.data.map((run) => run.id)).toEqual( + expect.arrayContaining([made.id, undoneRun.id, againRun.id]), + ); + }); + + test("another team's run doesn't exist", async () => { + const { send, host, peer, client, outsider, lead } = await team(); + const other = (await ( + await send("/reassignments", "POST", { + selection: { host: outsider.id }, + strategy: { to_host: peer.id }, + reason_code: "other", + }) + ).json()) as { id: string }; + expect((await lead(`/manager/reassignments/${other.id}`)).status).toBe(404); + expect((await lead(`/manager/reassignments/${other.id}/items`)).status).toBe(404); + expect((await lead(`/manager/reassignments/${other.id}/cancel`, "POST")).status).toBe(404); + expect((await lead(`/manager/reassignments/${other.id}/undo`, "POST", {})).status).toBe(404); + const list = (await (await lead("/manager/reassignments")).json()) as { data: unknown[] }; + expect(list.data).toEqual([]); + expect(client).toBeTruthy(); + expect(host).toBeTruthy(); + }); + + test("read-only looks and doesn't touch", async () => { + const { lead, host, peer, bookFor } = await team({ read_only: true }); + const mine = await bookFor(host.id, START); + expect((await lead("/manager/bookings")).status).toBe(200); + const refusals = [ + await lead("/manager/out_of_office", "POST", { ...WHEN, hosts: [host.id] }), + await lead(`/manager/bookings/${mine.id}/reassign`, "POST", { + changes: [{ to_host: peer.id }], + reason_code: "other", + }), + await lead("/manager/reassignments", "POST", { + selection: { host: host.id }, + strategy: { to_host: peer.id }, + reason_code: "other", + }), + ]; + for (const refused of refusals) { + expect(refused.status).toBe(403); + expect((await errorOf(refused)).code).toBe("read_only_session"); + } + }); + }); + + describe("a host giving away their own booking", () => { + async function setup() { + const base = await booker(); + const { send, host } = base; + const rules = [{ days: [0, 1, 2, 3, 4, 5, 6], start: "09:00", end: "17:00" }]; + const add = async (name: string) => { + const created = (await ( + await send("/hosts", "POST", { name, time_zone: "UTC" }) + ).json()) as { id: string }; + await send(`/hosts/${created.id}/schedules`, "POST", { rules }); + return created; + }; + const peer = await add("Dr. Peer"); + const stranger = await add("Dr. Stranger"); + const pool = (await ( + await send("/event_types", "POST", { + title: "Shared", + kind: "pool", + duration_minutes: 30, + hosts: [{ host: host.id }, { host: peer.id }], + }) + ).json()) as { id: string }; + const booking = (await ( + await send("/bookings", "POST", { + event_type: pool.id, + start: "2031-01-06T10:00:00.000Z", + host: host.id, + attendee: { + uid: "p", + name: "Private Person", + email: "private@example.com", + time_zone: "UTC", + }, + }) + ).json()) as { id: string }; + const mine = base.client(await base.tokenFor(`/hosts/${host.id}/sessions`, {})); + return { ...base, peer, stranger, booking, mine }; + } + + test("is off until the environment says so, and then nothing's there", async () => { + const { mine, booking, peer } = await setup(); + expect((await mine(`/host/bookings/${booking.id}/reassign_candidates`)).status).toBe(404); + const refused = await mine(`/host/bookings/${booking.id}/reassign`, "POST", { + to_host: peer.id, + }); + expect(refused.status).toBe(404); + const session = (await (await mine("/session")).json()) as { can_reassign: boolean }; + expect(session.can_reassign).toBe(false); + }); + + test("sees names only, and only those free who take it", async () => { + const { send, mine, booking, peer } = await setup(); + await send("/settings", "PATCH", { host_reassignment: "eligible" }); + const { data } = (await ( + await mine(`/host/bookings/${booking.id}/reassign_candidates`) + ).json()) as { data: Record[] }; + expect(data).toEqual([{ host: peer.id, name: "Dr. Peer" }]); + }); + + test("can give it to them, and to nobody else", async () => { + const { send, mine, booking, peer, stranger } = await setup(); + await send("/settings", "PATCH", { host_reassignment: "eligible" }); + const toStranger = await mine(`/host/bookings/${booking.id}/reassign`, "POST", { + to_host: stranger.id, + }); + expect(toStranger.status).toBe(422); + const past = await mine(`/host/bookings/${booking.id}/reassign`, "POST", { + to_host: peer.id, + overrides: ["any_host"], + }); + expect(past.status).toBe(422); + const moved = await mine(`/host/bookings/${booking.id}/reassign`, "POST", { + to_host: peer.id, + }); + expect(moved.status).toBe(200); + // It isn't theirs any more, and it looks like it. + const again = await mine(`/host/bookings/${booking.id}/reassign`, "POST", { + to_host: peer.id, + }); + expect(again.status).toBe(404); + }); + + test("someone else's can't be given away", async () => { + const { send, tokenFor, client, booking, peer } = await setup(); + await send("/settings", "PATCH", { host_reassignment: "eligible" }); + const theirs = client(await tokenFor(`/hosts/${peer.id}/sessions`, {})); + expect((await theirs(`/host/bookings/${booking.id}/reassign_candidates`)).status).toBe(404); + const refused = await theirs(`/host/bookings/${booking.id}/reassign`, "POST", { + to_host: peer.id, + }); + expect(refused.status).toBe(404); + }); + + test("read-only can't", async () => { + const { send, tokenFor, client, host, booking, peer } = await setup(); + await send("/settings", "PATCH", { host_reassignment: "eligible" }); + const session = client(await tokenFor(`/hosts/${host.id}/sessions`, { read_only: true })); + const refused = await session(`/host/bookings/${booking.id}/reassign`, "POST", { + to_host: peer.id, + }); + expect(refused.status).toBe(403); + }); + + test("a host's own list has no contact details", async () => { + const { mine, booking } = await setup(); + const response = await mine("/host/bookings"); + const text = await response.text(); + expect(text).toContain(booking.id); + expect(text).toContain("Private Person"); + expect(text).not.toContain("private@example.com"); + const { data } = JSON.parse(text) as { data: Record[] }; + expect(Object.keys(data[0] as object).sort()).toEqual( + [ + "attendees", + "end", + "group_session", + "id", + "no_show_at", + "start", + "status", + "title", + ].sort(), + ); + }); + }); + + test("a host's session can't start moving bookings by changing an entry", async () => { + const { host, tokenFor, client } = await booker(); + const mine = client(await tokenFor(`/hosts/${host.id}/sessions`, {})); + const created = await mine("/host/out_of_office", "POST", { + start_date: "2031-01-06", + end_date: "2031-01-06", + }); + const entry = (await created.json()) as { id: string }; + const refused = await mine(`/host/out_of_office/${entry.id}`, "PATCH", { + existing_bookings: "reassign", + }); + expect(refused.status).toBe(422); + }); + + test("a key that can only create can't start moving bookings", async () => { + const { tenant, app, send, host } = await booker(); + const created = await createEnvironmentApiKey({ + organizationId: tenant.organization.id, + environmentId: tenant.development.id, + name: "Create only", + userId: tenant.owner.id, + permissions: "full", + }); + await setKeyPermissions(created.id, { scheduling: ["create"] }); + const key = created.key; + const asThem = (path: string, body: unknown) => + app.request(`/v1/scheduling${path}`, { + method: "POST", + headers: { authorization: `Bearer ${key}`, "content-type": "application/json" }, + body: JSON.stringify(body), + }); + const entry = { host: host.id, start_date: "2031-01-06", end_date: "2031-01-06" }; + const moving = await asThem("/out_of_office", { ...entry, existing_bookings: "reassign" }); + expect(moving.status).toBe(403); + expect((await errorOf(moving)).code).toBe("insufficient_permissions"); + const calendar = { name: "UK", country: "GB", hosts: [host.id] }; + const holidays = await asThem("/holiday_calendars", { + ...calendar, + existing_bookings: "reassign", + }); + expect(holidays.status).toBe(403); + // Time away that leaves bookings alone is theirs to add. + expect((await asThem("/out_of_office", entry)).status).toBe(201); + expect((await asThem("/holiday_calendars", calendar)).status).toBe(201); + const runs = (await (await send("/reassignments", "GET")).json()) as { data: unknown[] }; + expect(runs.data).toEqual([]); + }); + + test("a host's session isn't told why a colleague can't take a booking", async () => { + const { host, tokenFor, client, send } = await booker(); + await send("/settings", "PATCH", { host_reassignment: "eligible" }); + const peer = (await ( + await send("/hosts", "POST", { name: "Dr. Peer", time_zone: "UTC" }) + ).json()) as { id: string }; + await send(`/hosts/${peer.id}/schedules`, "POST", { + rules: [{ days: [0, 1, 2, 3, 4, 5, 6], start: "09:00", end: "17:00" }], + }); + const pool = (await ( + await send("/event_types", "POST", { + title: "Shared", + kind: "pool", + duration_minutes: 30, + hosts: [{ host: host.id }, { host: peer.id }], + }) + ).json()) as { id: string }; + const booking = (await ( + await send("/bookings", "POST", { + event_type: pool.id, + start: "2031-01-06T10:00:00.000Z", + host: host.id, + attendee: { uid: "p", name: "Sam Lee", email: "sam@example.com", time_zone: "UTC" }, + }) + ).json()) as { id: string }; + await send("/out_of_office", "POST", { + host: peer.id, + start_date: "2031-01-06", + end_date: "2031-01-06", + }); + const mine = client(await tokenFor(`/hosts/${host.id}/sessions`, {})); + const refused = await mine(`/host/bookings/${booking.id}/reassign`, "POST", { + to_host: peer.id, + }); + expect(refused.status).toBe(422); + const body = JSON.stringify(await refused.json()); + expect(body).toContain("reassign_target_unavailable"); + expect(body).not.toContain("out_of_office"); + }); + + test("a host's session moving an entry that moves bookings doesn't start a run", async () => { + const { host, tokenFor, client, send, eventType } = await booker(); + const mine = client(await tokenFor(`/hosts/${host.id}/sessions`, {})); + const created = (await ( + await send("/out_of_office", "POST", { + host: host.id, + start_date: "2031-03-03", + end_date: "2031-03-03", + existing_bookings: "reassign", + }) + ).json()) as { data: { id: string }[] }; + await send("/bookings", "POST", { + event_type: eventType.id, + start: "2031-01-06T10:00:00.000Z", + attendee: { uid: "p", name: "Sam Lee", email: "sam@example.com", time_zone: "UTC" }, + }); + const moved = await mine(`/host/out_of_office/${created.data[0]?.id}`, "PATCH", { + start_date: "2031-01-06", + end_date: "2031-01-06", + }); + expect(moved.status).toBe(200); + const runs = (await (await send("/reassignments", "GET")).json()) as { data: unknown[] }; + expect(runs.data).toEqual([]); + }); + + test("a read-only host session can look but not change anything", async () => { + const { host, tokenFor, client } = await booker(); + const session = client(await tokenFor(`/hosts/${host.id}/sessions`, { read_only: true })); + expect((await session("/host/out_of_office")).status).toBe(200); + const refused = await session("/host/out_of_office", "POST", { + start_date: "2031-01-06", + end_date: "2031-01-06", + }); + expect(refused.status).toBe(403); + }); + + test("other kinds of session can't reach a host's time", async () => { + const { eventType, tokenFor, client } = await booker(); + const session = client(await tokenFor("/booker_sessions", { event_type: eventType.id })); + expect((await session("/host/out_of_office")).status).toBe(404); + expect( + ( + await session("/host/out_of_office", "POST", { + start_date: "2031-01-06", + end_date: "2031-01-06", + }) + ).status, + ).toBe(404); + }); + + test("a booker sees a message and a return date while the only host is away, never why", async () => { + const { send, eventType, tokenFor, client } = await booker(); + const now = Date.now(); + await send("/out_of_office", "POST", { + host: "doctor_1", + start: new Date(now - 3_600_000).toISOString(), + end: new Date(now + 3_600_000).toISOString(), + reason: "sick", + notes: "Recovering from surgery", + public_message: "Back this afternoon.", + }); + const session = client(await tokenFor("/booker_sessions", { event_type: eventType.id })); + const query = new URLSearchParams(FAR).toString(); + const response = await session(`/slots?${query}`); + const body = await response.text(); + expect(response.status).toBe(200); + const parsed = JSON.parse(body) as { away: { until: string; message: string } | null }; + expect(parsed.away?.message).toBe("Back this afternoon."); + expect(body).not.toContain("sick"); + expect(body).not.toContain("surgery"); + }); + + test("a booker gets no notice when nobody's away", async () => { + const { eventType, tokenFor, client } = await booker(); + const session = client(await tokenFor("/booker_sessions", { event_type: eventType.id })); + const response = await session(`/slots?${new URLSearchParams(FAR).toString()}`); + expect(((await response.json()) as { away: unknown }).away).toBeNull(); + }); +}); diff --git a/apps/api/test/sdk-contract.test.ts b/apps/api/test/sdk-contract.test.ts index fe1337c35..45c8e5979 100644 --- a/apps/api/test/sdk-contract.test.ts +++ b/apps/api/test/sdk-contract.test.ts @@ -3,6 +3,7 @@ * Wire-level drift between the API and the SDK's hand-written types fails here or in * contract-types.ts (compile time). */ + import { describe, expect, test } from "bun:test"; import { createCreditGrant, runDueBillingPeriods } from "@coss/billing"; import { createTestContract, recordTestUsage } from "@coss/billing/testing"; @@ -25,6 +26,8 @@ import { createLogWriter } from "@coss/logs"; import { cachedOverrides, setRateLimitOverride } from "@coss/ratelimit"; import { memoryLimiter } from "@coss/ratelimit/testing"; import { suspendOrganization } from "@coss/reputation"; +import { runDueReassignments } from "@coss/scheduling"; +import { testSchedulingDeps } from "@coss/scheduling/testing"; import { createTestTenant } from "@coss/tenancy/testing"; import { API_VERSIONS, LATEST_API_VERSION } from "@coss/versioning"; import { @@ -1966,6 +1969,330 @@ describe("coss.risk checkouts, devices and calibration against the API", () => { }); }); +describe("coss.scheduling.holidayCalendars against the API", () => { + test("creates, reads, changes and removes one", async () => { + const tenant = await createTestTenant(); + const { app } = createTestApp(); + const { scheduling } = clientFor(app, tenant.sandboxKey); + const host = must( + (await scheduling.hosts.create({ uid: "first", name: "First", time_zone: "UTC" })).data, + ); + const created = await scheduling.holidayCalendars.create({ + name: "UK", + country: "GB", + hosts: ["first"], + regions: ["England"], + }); + const calendar = must(created.data); + expect(calendar).toMatchObject({ + object: "scheduling.holiday_calendar", + source: "country", + country: "GB", + hosts: [host.id], + regions: ["England"], + }); + const list = must((await scheduling.holidayCalendars.list()).data); + expect(list.data.map((row) => row.id)).toContain(calendar.id); + expect(must((await scheduling.holidayCalendars.get(calendar.id)).data).name).toBe("UK"); + expect(must((await scheduling.holidayCalendars.holidays(calendar.id)).data).object).toBe( + "list", + ); + const renamed = must( + (await scheduling.holidayCalendars.update(calendar.id, { name: "London" })).data, + ); + expect(renamed.name).toBe("London"); + const sandboxFeed = await scheduling.holidayCalendars.create({ + name: "Feed", + ics_url: "https://example.com/holidays.ics", + hosts: ["first"], + }); + expect(sandboxFeed.error?.code).toBe("invalid_holiday_calendar"); + expect(must((await scheduling.holidayCalendars.remove(calendar.id)).data).deleted).toBe(true); + expect((await scheduling.holidayCalendars.get(calendar.id)).error?.code).toBe("not_found"); + }); +}); + +describe("coss.scheduling.reassignments against the API", () => { + test("previews, runs, reports, undoes and cancels", async () => { + const tenant = await createTestTenant(); + const { app } = createTestApp(); + const { scheduling } = clientFor(app, tenant.sandboxKey); + const rules = [{ days: [0, 1, 2, 3, 4, 5, 6], start: "09:00", end: "17:00" }]; + const first = must( + (await scheduling.hosts.create({ uid: "first", name: "First", time_zone: "UTC" })).data, + ); + const second = must( + (await scheduling.hosts.create({ uid: "second", name: "Second", time_zone: "UTC" })).data, + ); + await scheduling.schedules.create("first", { rules }); + await scheduling.schedules.create("second", { rules }); + const pool = must( + ( + await scheduling.eventTypes.create({ + title: "Consultation", + kind: "pool", + duration_minutes: 30, + hosts: [{ host: "first" }, { host: "second" }], + }) + ).data, + ); + const booking = must( + ( + await scheduling.bookings.create({ + event_type: pool.id, + start: "2031-01-06T10:00:00.000Z", + host: "first", + attendee: { uid: "p", name: "Sam", email: "sam@example.com", time_zone: "UTC" }, + }) + ).data, + ); + const preview = must( + ( + await scheduling.reassignments.preview({ + selection: { host: "first" }, + strategy: { preferred_hosts: ["second"] }, + }) + ).data, + ); + expect(preview.items[0]).toMatchObject({ booking: booking.id, to_host: second.id }); + + const created = await scheduling.reassignments.create( + { + selection: { host: "first" }, + strategy: { to_host: "second" }, + reason_code: "out_of_office", + }, + { idempotencyKey: "move-them" }, + ); + const run = must(created.data); + expect(run).toMatchObject({ object: "scheduling.reassignment", status: "pending" }); + const replay = await scheduling.reassignments.create( + { + selection: { host: "first" }, + strategy: { to_host: "second" }, + reason_code: "out_of_office", + }, + { idempotencyKey: "move-them" }, + ); + expect(replay.data?.id).toBe(run.id); + + const early = await scheduling.reassignments.undo(run.id); + expect(early.error?.code).toBe("reassignment_not_finished"); + + const deps = testSchedulingDeps(); + await runDueReassignments(deps, { scope: { organizationIds: [tenant.organization.id] } }); + const done = must((await scheduling.reassignments.get(run.id)).data); + expect(done.status).toBe("completed"); + expect(done.counts.reassigned).toBe(1); + const items = must((await scheduling.reassignments.items(run.id)).data); + expect(items.data[0]).toMatchObject({ + object: "scheduling.reassignment_item", + booking: booking.id, + from_host: first.id, + to_host: second.id, + outcome: "reassigned", + }); + const list = must((await scheduling.reassignments.list()).data); + expect(list.data.map((row) => row.id)).toContain(run.id); + + const undo = must((await scheduling.reassignments.undo(run.id)).data); + expect(undo.undo_of).toBe(run.id); + const cancelled = must((await scheduling.reassignments.cancel(undo.id)).data); + expect(cancelled.status).toBe("cancelled"); + }); + + test("an out of office entry can start one", async () => { + const tenant = await createTestTenant(); + const { app } = createTestApp(); + const { scheduling } = clientFor(app, tenant.sandboxKey); + const rules = [{ days: [0, 1, 2, 3, 4, 5, 6], start: "09:00", end: "17:00" }]; + await scheduling.hosts.create({ uid: "first", name: "First", time_zone: "UTC" }); + await scheduling.hosts.create({ uid: "second", name: "Second", time_zone: "UTC" }); + await scheduling.schedules.create("first", { rules }); + await scheduling.schedules.create("second", { rules }); + const pool = must( + ( + await scheduling.eventTypes.create({ + title: "Consultation", + kind: "pool", + duration_minutes: 30, + hosts: [{ host: "first" }, { host: "second" }], + }) + ).data, + ); + await scheduling.bookings.create({ + event_type: pool.id, + start: "2031-01-06T10:00:00.000Z", + host: "first", + attendee: { uid: "p", name: "Sam", email: "sam@example.com", time_zone: "UTC" }, + }); + const entries = must( + ( + await scheduling.outOfOffice.create({ + host: "first", + start: "2031-01-06T09:00:00.000Z", + end: "2031-01-06T18:00:00.000Z", + existing_bookings: "reassign", + reassign: { preferred_hosts: ["second"] }, + }) + ).data, + ); + expect(entries.data[0]?.existing_bookings).toBe("reassign"); + const runs = must((await scheduling.reassignments.list()).data); + expect(runs.data[0]?.out_of_office).toBe(entries.data[0]?.id as string); + }); +}); + +describe("coss.scheduling.bookings.reassign against the API", () => { + test("lists who could take it, gives it to them, and refuses what it should", async () => { + const tenant = await createTestTenant(); + const { app } = createTestApp(); + const { scheduling } = clientFor(app, tenant.sandboxKey); + const rules = [{ days: [0, 1, 2, 3, 4, 5, 6], start: "09:00", end: "17:00" }]; + const first = must( + (await scheduling.hosts.create({ uid: "first", name: "First", time_zone: "UTC" })).data, + ); + const second = must( + (await scheduling.hosts.create({ uid: "second", name: "Second", time_zone: "UTC" })).data, + ); + await scheduling.schedules.create("first", { rules }); + await scheduling.schedules.create("second", { rules }); + const pool = must( + ( + await scheduling.eventTypes.create({ + title: "Consultation", + kind: "pool", + duration_minutes: 30, + hosts: [{ host: "first" }, { host: "second" }], + }) + ).data, + ); + expect(pool.reassignment_enabled).toBe(true); + const booking = must( + ( + await scheduling.bookings.create({ + event_type: pool.id, + host: "first", + start: "2031-01-06T10:00:00.000Z", + attendee: { name: "Sam", email: "sam@example.com", time_zone: "UTC" }, + }) + ).data, + ); + const candidates = await scheduling.bookings.reassignCandidates(booking.id); + expect(candidates.data?.data[0]).toMatchObject({ + object: "scheduling.reassign_candidate", + host: second.id, + member: true, + available: true, + reason: null, + }); + const moved = await scheduling.bookings.reassign(booking.id, { + changes: [{ to_host: "second" }], + reason_code: "out_of_office", + }); + expect(moved.data?.hosts).toEqual([second.id]); + expect(moved.data?.id).toBe(booking.id); + const again = await scheduling.bookings.reassign(booking.id, { + changes: [{ to_host: "second" }], + reason_code: "other", + }); + expect(again.error?.code).toBe("invalid_reassignment"); + const back = await scheduling.bookings.reassign(booking.id, { + changes: [{ auto: true }], + reason_code: "workload", + }); + expect(back.data?.hosts).toEqual([first.id]); + }); +}); + +describe("coss.scheduling.outOfOffice against the API", () => { + test("marks a host away, stops offering them, shows conflicts and takes it back", async () => { + const tenant = await createTestTenant(); + const { app } = createTestApp(); + const { scheduling } = clientFor(app, tenant.sandboxKey); + const host = must( + (await scheduling.hosts.create({ uid: "doctor_1", name: "Dr. Reyes", time_zone: "UTC" })) + .data, + ); + await scheduling.schedules.create("doctor_1", { + rules: [{ days: [0, 1, 2, 3, 4, 5, 6], start: "09:00", end: "17:00" }], + }); + const eventType = must( + ( + await scheduling.eventTypes.create({ + title: "Consultation", + duration_minutes: 30, + hosts: [{ host: "doctor_1" }], + }) + ).data, + ); + const booking = must( + ( + await scheduling.bookings.create({ + event_type: eventType.id, + start: "2031-01-06T10:00:00.000Z", + attendee: { name: "Sam", email: "sam@example.com", time_zone: "UTC" }, + }) + ).data, + ); + + const created = must( + ( + await scheduling.outOfOffice.create({ + host: "doctor_1", + start_date: "2031-01-06", + end_date: "2031-01-06", + reason: "vacation", + notes: "Lisbon", + public_message: "Back on Tuesday.", + }) + ).data, + ); + const entry = must(created.data[0]); + expect(entry).toMatchObject({ + object: "scheduling.out_of_office", + host: host.id, + reason: "vacation", + notes: "Lisbon", + source: "api", + all_day: { start_date: "2031-01-06", end_date: "2031-01-06", time_zone: "UTC" }, + }); + expect((await scheduling.outOfOffice.get(entry.id)).data?.id).toBe(entry.id); + expect((await scheduling.outOfOffice.list({ host: "doctor_1" })).data?.data.length).toBe(1); + const slots = await scheduling.slots.list(eventType.id, { + start: "2031-01-06T00:00:00.000Z", + end: "2031-01-07T00:00:00.000Z", + }); + expect(slots.data?.data).toEqual([]); + const conflicts = await scheduling.outOfOffice.conflicts(entry.id); + expect(conflicts.data?.data.map((conflict) => conflict.booking)).toEqual([booking.id]); + expect((await scheduling.bookings.get(booking.id)).data?.status).toBe("confirmed"); + + const moved = await scheduling.outOfOffice.update(entry.id, { end_date: "2031-01-07" }); + expect(moved.data?.all_day?.end_date).toBe("2031-01-07"); + const synced = await scheduling.outOfOffice.put("leave_1", { + host: "doctor_1", + start_date: "2031-02-03", + end_date: "2031-02-03", + }); + expect(synced.data?.external_id).toBe("leave_1"); + const again = await scheduling.outOfOffice.put("leave_1", { + host: "doctor_1", + start_date: "2031-02-04", + end_date: "2031-02-04", + }); + expect(again.data?.id).toBe(must(synced.data).id); + + const removed = await scheduling.outOfOffice.remove(entry.id); + expect(removed.data).toEqual({ + object: "scheduling.out_of_office", + id: entry.id, + deleted: true, + }); + expect((await scheduling.outOfOffice.get(entry.id)).error?.code).toBe("not_found"); + }); +}); + describe("coss.scheduling against the API", () => { test("host, schedule, event type, slots, a booking's life, a hold, a session", async () => { const tenant = await createTestTenant(); diff --git a/apps/scheduling/AGENTS.md b/apps/scheduling/AGENTS.md index b6a674881..175ac2c5b 100644 --- a/apps/scheduling/AGENTS.md +++ b/apps/scheduling/AGENTS.md @@ -1,6 +1,6 @@ # apps/scheduling — the hosted scheduling pages -A host's page (`/h/`), the booker (`/b/`) and an attendee's manage page (`/m/`). Next.js 16 +A host's page (`/h/`), the booker (`/b/`), an attendee's manage page (`/m/`) and a team lead's page (`/t/`). Next.js 16 (:3005 locally), standalone image, on **a registrable domain of its own**, `cossbook.com` (never under coss.com, which would make it same-site with the dashboard). Product rules are in `packages/scheduling/AGENTS.md`. diff --git a/apps/scheduling/src/app/h/[sessionId]/bookings.tsx b/apps/scheduling/src/app/h/[sessionId]/bookings.tsx index ef2e63c07..1d3095b84 100644 --- a/apps/scheduling/src/app/h/[sessionId]/bookings.tsx +++ b/apps/scheduling/src/app/h/[sessionId]/bookings.tsx @@ -19,6 +19,13 @@ import { EmptyMedia, EmptyTitle, } from "@coss/ui/components/empty"; +import { + Select, + SelectItem, + SelectPopup, + SelectTrigger, + SelectValue, +} from "@coss/ui/components/select"; import { Skeleton } from "@coss/ui/components/skeleton"; import { Tabs, TabsList, TabsTab } from "@coss/ui/components/tabs"; import { CalendarIcon } from "lucide-react"; @@ -40,6 +47,86 @@ type HostBooking = { type BookingPage = { data: HostBooking[]; next_cursor: string | null }; +/** Giving a booking away: who's free and takes this, by name, and nothing about anyone else. */ +function GiveAway(props: { call: ClientCall; bookingId: string; onMoved: () => void }) { + const { call, bookingId, onMoved } = props; + const [people, setPeople] = useState<{ host: string; name: string }[] | null>(null); + const [to, setTo] = useState(null); + const [problem, setProblem] = useState(null); + const [working, setWorking] = useState(false); + const open = async () => { + setProblem(null); + try { + const response = await call<{ data: { host: string; name: string }[] }>( + `/host/bookings/${bookingId}/reassign_candidates`, + ); + setPeople(response.data); + } catch (error) { + setProblem((error as Error).message); + } + }; + const give = async () => { + if (!to) return; + setWorking(true); + setProblem(null); + try { + await call(`/host/bookings/${bookingId}/reassign`, { method: "POST", body: { to_host: to } }); + onMoved(); + } catch (error) { + setProblem((error as Error).message); + } finally { + setWorking(false); + } + }; + if (people === null) { + return ( + <> + + {problem ? ( +

+ {problem} +

+ ) : null} + + ); + } + if (people.length === 0) { + return Nobody else is free then.; + } + return ( +
+
+ +
+ + {problem ? ( +

+ {problem} +

+ ) : null} +
+ ); +} + type View = "upcoming" | "past"; const LIST_PATH: Record = { @@ -327,7 +414,10 @@ export function Bookings(props: { call: ClientCall; session: HostSession }) { ) ) : ( -
+
+ {session.can_reassign ? ( + + ) : null} {pending ? ( <>