diff --git a/.changeset/scheduling-oauth-apps.md b/.changeset/scheduling-oauth-apps.md new file mode 100644 index 000000000..191eb1346 --- /dev/null +++ b/.changeset/scheduling-oauth-apps.md @@ -0,0 +1,6 @@ +--- +"coss": minor +"coss-cli": patch +--- + +Scheduling's own OAuth apps: `coss.scheduling.oauthApps` connects hosts' calendars through your own Google, Microsoft or Zoom app, so the consent screen shows your brand. Calendar connections gain `oauth_app`, settings gain `privacy_url`, and there are `scheduling.oauth_app.failing` and `.restored` webhooks. `coss open scheduling-oauth-apps` opens its dashboard page. diff --git a/AGENTS.md b/AGENTS.md index e6daa64d7..523b989f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -270,8 +270,8 @@ variables (`infra/test` enforces the list): a hash that differs between laptops | `LOG_RETENTION_DAYS` | worker: days of environment logs kept (1-31, default 30); whole expired day partitions are dropped hourly | | `SCHEDULING_DATA_KEYS` | api, web, worker: comma-separated 32-byte base64 keys, newest first, encrypting attendees' details and answers. Unset in production → Scheduling is off (503 `scheduling_unavailable`); a fixed development key otherwise | | `SCHEDULING_PUBLIC_URL` | api, web, worker: origin of the hosted scheduling pages (`apps/scheduling`). Default `http://localhost:3005` outside production; unset in production → sessions have no `url` and emails no manage link | -| `GOOGLE_CALENDAR_CLIENT_ID`, `GOOGLE_CALENDAR_CLIENT_SECRET`, `MICROSOFT_CALENDAR_CLIENT_ID`, `MICROSOFT_CALENDAR_CLIENT_SECRET` | api, worker: Scheduling's OAuth apps for calendar access, separate from sign-in's. Redirect URIs `${API_PUBLIC_URL}/scheduling/oauth/{google,microsoft}/callback`. Each pair together, or that provider can't be connected | -| `ZOOM_CLIENT_ID`, `ZOOM_CLIENT_SECRET`, `ZOOM_WEBHOOK_SECRET_TOKEN` | api, worker: Scheduling's Zoom app, for meetings on hosts' accounts. Redirect URI `${API_PUBLIC_URL}/scheduling/oauth/zoom/callback`, deauthorization events at `${API_PUBLIC_URL}/scheduling/zoom/events`, verified with the token. The pair together, or Zoom can't be connected | +| `GOOGLE_CALENDAR_CLIENT_ID`, `GOOGLE_CALENDAR_CLIENT_SECRET`, `MICROSOFT_CALENDAR_CLIENT_ID`, `MICROSOFT_CALENDAR_CLIENT_SECRET` | api, worker: Scheduling's OAuth apps for calendar access, separate from sign-in's. Redirect URIs `${API_PUBLIC_URL}/scheduling/oauth/{google,microsoft}/callback`. Each pair together, or that provider can't be connected except through an environment's own OAuth app | +| `ZOOM_CLIENT_ID`, `ZOOM_CLIENT_SECRET`, `ZOOM_WEBHOOK_SECRET_TOKEN` | api, worker: Scheduling's Zoom app, for meetings on hosts' accounts. Redirect URI `${API_PUBLIC_URL}/scheduling/oauth/zoom/callback`, deauthorization events at `${API_PUBLIC_URL}/scheduling/zoom/events`, verified with the token. The pair together, or Zoom can't be connected except through an environment's own OAuth app | | `SMS_PROVIDER` | api, web, worker: provider of live environments' SMS and of new numbers (`fake` default, or `twilio`). Production with `fake` refuses to buy numbers or submit registrations, so nothing live is sent; the api only needs the name (callbacks are verified with each subaccount's token). Shape in `@coss/sms/env` | | `RISK_PHONE_LOOKUP`, `RISK_PHONE_SIM_SWAP` | api: risk's phone checks in live environments (`off` default: live checks answer 503; `twilio`: Twilio Lookup's line type and carrier with the parent account's `TWILIO_ACCOUNT_SID`/`TWILIO_AUTH_TOKEN`, paid per lookup — Twilio's DPA purpose must cover it first). `RISK_PHONE_SIM_SWAP=true` also asks for SIM swaps (only after Twilio's carrier approval and adding them back to the DPA: the phone check data and Annex 3's Twilio row). Sandbox environments use fixtures | | `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_PRIMARY_CUSTOMER_PROFILE_SID`, `SMS_SUBACCOUNT_PREFIX` | web + worker with `SMS_PROVIDER=twilio` (the first two also api, with `RISK_PHONE_LOOKUP=twilio`): Cal.com's parent Twilio account (shared ISV), the approved primary customer profile organizations' 10DLC brands are registered under, and the prefix of the subaccounts coss.com creates (`-`). Twilio calls back `${API_PUBLIC_URL}/sms/twilio/*` | @@ -534,7 +534,7 @@ notification emails) and, if there is useful advice, to `ADVICE` (`packages/repu 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`, `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`, + `authenticateSchedulingSession` (from the token), `handleCalendarPush`, `completeCalendarAuthorization` (from the push's channel and the consent's state), `handleZoomDeauthorization` (from Zoom's signed event), `zoomOauthAppForEvents` (read only to check that signature), `warnOfExpiringOauthAppSecrets`, `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` @@ -800,7 +800,8 @@ 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: 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 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, 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's own OAuth apps: callback domains served by coss.com (a customer's own page forwards instead), more than one app per provider per environment, and managing them in the iOS app, MCP and CLI, 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 07c47426e..41a082356 100644 --- a/apps/api/openapi.json +++ b/apps/api/openapi.json @@ -4400,6 +4400,86 @@ } ] }, + "SchedulingOauthAppWebhookEvent": { + "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.oauth_app.failing", + "scheduling.oauth_app.restored" + ] + }, + "data": { + "type": "object", + "properties": { + "oauth_app_id": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "What the provider said, when it refused." + }, + "occurred_at": { + "type": "string" + } + }, + "required": [ + "oauth_app_id", + "provider", + "code", + "occurred_at" + ] + } + }, + "required": [ + "id", + "object", + "api_version", + "created_at", + "type", + "data" + ], + "examples": [ + { + "id": "whev_01kd2m9a0b1c2d3e4f5g6h7j8m", + "object": "event", + "type": "scheduling.oauth_app.failing", + "created_at": "2027-01-01T10:00:00.000Z", + "data": { + "oauth_app_id": "calapp_01kd2m9a0b1c2d3e4f5g6h7j8m", + "provider": "google", + "code": "app_refused", + "occurred_at": "2027-01-01T10:00:00.000Z" + } + } + ] + }, "Environment": { "type": "object", "properties": { @@ -9978,10 +10058,12 @@ "scheduling.out_of_office.deleted", "scheduling.out_of_office.started", "scheduling.out_of_office.ended", - "scheduling.reassignment.completed" + "scheduling.reassignment.completed", + "scheduling.oauth_app.failing", + "scheduling.oauth_app.restored" ] }, - "maxItems": 98, + "maxItems": 100, "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": { @@ -10308,10 +10390,12 @@ "scheduling.out_of_office.deleted", "scheduling.out_of_office.started", "scheduling.out_of_office.ended", - "scheduling.reassignment.completed" + "scheduling.reassignment.completed", + "scheduling.oauth_app.failing", + "scheduling.oauth_app.restored" ] }, - "maxItems": 98, + "maxItems": 100, "description": "Event types delivered to this endpoint. An empty array means every event type, including ones added later." }, "api_version": { @@ -10891,10 +10975,12 @@ "scheduling.out_of_office.deleted", "scheduling.out_of_office.started", "scheduling.out_of_office.ended", - "scheduling.reassignment.completed" + "scheduling.reassignment.completed", + "scheduling.oauth_app.failing", + "scheduling.oauth_app.restored" ] }, - "maxItems": 98, + "maxItems": 100, "description": "Event types to receive. Defaults to every event type." }, "api_version": { @@ -11229,6 +11315,12 @@ }, { "$ref": "#/components/schemas/SchedulingReassignmentWebhookEvent" + }, + { + "$ref": "#/components/schemas/SchedulingCalendarDelegationWebhookEvent" + }, + { + "$ref": "#/components/schemas/SchedulingOauthAppWebhookEvent" } ], "description": "The event, as an endpoint on the request's API version receives it. Its `webhook-id` header is `event.id`." @@ -11704,7 +11796,9 @@ "scheduling.out_of_office.deleted", "scheduling.out_of_office.started", "scheduling.out_of_office.ended", - "scheduling.reassignment.completed" + "scheduling.reassignment.completed", + "scheduling.oauth_app.failing", + "scheduling.oauth_app.restored" ], "description": "The event type. `domain.*`, `account.*`, `subaccount.*` and `billing.*` events only reach live environments, so they can't be tested in a sandbox one." }, @@ -25194,6 +25288,13 @@ ], "description": "`needs_reauth`: access was lost, and this host isn't offered until it's back." }, + "oauth_app": { + "type": [ + "string", + "null" + ], + "description": "Your own OAuth app this connection's access came through. `null`: coss.com's, or no consent was involved." + }, "delegation": { "type": [ "string", @@ -25250,6 +25351,7 @@ "provider", "account", "status", + "oauth_app", "delegation", "calendars", "created_at" @@ -25263,6 +25365,7 @@ "account": "ana@example.com", "status": "active", "delegation": null, + "oauth_app": null, "calendars": [ { "id": "cal_01kd2m9a0b1c2d3e4f5g6h7j8m", @@ -25376,6 +25479,7 @@ "account": "ana@example.com", "status": "active", "delegation": null, + "oauth_app": null, "calendars": [ { "id": "cal_01kd2m9a0b1c2d3e4f5g6h7j8m", @@ -25809,6 +25913,458 @@ } ] }, + "SchedulingOauthApp": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "scheduling.oauth_app" + ] + }, + "id": { + "type": "string" + }, + "provider": { + "type": "string", + "enum": [ + "google", + "microsoft", + "zoom" + ] + }, + "client_id": { + "type": "string" + }, + "tenant_id": { + "type": [ + "string", + "null" + ] + }, + "redirect_uri": { + "type": [ + "string", + "null" + ] + }, + "coss_callback_url": { + "type": "string", + "description": "Register this with the provider if `redirect_uri` isn't set. If it is, that page forwards `code` and `state` here." + }, + "completion_url": { + "type": [ + "string", + "null" + ] + }, + "zoom_event_url": { + "type": [ + "string", + "null" + ], + "description": "Zoom only: the Event notification endpoint URL for your app's deauthorization events." + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "What to enable on your app at the provider." + }, + "status": { + "type": "string", + "enum": [ + "active", + "failing" + ], + "description": "`failing`: the provider refused the app's credentials. New consents are refused, and calendars connected through it can't be read until they work again." + }, + "last_error_code": { + "type": [ + "string", + "null" + ] + }, + "last_error_at": { + "type": [ + "string", + "null" + ] + }, + "secret_expires_at": { + "type": [ + "string", + "null" + ] + }, + "verified_at": { + "type": [ + "string", + "null" + ], + "description": "When a consent first completed through this app, which proves its secret." + }, + "connections": { + "type": "integer", + "description": "Calendar connections using this app. It can't be deleted while there are any." + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "object", + "id", + "provider", + "client_id", + "tenant_id", + "redirect_uri", + "coss_callback_url", + "completion_url", + "zoom_event_url", + "scopes", + "status", + "last_error_code", + "last_error_at", + "secret_expires_at", + "verified_at", + "connections", + "created_at", + "updated_at" + ], + "examples": [ + { + "object": "scheduling.oauth_app", + "id": "calapp_01kd2m9a0b1c2d3e4f5g6h7j8m", + "provider": "google", + "client_id": "1234567890-abc.apps.googleusercontent.com", + "tenant_id": null, + "redirect_uri": "https://example.com/oauth/google", + "coss_callback_url": "https://api.coss.com/scheduling/oauth/google/callback", + "completion_url": "https://example.com/calendar-connected", + "zoom_event_url": null, + "scopes": [ + "https://www.googleapis.com/auth/calendar.events.freebusy" + ], + "status": "active", + "last_error_code": null, + "last_error_at": null, + "secret_expires_at": null, + "verified_at": "2027-01-01T10:00:00.000Z", + "connections": 12, + "created_at": "2027-01-01T10:00:00.000Z", + "updated_at": "2027-01-01T10:00:00.000Z" + } + ] + }, + "CreateOauthAppRequest": { + "oneOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "google" + ] + }, + "client_id": { + "type": "string", + "minLength": 1, + "maxLength": 500, + "pattern": "^\\S+$", + "description": "The app's client ID at the provider." + }, + "client_secret": { + "type": "string", + "minLength": 1, + "maxLength": 1000, + "description": "The app's client secret. Never returned." + }, + "redirect_uri": { + "type": "string", + "maxLength": 2000, + "description": "A page on your own domain, registered with the provider, that forwards `code` and `state` to `coss_callback_url`. Google requires this for an external app, since it has to verify the redirect's domain as yours. Leave it out to use `coss_callback_url` directly." + }, + "completion_url": { + "type": "string", + "maxLength": 2000, + "description": "Where the browser lands after a consent that didn't name a `return_url`, with `coss_calendar` set to `connected`, `denied` or `invalid`. Leave it out for a plain page saying what happened." + }, + "secret_expires_at": { + "type": "string", + "format": "date-time", + "description": "When the client secret expires. Owners and admins are warned 14 days before." + } + }, + "required": [ + "provider", + "client_id", + "client_secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "microsoft" + ] + }, + "client_id": { + "type": "string", + "minLength": 1, + "maxLength": 500, + "pattern": "^\\S+$", + "description": "The app's client ID at the provider." + }, + "client_secret": { + "type": "string", + "minLength": 1, + "maxLength": 1000, + "description": "The app's client secret. Never returned." + }, + "tenant_id": { + "type": "string", + "pattern": "^[a-zA-Z0-9](?:[a-zA-Z0-9.-]{0,253})$", + "description": "The Directory (tenant) ID, for an app that's only for your own tenant. Leave it out for a multi-tenant app." + }, + "redirect_uri": { + "type": "string", + "maxLength": 2000, + "description": "A page on your own domain, registered with the provider, that forwards `code` and `state` to `coss_callback_url`. Google requires this for an external app, since it has to verify the redirect's domain as yours. Leave it out to use `coss_callback_url` directly." + }, + "completion_url": { + "type": "string", + "maxLength": 2000, + "description": "Where the browser lands after a consent that didn't name a `return_url`, with `coss_calendar` set to `connected`, `denied` or `invalid`. Leave it out for a plain page saying what happened." + }, + "secret_expires_at": { + "type": "string", + "format": "date-time", + "description": "When the client secret expires. Owners and admins are warned 14 days before." + } + }, + "required": [ + "provider", + "client_id", + "client_secret" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "zoom" + ] + }, + "client_id": { + "type": "string", + "minLength": 1, + "maxLength": 500, + "pattern": "^\\S+$", + "description": "The app's client ID at the provider." + }, + "client_secret": { + "type": "string", + "minLength": 1, + "maxLength": 1000, + "description": "The app's client secret. Never returned." + }, + "webhook_secret": { + "type": "string", + "minLength": 1, + "maxLength": 1000, + "description": "The Secret Token of your Zoom app, which signs its events at `zoom_event_url`. Never returned." + }, + "redirect_uri": { + "type": "string", + "maxLength": 2000, + "description": "A page on your own domain, registered with the provider, that forwards `code` and `state` to `coss_callback_url`. Google requires this for an external app, since it has to verify the redirect's domain as yours. Leave it out to use `coss_callback_url` directly." + }, + "completion_url": { + "type": "string", + "maxLength": 2000, + "description": "Where the browser lands after a consent that didn't name a `return_url`, with `coss_calendar` set to `connected`, `denied` or `invalid`. Leave it out for a plain page saying what happened." + }, + "secret_expires_at": { + "type": "string", + "format": "date-time", + "description": "When the client secret expires. Owners and admins are warned 14 days before." + } + }, + "required": [ + "provider", + "client_id", + "client_secret" + ], + "additionalProperties": false + } + ], + "examples": [ + { + "provider": "google", + "client_id": "1234567890-abc.apps.googleusercontent.com", + "client_secret": "GOCSPX-abcdefghijklmnopqrstuvwxyz", + "redirect_uri": "https://example.com/oauth/google", + "completion_url": "https://example.com/calendar-connected" + } + ] + }, + "SchedulingOauthAppList": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulingOauthApp" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "object", + "data", + "has_more" + ], + "examples": [ + { + "object": "list", + "data": [ + { + "object": "scheduling.oauth_app", + "id": "calapp_01kd2m9a0b1c2d3e4f5g6h7j8m", + "provider": "google", + "client_id": "1234567890-abc.apps.googleusercontent.com", + "tenant_id": null, + "redirect_uri": "https://example.com/oauth/google", + "coss_callback_url": "https://api.coss.com/scheduling/oauth/google/callback", + "completion_url": "https://example.com/calendar-connected", + "zoom_event_url": null, + "scopes": [ + "https://www.googleapis.com/auth/calendar.events.freebusy" + ], + "status": "active", + "last_error_code": null, + "last_error_at": null, + "secret_expires_at": null, + "verified_at": "2027-01-01T10:00:00.000Z", + "connections": 12, + "created_at": "2027-01-01T10:00:00.000Z", + "updated_at": "2027-01-01T10:00:00.000Z" + } + ], + "has_more": false + } + ] + }, + "UpdateOauthAppRequest": { + "type": "object", + "properties": { + "client_secret": { + "type": "string", + "minLength": 1, + "maxLength": 1000, + "description": "A new client secret. It's tried on a connection before it replaces the old one." + }, + "tenant_id": { + "type": [ + "string", + "null" + ], + "pattern": "^[a-zA-Z0-9](?:[a-zA-Z0-9.-]{0,253})$", + "description": "The Directory (tenant) ID, for an app that's only for your own tenant. Leave it out for a multi-tenant app." + }, + "webhook_secret": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 1000, + "description": "The Secret Token of your Zoom app, which signs its events at `zoom_event_url`. Never returned." + }, + "redirect_uri": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "description": "A page on your own domain, registered with the provider, that forwards `code` and `state` to `coss_callback_url`. Google requires this for an external app, since it has to verify the redirect's domain as yours. Leave it out to use `coss_callback_url` directly." + }, + "completion_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "description": "Where the browser lands after a consent that didn't name a `return_url`, with `coss_calendar` set to `connected`, `denied` or `invalid`. Leave it out for a plain page saying what happened." + }, + "secret_expires_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When the client secret expires. Owners and admins are warned 14 days before." + } + }, + "additionalProperties": false, + "examples": [ + { + "client_secret": "GOCSPX-abcdefghijklmnopqrstuvwxyz" + } + ] + }, + "DeletedSchedulingOauthApp": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "scheduling.oauth_app" + ] + }, + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "object", + "id", + "deleted" + ], + "examples": [ + { + "object": "scheduling.oauth_app", + "id": "calapp_01kd2m9a0b1c2d3e4f5g6h7j8m", + "deleted": true + } + ] + }, "SetSimulatedBusyRequest": { "type": "object", "properties": { @@ -26137,6 +26693,13 @@ ], "description": "At the top of the hosted pages, and of emails sent from `email_from`." }, + "privacy_url": { + "type": [ + "string", + "null" + ], + "description": "Your privacy policy, linked on a host's page beside what connecting a calendar does, once this environment has its own OAuth app and a `brand_name`." + }, "email_from": { "type": [ "string", @@ -26237,6 +26800,7 @@ "brand_name", "accent_color", "logo", + "privacy_url", "email_from", "sms_from", "name_host_in_attendee_emails", @@ -26256,6 +26820,7 @@ "brand_name": "Acme Health", "accent_color": null, "logo": null, + "privacy_url": null, "email_from": "appointments@example.com", "sms_from": null, "name_host_in_attendee_emails": false, @@ -26319,6 +26884,15 @@ "additionalProperties": false, "description": "Your logo, a PNG or JPEG. `null` removes it." }, + "privacy_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2000, + "format": "uri", + "description": "Your privacy policy, as an https URL, for hosts connecting a calendar through your own OAuth app. `null` removes it." + }, "email_from": { "type": [ "string", @@ -40680,8 +41254,555 @@ } } }, - "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "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" + ] + } + }, + "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" + ] + } + }, + "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": { + "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" + ] + } + }, + "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" + ] + } + }, + "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" + ] + } + }, + "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.", @@ -40773,14 +41894,14 @@ } } }, - "/v1/emails/batch": { - "post": { - "operationId": "sendEmailBatch", + "/v1/emails/{id}": { + "get": { + "operationId": "getEmail", "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.", + "summary": "Retrieve an email", + "description": "Only emails sent from the API key's own environment can be retrieved.", "security": [ { "bearerAuth": [] @@ -40790,35 +41911,23 @@ { "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.", + "description": "The email's id.", "examples": [ - "welcome-user-123" + "email_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, - "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" + "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/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.", + "200": { + "description": "The email.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -40829,15 +41938,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": { @@ -40851,13 +41951,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailBatch" + "$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`).", + "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.", @@ -40898,8 +41998,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.", + "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.", @@ -40919,8 +42019,8 @@ } } }, - "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`).", + "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.", @@ -40940,8 +42040,8 @@ } } }, - "409": { - "description": "Requests with this `Idempotency-Key` are racing for an expired key (`request_in_progress`). Retry after `Retry-After` seconds.", + "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.", @@ -40953,7 +42053,7 @@ } }, "Retry-After": { - "description": "Seconds to wait.", + "description": "Seconds to wait before retrying.", "schema": { "type": "integer" } @@ -40967,8 +42067,8 @@ } } }, - "413": { - "description": "The request body is larger than 40 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.", @@ -40988,8 +42088,157 @@ } } }, - "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.", + "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" + ] + } + }, + "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.", @@ -41010,7 +42259,7 @@ } }, "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).", + "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.", @@ -41031,7 +42280,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.", @@ -41043,7 +42292,7 @@ } }, "Retry-After": { - "description": "Seconds to wait.", + "description": "Seconds to wait before retrying.", "schema": { "type": "integer" } @@ -41102,14 +42351,14 @@ } } }, - "/v1/emails/batch/{id}": { - "get": { - "operationId": "getEmailBatch", + "/v1/emails/{id}/reschedule": { + "post": { + "operationId": "rescheduleEmail", "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.", + "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": [] @@ -41119,13 +42368,13 @@ { "schema": { "type": "string", - "description": "The batch's id (`batch_id` of its emails).", + "description": "The email's id.", "examples": [ - "batch_01k8d0n6x5f3w2e1r9t7y4u6i8" + "email_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The batch's id (`batch_id` of its emails).", + "description": "The email's id.", "name": "id", "in": "path" }, @@ -41133,9 +42382,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RescheduleEmailRequest" + } + } + } + }, "responses": { "200": { - "description": "The batch.", + "description": "The rescheduled email.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41159,13 +42418,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailBatch" + "$ref": "#/components/schemas/Email" } } } }, "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.", @@ -41228,7 +42487,49 @@ } }, "404": { - "description": "No batch with this id exists in the API key's environment (`not_found`).", + "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.", @@ -41320,14 +42621,14 @@ } } }, - "/v1/emails/{id}": { + "/v1/emails/{id}/events": { "get": { - "operationId": "getEmail", + "operationId": "listEmailEvents", "tags": [ "Emails" ], - "summary": "Retrieve an email", - "description": "Only emails sent from the API key's own environment can be retrieved.", + "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": [] @@ -41353,7 +42654,7 @@ ], "responses": { "200": { - "description": "The email.", + "description": "The email's events.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41377,7 +42678,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Email" + "$ref": "#/components/schemas/EmailEventList" } } } @@ -41538,14 +42839,14 @@ } } }, - "/v1/emails/{id}/cancel": { - "post": { - "operationId": "cancelEmail", + "/v1/emails/{id}/links": { + "get": { + "operationId": "listEmailLinks", "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.", + "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": [] @@ -41571,7 +42872,7 @@ ], "responses": { "200": { - "description": "The canceled email.", + "description": "The email's tracked links.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41595,7 +42896,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Email" + "$ref": "#/components/schemas/EmailLinkList" } } } @@ -41684,27 +42985,6 @@ } } }, - "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": { @@ -41777,50 +43057,32 @@ } } }, - "/v1/emails/{id}/reschedule": { + "/v1/email_templates": { "post": { - "operationId": "rescheduleEmail", + "operationId": "createEmailTemplate", "tags": [ - "Emails" + "Email templates" ], - "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": "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": [] } ], - "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" + "$ref": "#/components/schemas/CreateEmailTemplateRequest" } } } }, "responses": { "200": { - "description": "The rescheduled email.", + "description": "The template.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -41844,7 +43106,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Email" + "$ref": "#/components/schemas/EmailTemplate" } } } @@ -41912,8 +43174,8 @@ } } }, - "404": { - "description": "No email with this id exists in the API key's environment (`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.", @@ -41955,7 +43217,7 @@ } }, "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`.", + "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.", @@ -42044,35 +43306,47 @@ } } } - } - } - }, - "/v1/emails/{id}/events": { + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, "get": { - "operationId": "listEmailEvents", + "operationId": "listEmailTemplates", "tags": [ - "Emails" + "Email templates" ], - "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": "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": "The email's id.", - "examples": [ - "email_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "description": "Cursor: the `id` of the last template of the previous page." }, - "required": true, - "description": "The email's id.", - "name": "id", - "in": "path" + "required": false, + "description": "Cursor: the `id` of the last template of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -42080,7 +43354,7 @@ ], "responses": { "200": { - "description": "The email's events.", + "description": "A page of templates.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42104,7 +43378,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailEventList" + "$ref": "#/components/schemas/EmailTemplateList" } } } @@ -42172,8 +43446,8 @@ } } }, - "404": { - "description": "No email 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.", @@ -42265,14 +43539,14 @@ } } }, - "/v1/emails/{id}/links": { + "/v1/email_templates/{id}": { "get": { - "operationId": "listEmailLinks", + "operationId": "getEmailTemplate", "tags": [ - "Emails" + "Email templates" ], - "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": "Retrieve an email template", + "description": "A template by id or alias. Its content is in its draft and versions.", "security": [ { "bearerAuth": [] @@ -42282,13 +43556,13 @@ { "schema": { "type": "string", - "description": "The email's id.", + "description": "The template's id or alias.", "examples": [ - "email_01k7c8m4q2f3e9v6t5r1n0p8yz" + "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The email's id.", + "description": "The template's id or alias.", "name": "id", "in": "path" }, @@ -42298,7 +43572,7 @@ ], "responses": { "200": { - "description": "The email's tracked links.", + "description": "The template.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42322,7 +43596,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailLinkList" + "$ref": "#/components/schemas/EmailTemplate" } } } @@ -42391,7 +43665,7 @@ } }, "404": { - "description": "No email with this id exists in the API key's environment (`not_found`).", + "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.", @@ -42481,27 +43755,43 @@ } } } - } - }, - "/v1/email_templates": { - "post": { - "operationId": "createEmailTemplate", + }, + "patch": { + "operationId": "updateEmailTemplate", "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.", + "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" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateEmailTemplateRequest" + "$ref": "#/components/schemas/UpdateEmailTemplateRequest" } } } @@ -42600,6 +43890,27 @@ } } }, + "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": { @@ -42643,7 +43954,7 @@ } }, "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`).", + "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.", @@ -42732,47 +44043,33 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } }, - "get": { - "operationId": "listEmailTemplates", + "delete": { + "operationId": "deleteEmailTemplate", "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.", + "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": "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." + "description": "The template's id or alias.", + "examples": [ + "etpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "required": false, - "description": "Cursor: the `id` of the last template of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The template's id or alias.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -42780,7 +44077,7 @@ ], "responses": { "200": { - "description": "A page of templates.", + "description": "The template was deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -42804,7 +44101,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplateList" + "$ref": "#/components/schemas/DeletedEmailTemplate" } } } @@ -42872,8 +44169,8 @@ } } }, - "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "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.", @@ -42965,14 +44262,14 @@ } } }, - "/v1/email_templates/{id}": { + "/v1/email_templates/{id}/draft": { "get": { - "operationId": "getEmailTemplate", + "operationId": "getEmailTemplateDraft", "tags": [ "Email templates" ], - "summary": "Retrieve an email template", - "description": "A template by id or alias. Its content is in its draft and versions.", + "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": [ { "bearerAuth": [] @@ -42998,7 +44295,7 @@ ], "responses": { "200": { - "description": "The template.", + "description": "The draft.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43022,7 +44319,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplate" + "$ref": "#/components/schemas/EmailTemplateVersion" } } } @@ -43182,13 +44479,13 @@ } } }, - "patch": { - "operationId": "updateEmailTemplate", + "put": { + "operationId": "updateEmailTemplateDraft", "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`).", + "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": [] @@ -43217,14 +44514,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateEmailTemplateRequest" + "$ref": "#/components/schemas/UpdateEmailTemplateDraftRequest" } } } }, "responses": { "200": { - "description": "The template.", + "description": "The saved draft.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43248,7 +44545,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplate" + "$ref": "#/components/schemas/EmailTemplateVersion" } } } @@ -43380,7 +44677,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`), or another template has the alias (`template_alias_taken`).", + "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.", @@ -43470,14 +44767,16 @@ } } } - }, - "delete": { - "operationId": "deleteEmailTemplate", + } + }, + "/v1/email_templates/{id}/publish": { + "post": { + "operationId": "publishEmailTemplate", "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`.", + "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": [ { "bearerAuth": [] @@ -43501,9 +44800,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublishEmailTemplateRequest" + } + } + } + }, "responses": { "200": { - "description": "The template was deleted.", + "description": "The published version.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43527,13 +44836,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedEmailTemplate" + "$ref": "#/components/schemas/EmailTemplateVersion" } } } }, "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.", @@ -43616,6 +44925,69 @@ } } }, + "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 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" + } + } + } + }, "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": { @@ -43688,14 +45060,14 @@ } } }, - "/v1/email_templates/{id}/draft": { - "get": { - "operationId": "getEmailTemplateDraft", + "/v1/email_templates/{id}/render": { + "post": { + "operationId": "renderEmailTemplate", "tags": [ "Email templates" ], - "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": "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": [ { "bearerAuth": [] @@ -43719,9 +45091,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RenderEmailTemplateRequest" + } + } + } + }, "responses": { "200": { - "description": "The draft.", + "description": "The rendered template.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43745,13 +45127,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplateVersion" + "$ref": "#/components/schemas/RenderedEmailTemplate" } } } }, "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.", @@ -43834,6 +45216,69 @@ } } }, + "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`).", + "headers": { + "X-Request-Id": { + "description": "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": { @@ -43904,14 +45349,16 @@ } } } - }, - "put": { - "operationId": "updateEmailTemplateDraft", + } + }, + "/v1/email_templates/{id}/versions": { + "get": { + "operationId": "listEmailTemplateVersions", "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`).", + "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": [ { "bearerAuth": [] @@ -43931,23 +45378,36 @@ "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/UpdateEmailTemplateDraftRequest" - } - } - } - }, "responses": { "200": { - "description": "The saved draft.", + "description": "A page of versions.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -43971,13 +45431,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplateVersion" + "$ref": "#/components/schemas/EmailTemplateVersionList" } } } }, "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.", @@ -44060,50 +45520,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 draft changed since `revision` (`template_revision_mismatch`).", + "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.", @@ -44195,14 +45613,14 @@ } } }, - "/v1/email_templates/{id}/publish": { - "post": { - "operationId": "publishEmailTemplate", + "/v1/email_templates/{id}/versions/{version}": { + "get": { + "operationId": "getEmailTemplateVersion", "tags": [ "Email templates" ], - "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": "Retrieve a template version", + "description": "A published version, which never changes.", "security": [ { "bearerAuth": [] @@ -44222,23 +45640,27 @@ "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/PublishEmailTemplateRequest" - } - } - } - }, "responses": { "200": { - "description": "The published version.", + "description": "The version.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44268,7 +45690,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.", @@ -44331,70 +45753,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 draft has problems or no content for its default locale (`template_invalid`, see `issues`), or changed since `revision` (`template_revision_mismatch`).", + "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.", @@ -44486,14 +45845,14 @@ } } }, - "/v1/email_templates/{id}/render": { + "/v1/email_templates/{id}/versions/{version}/restore": { "post": { - "operationId": "renderEmailTemplate", + "operationId": "restoreEmailTemplateVersion", "tags": [ "Email templates" ], - "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": "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": [] @@ -44513,23 +45872,27 @@ "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/RenderEmailTemplateRequest" - } - } - } - }, "responses": { "200": { - "description": "The rendered template.", + "description": "The draft.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44553,13 +45916,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RenderedEmailTemplate" + "$ref": "#/components/schemas/EmailTemplateVersion" } } } }, "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.", @@ -44642,50 +46005,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 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": "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.", @@ -44777,63 +46098,32 @@ } } }, - "/v1/email_templates/{id}/versions": { - "get": { - "operationId": "listEmailTemplateVersions", + "/v1/domains": { + "post": { + "operationId": "createDomain", "tags": [ - "Email templates" + "Domains" ], - "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": "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": [] } ], - "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, - "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/CreateDomainRequest" + } + } } - ], + }, "responses": { "200": { - "description": "A page of versions.", + "description": "The domain, with the DNS records to publish.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -44857,13 +46147,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplateVersionList" + "$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.", @@ -44904,6 +46194,27 @@ } } }, + "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": { @@ -44925,8 +46236,29 @@ } } }, - "404": { - "description": "No template with this id or alias exists in the API key's environment (`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.", + "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.", @@ -44947,7 +46279,7 @@ } }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "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.", @@ -45015,6 +46347,27 @@ } } }, + "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": { @@ -45036,17 +46389,20 @@ } } } - } - } - }, - "/v1/email_templates/{id}/versions/{version}": { + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, "get": { - "operationId": "getEmailTemplateVersion", + "operationId": "listDomains", "tags": [ - "Email templates" + "Domains" ], - "summary": "Retrieve a template version", - "description": "A published version, which never changes.", + "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": [] @@ -45056,29 +46412,14 @@ { "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 - ] + "minLength": 1, + "maxLength": 256, + "description": "Only this sub-account's domains (its id or your uid)." }, - "required": true, - "description": "A published version number.", - "name": "version", - "in": "path" + "required": false, + "description": "Only this sub-account's domains (its id or your uid).", + "name": "subaccount", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -45086,7 +46427,7 @@ ], "responses": { "200": { - "description": "The version.", + "description": "The organization's domains.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45110,7 +46451,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplateVersion" + "$ref": "#/components/schemas/DomainList" } } } @@ -45178,8 +46519,8 @@ } } }, - "404": { - "description": "No template with this id or alias exists in the API key's environment, or it has no such version (`not_found`).", + "422": { + "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.", @@ -45271,14 +46612,14 @@ } } }, - "/v1/email_templates/{id}/versions/{version}/restore": { - "post": { - "operationId": "restoreEmailTemplateVersion", + "/v1/domains/{id}": { + "patch": { + "operationId": "updateDomain", "tags": [ - "Email templates" + "Domains" ], - "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": "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": [] @@ -45288,37 +46629,33 @@ { "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": "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/UpdateDomainRequest" + } + } + } + }, "responses": { "200": { - "description": "The draft.", + "description": "The domain.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45342,13 +46679,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailTemplateVersion" + "$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.", @@ -45411,7 +46748,49 @@ } }, "404": { - "description": "No template with this id or alias 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.", @@ -45432,7 +46811,7 @@ } }, "422": { - "description": "The template has no such version (`template_version_not_found`).", + "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.", @@ -45522,34 +46901,40 @@ } } } - } - }, - "/v1/domains": { - "post": { - "operationId": "createDomain", + }, + "get": { + "operationId": "getDomain", "tags": [ "Domains" ], - "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 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": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateDomainRequest" - } - } + "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, with the DNS records to publish.", + "description": "The domain.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45579,7 +46964,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.", @@ -45620,27 +47005,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": { @@ -45662,50 +47026,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 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.", @@ -45773,27 +47095,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": { @@ -45815,20 +47116,15 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } }, - "get": { - "operationId": "listDomains", + "delete": { + "operationId": "deleteDomain", "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.", + "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": [] @@ -45838,14 +47134,15 @@ { "schema": { "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Only this sub-account's domains (its id or your uid)." + "description": "The domain's id.", + "examples": [ + "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "required": false, - "description": "Only this sub-account's domains (its id or your uid).", - "name": "subaccount", - "in": "query" + "required": true, + "description": "The domain's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -45853,7 +47150,7 @@ ], "responses": { "200": { - "description": "The organization's domains.", + "description": "The domain was removed.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -45877,7 +47174,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DomainList" + "$ref": "#/components/schemas/DeletedDomain" } } } @@ -45945,8 +47242,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": "`subaccount` names no sub-account of the API key's environment (`subaccount_not_found`).", + "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.", @@ -46014,6 +47332,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": { @@ -46038,14 +47377,14 @@ } } }, - "/v1/domains/{id}": { - "patch": { - "operationId": "updateDomain", + "/v1/domains/{id}/verify": { + "post": { + "operationId": "verifyDomain", "tags": [ "Domains" ], - "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": "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": [] @@ -46069,19 +47408,9 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateDomainRequest" - } - } - } - }, "responses": { "200": { - "description": "The domain.", + "description": "The domain, as the check left it.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -46111,7 +47440,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.", @@ -46194,8 +47523,8 @@ } } }, - "413": { - "description": "The request body is larger than 5 MB (`payload_too_large`).", + "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.", @@ -46215,8 +47544,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "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.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -46226,26 +47555,11 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "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.", + }, + "Retry-After": { + "description": "Seconds to wait.", "schema": { - "type": "string", - "examples": [ - "req_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "type": "integer" } } }, @@ -46257,8 +47571,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.", @@ -46268,12 +47582,6 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Retry-After": { - "description": "Seconds to wait before retrying.", - "schema": { - "type": "integer" - } } }, "content": { @@ -46284,8 +47592,8 @@ } } }, - "500": { - "description": "Unexpected server error (`api_error` / `internal_error`).", + "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.", @@ -46327,14 +47635,16 @@ } } } - }, - "get": { - "operationId": "getDomain", + } + }, + "/v1/domains/{id}/tracking": { + "post": { + "operationId": "createTrackingDomain", "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.", + "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": [] @@ -46358,9 +47668,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTrackingDomainRequest" + } + } + } + }, "responses": { "200": { - "description": "The domain.", + "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.", @@ -46390,7 +47710,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.", @@ -46431,6 +47751,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": { @@ -46473,6 +47814,69 @@ } } }, + "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 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.", + "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": { @@ -46521,6 +47925,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": { @@ -46545,12 +47970,12 @@ } }, "delete": { - "operationId": "deleteDomain", + "operationId": "deleteTrackingDomain", "tags": [ "Domains" ], - "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": "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": [] @@ -46576,7 +48001,7 @@ ], "responses": { "200": { - "description": "The domain was removed.", + "description": "The domain, without a tracking domain.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -46600,7 +48025,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedDomain" + "$ref": "#/components/schemas/Domain" } } } @@ -46669,28 +48094,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 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.", @@ -46759,7 +48163,7 @@ } }, "502": { - "description": "The delivery provider could not remove the domain; retry (`provider_error`).", + "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.", @@ -46803,14 +48207,14 @@ } } }, - "/v1/domains/{id}/verify": { + "/v1/domains/{id}/receiving": { "post": { - "operationId": "verifyDomain", + "operationId": "createReceivingDomain", "tags": [ "Domains" ], - "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": "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": [] @@ -46834,9 +48238,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReceivingDomainRequest" + } + } + } + }, "responses": { "200": { - "description": "The domain, as the check left it.", + "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.", @@ -46866,7 +48280,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.", @@ -46949,8 +48363,8 @@ } } }, - "422": { - "description": "The organization already has 5 domains awaiting verification, so a `failed` one cannot restart (`too_many_pending_domains`).", + "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.", @@ -46970,8 +48384,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.", + "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.", @@ -46981,11 +48395,26 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Retry-After": { - "description": "Seconds to wait.", + } + }, + "content": { + "application/json": { "schema": { - "type": "integer" + "$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.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] } } }, @@ -46997,8 +48426,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.", @@ -47008,6 +48437,12 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } } }, "content": { @@ -47018,8 +48453,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.", @@ -47061,16 +48496,14 @@ } } } - } - }, - "/v1/domains/{id}/tracking": { - "post": { - "operationId": "createTrackingDomain", + }, + "delete": { + "operationId": "deleteReceivingDomain", "tags": [ "Domains" ], - "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": "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": [] @@ -47094,19 +48527,9 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTrackingDomainRequest" - } - } - } - }, "responses": { "200": { - "description": "The domain, with its tracking domain and the CNAME to publish.", + "description": "The domain, without a receiving domain.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -47136,7 +48559,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.", @@ -47177,27 +48600,6 @@ } } }, - "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": { @@ -47220,70 +48622,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`).", - "headers": { - "X-Request-Id": { - "description": "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 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": "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.", @@ -47351,27 +48690,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": { @@ -47394,14 +48712,16 @@ } } } - }, - "delete": { - "operationId": "deleteTrackingDomain", + } + }, + "/v1/domains/{id}/dmarc": { + "get": { + "operationId": "getDomainDmarc", "tags": [ "Domains" ], - "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": "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": [] @@ -47427,7 +48747,7 @@ ], "responses": { "200": { - "description": "The domain, without a tracking domain.", + "description": "The domain's DMARC status.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -47451,7 +48771,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/DomainDmarc" } } } @@ -47520,7 +48840,7 @@ } }, "404": { - "description": "No domain with this id exists in this organization, or it has no tracking domain (`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.", @@ -47588,27 +48908,6 @@ } } }, - "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.", - "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": { @@ -47633,14 +48932,14 @@ } } }, - "/v1/domains/{id}/receiving": { - "post": { - "operationId": "createReceivingDomain", + "/v1/domains/{id}/dmarc/reports": { + "get": { + "operationId": "listDomainDmarcReports", "tags": [ "Domains" ], - "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": "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": [] @@ -47660,23 +48959,36 @@ "name": "id", "in": "path" }, + { + "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 report of the previous page." + }, + "required": false, + "description": "Cursor: the `id` of the last report of the previous page.", + "name": "after", + "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": "A page of DMARC reports.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -47700,13 +49012,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/DmarcReportList" } } } }, "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.", @@ -47789,50 +49101,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 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`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -47922,14 +49192,16 @@ } } } - }, - "delete": { - "operationId": "deleteReceivingDomain", + } + }, + "/v1/domains/{id}/dmarc/reports/{report_id}": { + "get": { + "operationId": "getDomainDmarcReport", "tags": [ "Domains" ], - "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": "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": [] @@ -47949,13 +49221,26 @@ "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 domain, without a receiving domain.", + "description": "The report.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -47979,7 +49264,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/DmarcReport" } } } @@ -48048,7 +49333,7 @@ } }, "404": { - "description": "No domain with this id exists in this organization, or it does not receive email (`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.", @@ -48140,40 +49425,22 @@ } } }, - "/v1/domains/{id}/dmarc": { + "/v1/dedicated_ips": { "get": { - "operationId": "getDomainDmarc", + "operationId": "getDedicatedIpPool", "tags": [ - "Domains" + "Dedicated IPs" ], - "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.", + "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 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.", + "description": "The organization's dedicated IPs.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48197,7 +49464,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DomainDmarc" + "$ref": "#/components/schemas/DedicatedIpPool" } } } @@ -48265,27 +49532,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" - } - } - } - }, "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": { @@ -48355,66 +49601,40 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/domains/{id}/dmarc/reports": { - "get": { - "operationId": "listDomainDmarcReports", + "/v1/webhook_endpoints": { + "post": { + "operationId": "createWebhookEndpoint", "tags": [ - "Domains" + "Webhook endpoints" ], - "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": "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": [] } ], - "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 20." - }, - "required": false, - "description": "1-100, default 20.", - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "maxLength": 64, - "description": "Cursor: the `id` of the last report of the previous page." - }, - "required": false, - "description": "Cursor: the `id` of the last report of the previous page.", - "name": "after", - "in": "query" - }, - { - "$ref": "#/components/parameters/CossVersion" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookEndpointRequest" + } + } } - ], + }, "responses": { "200": { - "description": "A page of DMARC reports.", + "description": "The endpoint and its signing secret.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48438,13 +49658,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DmarcReportList" + "$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.", @@ -48506,8 +49726,29 @@ } } }, - "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.", + "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.", @@ -48528,7 +49769,7 @@ } }, "422": { - "description": "A query parameter failed validation (`validation_error`).", + "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.", @@ -48617,56 +49858,28 @@ } } } - } - } - }, - "/v1/domains/{id}/dmarc/reports/{report_id}": { + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, "get": { - "operationId": "getDomainDmarcReport", + "operationId": "listWebhookEndpoints", "tags": [ - "Domains" + "Webhook endpoints" ], - "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": "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 domain's id.", - "examples": [ - "dom_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "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 report.", + "description": "The environment's webhook endpoints.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48690,7 +49903,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DmarcReport" + "$ref": "#/components/schemas/WebhookEndpointList" } } } @@ -48758,27 +49971,6 @@ } } }, - "404": { - "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.", - "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": { @@ -48848,25 +50040,48 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/dedicated_ips": { + "/v1/webhook_endpoints/{id}": { "get": { - "operationId": "getDedicatedIpPool", + "operationId": "getWebhookEndpoint", "tags": [ - "Dedicated IPs" + "Webhook endpoints" ], - "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": "Retrieve a webhook endpoint", + "description": "The endpoint without its signing secret, which is only returned when created or rolled.", "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 organization's dedicated IPs.", + "description": "The webhook endpoint.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -48890,7 +50105,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DedicatedIpPool" + "$ref": "#/components/schemas/WebhookEndpoint" } } } @@ -48958,6 +50173,27 @@ } } }, + "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" + } + } + } + }, "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": { @@ -49027,40 +50263,51 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - } - }, - "/v1/webhook_endpoints": { - "post": { - "operationId": "createWebhookEndpoint", + } + }, + "patch": { + "operationId": "updateWebhookEndpoint", "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).", + "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": [] } ], + "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" + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateWebhookEndpointRequest" + "$ref": "#/components/schemas/UpdateWebhookEndpointRequest" } } } }, "responses": { "200": { - "description": "The endpoint and its signing secret.", + "description": "The updated endpoint.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49084,7 +50331,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEndpointWithSecret" + "$ref": "#/components/schemas/WebhookEndpoint" } } } @@ -49152,6 +50399,27 @@ } } }, + "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": { @@ -49195,7 +50463,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`) 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.", @@ -49284,28 +50552,41 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } }, - "get": { - "operationId": "listWebhookEndpoints", + "delete": { + "operationId": "deleteWebhookEndpoint", "tags": [ "Webhook endpoints" ], - "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": "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 environment's webhook endpoints.", + "description": "The endpoint was deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49329,7 +50610,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEndpointList" + "$ref": "#/components/schemas/DeletedWebhookEndpoint" } } } @@ -49397,6 +50678,27 @@ } } }, + "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" + } + } + } + }, "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": { @@ -49466,22 +50768,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/webhook_endpoints/{id}": { - "get": { - "operationId": "getWebhookEndpoint", + "/v1/webhook_endpoints/{id}/roll_secret": { + "post": { + "operationId": "rollWebhookEndpointSecret", "tags": [ "Webhook endpoints" ], - "summary": "Retrieve a webhook endpoint", - "description": "The endpoint without its signing secret, which is only returned when created or rolled.", + "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": [] @@ -49505,9 +50802,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RollWebhookEndpointSecretRequest" + } + } + } + }, "responses": { "200": { - "description": "The webhook endpoint.", + "description": "The endpoint and its new signing secret.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49531,13 +50838,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEndpoint" + "$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.", @@ -49620,6 +50927,69 @@ } } }, + "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.", + "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": { @@ -49690,14 +51060,16 @@ } } } - }, - "patch": { - "operationId": "updateWebhookEndpoint", + } + }, + "/v1/webhook_endpoints/{id}/recover": { + "post": { + "operationId": "recoverWebhookEndpoint", "tags": [ "Webhook endpoints" ], - "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": "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": [] @@ -49726,14 +51098,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateWebhookEndpointRequest" + "$ref": "#/components/schemas/RecoverWebhookEndpointRequest" } } } }, "responses": { "200": { - "description": "The updated endpoint.", + "description": "How many deliveries were queued.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -49757,7 +51129,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEndpoint" + "$ref": "#/components/schemas/WebhookEndpointRecovery" } } } @@ -49889,7 +51261,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`) or the URL is not a usable https:// URL (`invalid_url`).", + "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.", @@ -49979,14 +51351,16 @@ } } } - }, - "delete": { - "operationId": "deleteWebhookEndpoint", + } + }, + "/v1/webhook_endpoints/{id}/deliveries": { + "get": { + "operationId": "listWebhookDeliveries", "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.", + "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": [] @@ -50006,13 +51380,60 @@ "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, + "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 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" } ], "responses": { "200": { - "description": "The endpoint was deleted.", + "description": "A page of deliveries.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -50036,7 +51457,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedWebhookEndpoint" + "$ref": "#/components/schemas/WebhookDeliveryList" } } } @@ -50125,6 +51546,27 @@ } } }, + "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": { @@ -50197,14 +51639,14 @@ } } }, - "/v1/webhook_endpoints/{id}/roll_secret": { - "post": { - "operationId": "rollWebhookEndpointSecret", + "/v1/webhook_endpoints/{id}/deliveries/{delivery_id}": { + "get": { + "operationId": "getWebhookDelivery", "tags": [ "Webhook endpoints" ], - "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": "Retrieve a delivery", + "description": "One delivery of the endpoint, with every HTTP attempt and the payload as sent.", "security": [ { "bearerAuth": [] @@ -50224,23 +51666,26 @@ "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" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RollWebhookEndpointSecretRequest" - } - } - } - }, "responses": { "200": { - "description": "The endpoint and its new signing secret.", + "description": "The delivery.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -50264,13 +51709,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEndpointWithSecret" + "$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.", @@ -50333,70 +51778,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 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.", @@ -50488,14 +51870,14 @@ } } }, - "/v1/webhook_endpoints/{id}/recover": { + "/v1/webhook_endpoints/{id}/deliveries/{delivery_id}/retry": { "post": { - "operationId": "recoverWebhookEndpoint", + "operationId": "retryWebhookDelivery", "tags": [ "Webhook endpoints" ], - "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": "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": [] @@ -50515,23 +51897,26 @@ "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" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RecoverWebhookEndpointRequest" - } - } - } - }, "responses": { "200": { - "description": "How many deliveries were queued.", + "description": "The delivery, now `pending`.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -50555,13 +51940,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEndpointRecovery" + "$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.", @@ -50624,70 +52009,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 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.", @@ -50779,87 +52101,32 @@ } } }, - "/v1/webhook_endpoints/{id}/deliveries": { - "get": { - "operationId": "listWebhookDeliveries", + "/v1/webhook_listeners": { + "post": { + "operationId": "createWebhookListener", "tags": [ "Webhook endpoints" ], - "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": "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 webhook endpoint's id.", - "examples": [ - "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "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, - "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 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" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookListenerRequest" + } + } } - ], + }, "responses": { "200": { - "description": "A page of deliveries.", + "description": "The listener.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -50883,13 +52150,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookDeliveryList" + "$ref": "#/components/schemas/WebhookListener" } } } }, "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.", @@ -50951,8 +52218,29 @@ } } }, - "404": { - "description": "No webhook endpoint with this id exists in the API key's environment (`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.", + "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.", @@ -50973,7 +52261,7 @@ } }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "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.", @@ -51062,17 +52350,22 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/webhook_endpoints/{id}/deliveries/{delivery_id}": { - "get": { - "operationId": "getWebhookDelivery", + "/v1/webhook_listeners/{id}/poll": { + "post": { + "operationId": "pollWebhookListener", "tags": [ "Webhook endpoints" ], - "summary": "Retrieve a delivery", - "description": "One delivery of the endpoint, with every HTTP attempt and the payload as sent.", + "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": [] @@ -51082,36 +52375,33 @@ { "schema": { "type": "string", - "description": "The webhook endpoint's id.", + "description": "The webhook listener's id.", "examples": [ - "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" + "whlis_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The webhook endpoint's id.", + "description": "The webhook listener'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" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PollWebhookListenerRequest" + } + } + } + }, "responses": { "200": { - "description": "The delivery.", + "description": "Deliveries to forward (possibly none).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51135,13 +52425,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookDelivery" + "$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.", @@ -51204,7 +52494,70 @@ } }, "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`).", + "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.", @@ -51296,14 +52649,14 @@ } } }, - "/v1/webhook_endpoints/{id}/deliveries/{delivery_id}/retry": { - "post": { - "operationId": "retryWebhookDelivery", + "/v1/webhook_listeners/{id}": { + "delete": { + "operationId": "deleteWebhookListener", "tags": [ "Webhook endpoints" ], - "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": "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": [] @@ -51313,36 +52666,23 @@ { "schema": { "type": "string", - "description": "The webhook endpoint's id.", + "description": "The webhook listener's id.", "examples": [ - "whep_01k7c8m4q2f3e9v6t5r1n0p8yz" + "whlis_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The webhook endpoint's id.", + "description": "The webhook listener'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 delivery, now `pending`.", + "description": "The listener was closed.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -51366,7 +52706,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookDelivery" + "$ref": "#/components/schemas/DeletedWebhookListener" } } } @@ -51435,7 +52775,7 @@ } }, "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`).", + "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.", @@ -51527,14 +52867,14 @@ } } }, - "/v1/webhook_listeners": { + "/v1/webhook_events/test": { "post": { - "operationId": "createWebhookListener", + "operationId": "createTestWebhookEvent", "tags": [ "Webhook endpoints" ], - "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": "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": [] @@ -51545,14 +52885,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateWebhookListenerRequest" + "$ref": "#/components/schemas/CreateTestWebhookEventRequest" } } } }, "responses": { "200": { - "description": "The listener.", + "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.", @@ -51576,7 +52916,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookListener" + "$ref": "#/components/schemas/WebhookTestEvent" } } } @@ -51624,7 +52964,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.", @@ -51687,7 +53027,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`), 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.", @@ -51784,50 +53124,32 @@ ] } }, - "/v1/webhook_listeners/{id}/poll": { + "/v1/suppressions": { "post": { - "operationId": "pollWebhookListener", + "operationId": "createSuppression", "tags": [ - "Webhook endpoints" + "Suppressions" ], - "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": "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 webhook listener's id.", - "examples": [ - "whlis_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The webhook listener's id.", - "name": "id", - "in": "path" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PollWebhookListenerRequest" + "$ref": "#/components/schemas/CreateSuppressionRequest" } } } }, "responses": { "200": { - "description": "Deliveries to forward (possibly none).", + "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.", @@ -51851,7 +53173,37 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookListenerPoll" + "$ref": "#/components/schemas/Suppression" + } + } + } + }, + "201": { + "description": "The new 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/Suppression" } } } @@ -51920,7 +53272,7 @@ } }, "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`).", + "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.", @@ -51983,7 +53335,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`).", + "description": "The body failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52072,17 +53424,20 @@ } } } - } - } - }, - "/v1/webhook_listeners/{id}": { - "delete": { - "operationId": "deleteWebhookListener", + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, + "get": { + "operationId": "listSuppressions", "tags": [ - "Webhook endpoints" + "Suppressions" ], - "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": "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": [] @@ -52092,15 +53447,76 @@ { "schema": { "type": "string", - "description": "The webhook listener'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": [ - "whlis_01k7c8m4q2f3e9v6t5r1n0p8yz" + "user_8812" ] }, - "required": true, - "description": "The webhook listener'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" @@ -52108,7 +53524,7 @@ ], "responses": { "200": { - "description": "The listener was closed.", + "description": "A page of entries.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52132,7 +53548,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedWebhookListener" + "$ref": "#/components/schemas/SuppressionList" } } } @@ -52201,7 +53617,28 @@ } }, "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`).", + "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": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52293,32 +53730,77 @@ } } }, - "/v1/webhook_events/test": { - "post": { - "operationId": "createTestWebhookEvent", + "/v1/suppressions/check": { + "get": { + "operationId": "checkSuppression", "tags": [ - "Webhook endpoints" + "Suppressions" ], - "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": "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": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTestWebhookEventRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The address to check.", + "examples": [ + "user@example.com" + ] + }, + "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" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { "200": { - "description": "The event that was sent, and to how many receivers.", + "description": "The result.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52342,13 +53824,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookTestEvent" + "$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.", @@ -52390,28 +53872,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`).", + "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.", @@ -52431,8 +53892,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "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.", @@ -52453,7 +53914,7 @@ } }, "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).", + "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.", @@ -52542,22 +54003,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/suppressions": { + "/v1/suppressions/import": { "post": { - "operationId": "createSuppression", + "operationId": "importSuppressions", "tags": [ "Suppressions" ], - "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": "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": [] @@ -52568,44 +54024,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateSuppressionRequest" + "$ref": "#/components/schemas/ImportSuppressionsRequest" } } } }, "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": "What the import did.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52629,7 +54055,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Suppression" + "$ref": "#/components/schemas/SuppressionImport" } } } @@ -52761,7 +54187,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`, see `issues`).", + "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.", @@ -52856,14 +54282,16 @@ "$ref": "#/components/parameters/CossVersion" } ] - }, + } + }, + "/v1/suppressions/export": { "get": { - "operationId": "listSuppressions", + "operationId": "exportSuppressions", "tags": [ "Suppressions" ], - "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": "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": [] @@ -52875,26 +54303,16 @@ "type": "string", "minLength": 1, "maxLength": 256, - "description": "List a sub-account's own list (its id or your uid) instead of the environment's.", + "description": "Export a sub-account's own list (its id or your uid) instead of the environment's.", "examples": [ "user_8812" ] }, "required": false, - "description": "List a sub-account's own list (its id or your uid) instead of the environment's.", + "description": "Export 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": [ @@ -52922,26 +54340,24 @@ "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." + "enum": [ + "bounce", + "complaint", + "manual", + "import", + "unsubscribe" + ], + "description": "Only entries with this reason.", + "examples": [ + "bounce" + ] }, "required": false, - "description": "Cursor: the `id` of the last entry of the previous page.", - "name": "after", + "description": "Only entries with this reason.", + "name": "reason", "in": "query" }, { @@ -52950,7 +54366,7 @@ ], "responses": { "200": { - "description": "A page of entries.", + "description": "The CSV file.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -52972,9 +54388,9 @@ } }, "content": { - "application/json": { + "text/csv": { "schema": { - "$ref": "#/components/schemas/SuppressionList" + "type": "string" } } } @@ -53156,14 +54572,14 @@ } } }, - "/v1/suppressions/check": { + "/v1/suppressions/{id}": { "get": { - "operationId": "checkSuppression", + "operationId": "getSuppression", "tags": [ "Suppressions" ], - "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 suppression", + "description": "One entry of the suppression list of the API key's environment.", "security": [ { "bearerAuth": [] @@ -53173,52 +54589,15 @@ { "schema": { "type": "string", - "description": "The address to check.", + "description": "The suppression's id.", "examples": [ - "user@example.com" + "sup_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 suppression's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -53226,7 +54605,7 @@ ], "responses": { "200": { - "description": "The result.", + "description": "The entry.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53250,7 +54629,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuppressionCheck" + "$ref": "#/components/schemas/Suppression" } } } @@ -53319,28 +54698,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 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.", @@ -53430,34 +54788,40 @@ } } } - } - }, - "/v1/suppressions/import": { - "post": { - "operationId": "importSuppressions", + }, + "delete": { + "operationId": "deleteSuppression", "tags": [ "Suppressions" ], - "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": "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": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ImportSuppressionsRequest" - } - } + "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": "What the import did.", + "description": "The entry was removed.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53481,13 +54845,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuppressionImport" + "$ref": "#/components/schemas/DeletedSuppression" } } } }, "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.", @@ -53550,70 +54914,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 is not a list of 1-1000 `{ address, scope? }` objects (`validation_error`).", + "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.", @@ -53702,97 +55003,35 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/suppressions/export": { - "get": { - "operationId": "exportSuppressions", + "/v1/subaccounts": { + "post": { + "operationId": "createSubaccount", "tags": [ - "Suppressions" + "Sub-accounts" ], - "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": "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": [] } ], - "parameters": [ - { - "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.", - "examples": [ - "bounce" - ] - }, - "required": false, - "description": "Only entries with this reason.", - "name": "reason", - "in": "query" - }, - { - "$ref": "#/components/parameters/CossVersion" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSubaccountRequest" + } + } } - ], + }, "responses": { "200": { - "description": "The CSV file.", + "description": "The sub-account.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -53814,15 +55053,15 @@ } }, "content": { - "text/csv": { + "application/json": { "schema": { - "type": "string" + "$ref": "#/components/schemas/Subaccount" } } } }, "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.", @@ -53863,6 +55102,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": { @@ -53884,8 +55144,29 @@ } } }, - "404": { - "description": "`subaccount` names no sub-account of the API key's environment (`subaccount_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.", + "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.", @@ -53906,7 +55187,7 @@ } }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "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.", @@ -53995,35 +55276,50 @@ } } } - } - } - }, - "/v1/suppressions/{id}": { + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, "get": { - "operationId": "getSuppression", + "operationId": "listSubaccounts", "tags": [ - "Suppressions" + "Sub-accounts" ], - "summary": "Retrieve a suppression", - "description": "One entry of the suppression list of the API key's environment.", + "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 suppression's id.", + "description": "The last `id` of the previous page.", "examples": [ - "sup_01k7c8m4q2f3e9v6t5r1n0p8yz" + "sub_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, - "required": true, - "description": "The suppression's id.", - "name": "id", - "in": "path" + "required": false, + "description": "The last `id` of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -54031,7 +55327,7 @@ ], "responses": { "200": { - "description": "The entry.", + "description": "A page of sub-accounts.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54055,7 +55351,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Suppression" + "$ref": "#/components/schemas/SubaccountList" } } } @@ -54123,27 +55419,6 @@ } } }, - "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": { @@ -54214,14 +55489,16 @@ } } } - }, - "delete": { - "operationId": "deleteSuppression", + } + }, + "/v1/subaccounts/{subaccount}": { + "get": { + "operationId": "getSubaccount", "tags": [ - "Suppressions" + "Sub-accounts" ], - "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": "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": [] @@ -54231,14 +55508,14 @@ { "schema": { "type": "string", - "description": "The suppression's id.", + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", "examples": [ - "sup_01k7c8m4q2f3e9v6t5r1n0p8yz" + "user_8812" ] }, "required": true, - "description": "The suppression's id.", - "name": "id", + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", + "name": "subaccount", "in": "path" }, { @@ -54247,7 +55524,7 @@ ], "responses": { "200": { - "description": "The entry was removed.", + "description": "The sub-account.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54271,7 +55548,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSuppression" + "$ref": "#/components/schemas/Subaccount" } } } @@ -54340,7 +55617,7 @@ } }, "404": { - "description": "No suppression 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.", @@ -54430,27 +55707,43 @@ } } } - } - }, - "/v1/subaccounts": { - "post": { - "operationId": "createSubaccount", + }, + "patch": { + "operationId": "updateSubaccount", "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.", + "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": [] } ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", + "examples": [ + "user_8812" + ] + }, + "required": true, + "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/CreateSubaccountRequest" + "$ref": "#/components/schemas/UpdateSubaccountRequest" } } } @@ -54528,8 +55821,8 @@ } } }, - "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.", + "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.", @@ -54549,8 +55842,8 @@ } } }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "404": { + "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.", @@ -54613,7 +55906,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": "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.", @@ -54702,58 +55995,41 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } }, - "get": { - "operationId": "listSubaccounts", + "delete": { + "operationId": "deleteSubaccount", "tags": [ "Sub-accounts" ], - "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": "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": [] } ], "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 sub-account's `id` (`sub_…`) or your `uid` for it.", "examples": [ - "sub_01k7c8m4q2f3e9v6t5r1n0p8yz" + "user_8812" ] }, - "required": false, - "description": "The last `id` of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", + "name": "subaccount", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" } ], "responses": { - "200": { - "description": "A page of sub-accounts.", + "202": { + "description": "The sub-account, being deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -54777,7 +56053,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SubaccountList" + "$ref": "#/components/schemas/Subaccount" } } } @@ -54845,6 +56121,27 @@ } } }, + "404": { + "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.", + "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": { @@ -54917,14 +56214,14 @@ } } }, - "/v1/subaccounts/{subaccount}": { - "get": { - "operationId": "getSubaccount", + "/v1/subaccounts/{subaccount}/reinstate": { + "post": { + "operationId": "reinstateSubaccount", "tags": [ "Sub-accounts" ], - "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": "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": [] @@ -55133,14 +56430,16 @@ } } } - }, - "patch": { - "operationId": "updateSubaccount", + } + }, + "/v1/subaccounts/{subaccount}/stats": { + "get": { + "operationId": "getSubaccountStats", "tags": [ "Sub-accounts" ], - "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": "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": [] @@ -55160,23 +56459,41 @@ "name": "subaccount", "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" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateSubaccountRequest" - } - } - } - }, "responses": { "200": { - "description": "The sub-account.", + "description": "The sub-account's stats.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -55200,13 +56517,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Subaccount" + "$ref": "#/components/schemas/SubaccountStats" } } } }, "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.", @@ -55289,50 +56606,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 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": "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.", @@ -55422,14 +56697,16 @@ } } } - }, - "delete": { - "operationId": "deleteSubaccount", + } + }, + "/v1/sms": { + "post": { + "operationId": "sendSms", "tags": [ - "Sub-accounts" + "SMS" ], - "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": "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": [] @@ -55439,23 +56716,35 @@ { "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 message (with its current status) instead of sending again, 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 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": { "202": { - "description": "The sub-account, being deleted.", + "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.", @@ -55466,6 +56755,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": { @@ -55479,13 +56777,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Subaccount" + "$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.", @@ -55526,8 +56824,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.", @@ -55547,8 +56866,77 @@ } } }, - "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.", + "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.", @@ -55569,7 +56957,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.", @@ -55581,7 +56969,7 @@ } }, "Retry-After": { - "description": "Seconds to wait before retrying.", + "description": "Seconds to wait.", "schema": { "type": "integer" } @@ -55638,16 +57026,14 @@ } } } - } - }, - "/v1/subaccounts/{subaccount}/reinstate": { - "post": { - "operationId": "reinstateSubaccount", + }, + "get": { + "operationId": "listSms", "tags": [ - "Sub-accounts" + "SMS" ], - "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": "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": [] @@ -55657,15 +57043,68 @@ { "schema": { "type": "string", - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", - "examples": [ - "user_8812" - ] + "pattern": "^\\+[1-9]\\d{6,14}$", + "description": "Only messages to this number." }, - "required": true, - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", - "name": "subaccount", - "in": "path" + "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." + }, + "required": false, + "description": "Cursor: the `id` of the last item of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -55673,7 +57112,7 @@ ], "responses": { "200": { - "description": "The sub-account.", + "description": "A page of messages.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -55697,7 +57136,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Subaccount" + "$ref": "#/components/schemas/SmsList" } } } @@ -55765,8 +57204,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.", @@ -55858,68 +57297,32 @@ } } }, - "/v1/subaccounts/{subaccount}/stats": { - "get": { - "operationId": "getSubaccountStats", + "/v1/sms/suppressions": { + "post": { + "operationId": "createSmsSuppression", "tags": [ - "Sub-accounts" + "SMS" ], - "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": "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": [] } ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", - "examples": [ - "user_8812" - ] - }, - "required": true, - "description": "The sub-account's `id` (`sub_…`) or your `uid` for it.", - "name": "subaccount", - "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" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSmsSuppressionRequest" + } + } } - ], + }, "responses": { "200": { - "description": "The sub-account's stats.", + "description": "The number already had an entry.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -55943,13 +57346,43 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SubaccountStats" + "$ref": "#/components/schemas/SmsSuppression" + } + } + } + }, + "201": { + "description": "The new 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" } } } }, "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.", @@ -56011,8 +57444,29 @@ } } }, - "404": { - "description": "No sub-account with this id or uid exists in the API key's environment (`subaccount_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.", + "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.", @@ -56033,7 +57487,7 @@ } }, "422": { - "description": "A query parameter failed validation (`validation_error`), or `from` is after `to` (`invalid_range`).", + "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.", @@ -56122,17 +57576,20 @@ } } } - } - } - }, - "/v1/sms": { - "post": { - "operationId": "sendSms", + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, + "get": { + "operationId": "listSmsSuppressions", "tags": [ "SMS" ], - "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": "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": [] @@ -56142,35 +57599,44 @@ { "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.", - "examples": [ - "welcome-user-123" - ] + "pattern": "^\\+[1-9]\\d{6,14}$", + "description": "Only this number's entry." }, "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" + "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/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": "A page of entries.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56181,15 +57647,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": { @@ -56203,13 +57660,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Sms" + "$ref": "#/components/schemas/SmsSuppressionList" } } } }, "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.", @@ -56250,98 +57707,8 @@ } } }, - "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 (`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`).", - "headers": { - "X-Request-Id": { - "description": "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.", @@ -56362,7 +57729,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": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56383,7 +57750,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.", @@ -56395,7 +57762,7 @@ } }, "Retry-After": { - "description": "Seconds to wait.", + "description": "Seconds to wait before retrying.", "schema": { "type": "integer" } @@ -56452,14 +57819,16 @@ } } } - }, + } + }, + "/v1/sms/suppressions/check": { "get": { - "operationId": "listSms", + "operationId": "checkSmsSuppression", "tags": [ "SMS" ], - "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": "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": [] @@ -56469,67 +57838,10 @@ { "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." + "pattern": "^\\+[1-9]\\d{6,14}$" }, - "required": false, - "description": "Cursor: the `id` of the last item of the previous page.", - "name": "after", + "required": true, + "name": "phone_number", "in": "query" }, { @@ -56538,7 +57850,7 @@ ], "responses": { "200": { - "description": "A page of messages.", + "description": "The result.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56562,7 +57874,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SmsList" + "$ref": "#/components/schemas/SmsSuppressionCheck" } } } @@ -56723,62 +58035,40 @@ } } }, - "/v1/sms/suppressions": { - "post": { - "operationId": "createSmsSuppression", + "/v1/sms/suppressions/{id}": { + "get": { + "operationId": "getSmsSuppression", "tags": [ "SMS" ], - "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": "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/CreateSmsSuppressionRequest" - } - } + "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 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 entry.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -56808,7 +58098,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.", @@ -56870,50 +58160,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 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.", @@ -57002,20 +58250,15 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } }, - "get": { - "operationId": "listSmsSuppressions", + "delete": { + "operationId": "deleteSmsSuppression", "tags": [ "SMS" ], - "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": "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": [] @@ -57025,36 +58268,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 entry's id.", + "examples": [ + "smssup_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "required": false, - "description": "Cursor: the `id` of the last item of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The entry's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -57062,7 +58284,7 @@ ], "responses": { "200": { - "description": "A page of entries.", + "description": "The entry was removed.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57086,7 +58308,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SmsSuppressionList" + "$ref": "#/components/schemas/DeletedSmsSuppression" } } } @@ -57154,8 +58376,8 @@ } } }, - "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "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.", @@ -57247,14 +58469,14 @@ } } }, - "/v1/sms/suppressions/check": { + "/v1/sms/received": { "get": { - "operationId": "checkSmsSuppression", + "operationId": "listReceivedSms", "tags": [ "SMS" ], - "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": "List received SMS", + "description": "Messages sent to the organization's numbers whose replies go to the API key's environment, newest first.", "security": [ { "bearerAuth": [] @@ -57264,10 +58486,64 @@ { "schema": { "type": "string", - "pattern": "^\\+[1-9]\\d{6,14}$" + "pattern": "^\\+[1-9]\\d{6,14}$", + "description": "Only messages from this number." }, - "required": true, - "name": "phone_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" }, { @@ -57276,7 +58552,7 @@ ], "responses": { "200": { - "description": "The result.", + "description": "A page of received messages.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57300,7 +58576,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SmsSuppressionCheck" + "$ref": "#/components/schemas/ReceivedSmsList" } } } @@ -57461,14 +58737,14 @@ } } }, - "/v1/sms/suppressions/{id}": { + "/v1/sms/received/{id}": { "get": { - "operationId": "getSmsSuppression", + "operationId": "getReceivedSms", "tags": [ "SMS" ], - "summary": "Retrieve an SMS suppression", - "description": "One entry of the SMS suppression list of the API key's environment.", + "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": [] @@ -57478,13 +58754,13 @@ { "schema": { "type": "string", - "description": "The entry's id.", + "description": "The received message's id.", "examples": [ - "smssup_01k7c8m4q2f3e9v6t5r1n0p8yz" + "smsin_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The entry's id.", + "description": "The received message's id.", "name": "id", "in": "path" }, @@ -57494,7 +58770,7 @@ ], "responses": { "200": { - "description": "The entry.", + "description": "The received message.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57518,7 +58794,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SmsSuppression" + "$ref": "#/components/schemas/ReceivedSms" } } } @@ -57587,7 +58863,7 @@ } }, "404": { - "description": "No entry with this id exists in the API key's environment (`not_found`).", + "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.", @@ -57677,40 +58953,24 @@ } } } - }, - "delete": { - "operationId": "deleteSmsSuppression", + } + }, + "/v1/sms/numbers": { + "get": { + "operationId": "listSmsNumbers", "tags": [ "SMS" ], - "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": "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": [] } ], - "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 numbers.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -57734,7 +58994,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSmsSuppression" + "$ref": "#/components/schemas/SmsNumberList" } } } @@ -57802,27 +59062,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": { @@ -57892,17 +59131,22 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/sms/received": { + "/v1/sms/{id}": { "get": { - "operationId": "listReceivedSms", + "operationId": "getSms", "tags": [ "SMS" ], - "summary": "List received SMS", - "description": "Messages sent to the organization's numbers whose replies go to the API key's environment, newest first.", + "summary": "Retrieve an SMS", + "description": "One message sent from the API key's environment, with its current status.", "security": [ { "bearerAuth": [] @@ -57912,65 +59156,15 @@ { "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." + "description": "The message's id.", + "examples": [ + "sms_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "required": false, - "description": "Cursor: the `id` of the last item of the previous page.", - "name": "after", - "in": "query" + "required": true, + "description": "The message's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -57978,7 +59172,7 @@ ], "responses": { "200": { - "description": "A page of received messages.", + "description": "The message.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58002,7 +59196,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReceivedSmsList" + "$ref": "#/components/schemas/Sms" } } } @@ -58070,8 +59264,8 @@ } } }, - "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "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.", @@ -58163,14 +59357,14 @@ } } }, - "/v1/sms/received/{id}": { + "/v1/sms/{id}/events": { "get": { - "operationId": "getReceivedSms", + "operationId": "listSmsEvents", "tags": [ "SMS" ], - "summary": "Retrieve a received SMS", - "description": "One message received on a number of the organization and routed to the API key's environment.", + "summary": "List an SMS's events", + "description": "Everything that happened to the message, oldest first.", "security": [ { "bearerAuth": [] @@ -58180,13 +59374,13 @@ { "schema": { "type": "string", - "description": "The received message's id.", + "description": "The message's id.", "examples": [ - "smsin_01k7c8m4q2f3e9v6t5r1n0p8yz" + "sms_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The received message's id.", + "description": "The message's id.", "name": "id", "in": "path" }, @@ -58196,7 +59390,7 @@ ], "responses": { "200": { - "description": "The received message.", + "description": "The events.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58220,7 +59414,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReceivedSms" + "$ref": "#/components/schemas/SmsEventList" } } } @@ -58289,7 +59483,7 @@ } }, "404": { - "description": "No received message 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.", @@ -58381,22 +59575,40 @@ } } }, - "/v1/sms/numbers": { - "get": { - "operationId": "listSmsNumbers", + "/v1/sms/{id}/cancel": { + "post": { + "operationId": "cancelSms", "tags": [ "SMS" ], - "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": "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": [] } ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The message's id.", + "examples": [ + "sms_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The message's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "responses": { "200": { - "description": "The numbers.", + "description": "The canceled message.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58420,7 +59632,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SmsNumberList" + "$ref": "#/components/schemas/Sms" } } } @@ -58488,6 +59700,48 @@ } } }, + "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`).", + "headers": { + "X-Request-Id": { + "description": "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": { @@ -58557,48 +59811,35 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/sms/{id}": { - "get": { - "operationId": "getSms", + "/v1/sign/documents": { + "post": { + "operationId": "uploadSignDocument", "tags": [ - "SMS" + "Sign documents" ], - "summary": "Retrieve an SMS", - "description": "One message sent from the API key's environment, with its current status.", + "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 message's id.", - "examples": [ - "sms_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] - }, - "required": true, - "description": "The message'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 message.", + "201": { + "description": "The uploaded document.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58622,13 +59863,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Sms" + "$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.", @@ -58690,8 +59931,50 @@ } } }, - "404": { - "description": "No 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" + } + } + } + }, + "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 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.", @@ -58760,7 +60043,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.", @@ -58780,35 +60063,48 @@ } } } - } - } - }, - "/v1/sms/{id}/events": { + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, "get": { - "operationId": "listSmsEvents", + "operationId": "listSignDocuments", "tags": [ - "SMS" + "Sign documents" ], - "summary": "List an SMS's events", - "description": "Everything that happened to the message, oldest first.", + "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" + }, { "schema": { "type": "string", - "description": "The message's id.", - "examples": [ - "sms_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "maxLength": 64, + "description": "Cursor: the `id` of the last item of the previous page." }, - "required": true, - "description": "The message'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" @@ -58816,7 +60112,7 @@ ], "responses": { "200": { - "description": "The events.", + "description": "A page of documents.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -58840,7 +60136,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SmsEventList" + "$ref": "#/components/schemas/SignDocumentList" } } } @@ -58908,8 +60204,8 @@ } } }, - "404": { - "description": "No message 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.", @@ -59001,14 +60297,14 @@ } } }, - "/v1/sms/{id}/cancel": { - "post": { - "operationId": "cancelSms", + "/v1/sign/documents/{id}": { + "get": { + "operationId": "getSignDocument", "tags": [ - "SMS" + "Sign documents" ], - "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": "Retrieve a document", + "description": "An uploaded document's details: its hash, page sizes and text anchors. Its bytes are never returned here.", "security": [ { "bearerAuth": [] @@ -59018,13 +60314,13 @@ { "schema": { "type": "string", - "description": "The message's id.", + "description": "The document's id.", "examples": [ - "sms_01k7c8m4q2f3e9v6t5r1n0p8yz" + "sdoc_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The message's id.", + "description": "The document's id.", "name": "id", "in": "path" }, @@ -59034,7 +60330,7 @@ ], "responses": { "200": { - "description": "The canceled message.", + "description": "The document.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59058,7 +60354,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Sms" + "$ref": "#/components/schemas/SignDocument" } } } @@ -59127,28 +60423,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 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.", @@ -59238,34 +60513,40 @@ } } } - } - }, - "/v1/sign/documents": { - "post": { - "operationId": "uploadSignDocument", + }, + "delete": { + "operationId": "deleteSignDocument", "tags": [ "Sign documents" ], - "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": "Delete a document", + "description": "Deletes an uploaded document and its file. Documents a request uses can't be deleted.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UploadSignDocumentRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The document's id.", + "examples": [ + "sdoc_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + }, + "required": true, + "description": "The document's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { - "201": { - "description": "The uploaded document.", + "200": { + "description": "The document was deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59289,13 +60570,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignDocument" + "$ref": "#/components/schemas/DeletedSignDocument" } } } }, "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.", @@ -59357,29 +60638,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" - } - } - } - }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "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.", @@ -59400,7 +60660,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": "A signature request uses the document (`document_in_use`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59489,56 +60749,55 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, - "get": { - "operationId": "listSignDocuments", + } + } + }, + "/v1/sign/requests": { + "post": { + "operationId": "createSignRequest", "tags": [ - "Sign documents" + "Signature requests" ], - "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 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": [] } ], "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." + "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": "Cursor: the `id` of the last item of the previous page.", - "name": "after", - "in": "query" + "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/CreateSignRequestRequest" + } + } + } + }, "responses": { - "200": { - "description": "A page of documents.", + "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.", @@ -59549,6 +60808,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": { @@ -59562,13 +60830,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignDocumentList" + "$ref": "#/components/schemas/SignRequest" } } } }, "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.", @@ -59609,6 +60877,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": { @@ -59630,8 +60919,71 @@ } } }, + "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`).", + "headers": { + "X-Request-Id": { + "description": "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 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.", @@ -59700,7 +61052,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.", @@ -59721,16 +61073,14 @@ } } } - } - }, - "/v1/sign/documents/{id}": { + }, "get": { - "operationId": "getSignDocument", + "operationId": "listSignRequests", "tags": [ - "Sign documents" + "Signature requests" ], - "summary": "Retrieve a document", - "description": "An uploaded document's details: its hash, page sizes and text anchors. Its bytes are never returned here.", + "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": [] @@ -59740,15 +61090,35 @@ { "schema": { "type": "string", - "description": "The document's id.", - "examples": [ - "sdoc_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "description": "Only requests in these statuses, comma-separated: `sent,draft`." }, - "required": true, - "description": "The document's id.", - "name": "id", - "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" @@ -59756,7 +61126,7 @@ ], "responses": { "200": { - "description": "The document.", + "description": "A page of signature requests.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59780,7 +61150,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignDocument" + "$ref": "#/components/schemas/SignRequestList" } } } @@ -59848,8 +61218,8 @@ } } }, - "404": { - "description": "No document 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.", @@ -59939,14 +61309,16 @@ } } } - }, - "delete": { - "operationId": "deleteSignDocument", + } + }, + "/v1/sign/requests/{id}": { + "get": { + "operationId": "getSignRequest", "tags": [ - "Sign documents" + "Signature requests" ], - "summary": "Delete a document", - "description": "Deletes an uploaded document and its file. Documents a request uses can't be deleted.", + "summary": "Retrieve a signature request", + "description": "A signature request with its documents, signers and fields, and `signed_document` once it's sealed.", "security": [ { "bearerAuth": [] @@ -59956,13 +61328,13 @@ { "schema": { "type": "string", - "description": "The document's id.", + "description": "The signature request's id.", "examples": [ - "sdoc_01k7c8m4q2f3e9v6t5r1n0p8yz" + "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The document's id.", + "description": "The signature request's id.", "name": "id", "in": "path" }, @@ -59972,7 +61344,7 @@ ], "responses": { "200": { - "description": "The document was deleted.", + "description": "The signature request.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -59996,7 +61368,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSignDocument" + "$ref": "#/components/schemas/SignRequest" } } } @@ -60065,28 +61437,7 @@ } }, "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": "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.", @@ -60155,7 +61506,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.", @@ -60176,16 +61527,14 @@ } } } - } - }, - "/v1/sign/requests": { - "post": { - "operationId": "createSignRequest", + }, + "delete": { + "operationId": "deleteSignRequest", "tags": [ "Signature requests" ], - "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": "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": [] @@ -60195,35 +61544,23 @@ { "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.", + "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 request (in its current state) instead of creating another, 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/CreateSignRequestRequest" - } - } - } - }, "responses": { - "201": { - "description": "The signature request (`sent`, or `draft`). A replay returns the original request in its current state.", + "200": { + "description": "The deleted request.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60234,15 +61571,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": { @@ -60256,13 +61584,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignRequest" + "$ref": "#/components/schemas/DeletedSignRequest" } } } }, "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.", @@ -60303,27 +61631,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": { @@ -60345,50 +61652,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 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.", @@ -60409,7 +61674,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 request is `sent`: cancel it first (`request_not_deletable`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60499,14 +61764,16 @@ } } } - }, - "get": { - "operationId": "listSignRequests", + } + }, + "/v1/sign/requests/{id}/send": { + "post": { + "operationId": "sendSignRequest", "tags": [ "Signature requests" ], - "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": "Send a draft signature request", + "description": "Sends a `draft`. An already sent request is returned as is.", "security": [ { "bearerAuth": [] @@ -60516,35 +61783,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 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" @@ -60552,7 +61799,7 @@ ], "responses": { "200": { - "description": "A page of signature requests.", + "description": "The sent request.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60576,7 +61823,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignRequestList" + "$ref": "#/components/schemas/SignRequest" } } } @@ -60623,6 +61870,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": { @@ -60644,8 +61912,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 a draft (`request_not_draft`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60737,14 +62026,14 @@ } } }, - "/v1/sign/requests/{id}": { - "get": { - "operationId": "getSignRequest", + "/v1/sign/requests/{id}/cancel": { + "post": { + "operationId": "cancelSignRequest", "tags": [ "Signature requests" ], - "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 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": [] @@ -60770,7 +62059,7 @@ ], "responses": { "200": { - "description": "The signature request.", + "description": "The canceled request.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -60883,6 +62172,27 @@ } } }, + "422": { + "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.", + "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": { @@ -60953,14 +62263,16 @@ } } } - }, - "delete": { - "operationId": "deleteSignRequest", + } + }, + "/v1/sign/requests/{id}/audit_trail": { + "get": { + "operationId": "listSignRequestEvents", "tags": [ "Signature requests" ], - "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": "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": [] @@ -60986,7 +62298,7 @@ ], "responses": { "200": { - "description": "The deleted request.", + "description": "The audit trail.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61010,7 +62322,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSignRequest" + "$ref": "#/components/schemas/SignEventList" } } } @@ -61099,27 +62411,6 @@ } } }, - "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": { @@ -61169,7 +62460,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.", @@ -61192,14 +62483,14 @@ } } }, - "/v1/sign/requests/{id}/send": { + "/v1/sign/requests/{id}/remind": { "post": { - "operationId": "sendSignRequest", + "operationId": "remindSignRequest", "tags": [ "Signature requests" ], - "summary": "Send a draft signature request", - "description": "Sends a `draft`. An already sent request is returned as is.", + "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": [] @@ -61225,7 +62516,7 @@ ], "responses": { "200": { - "description": "The sent request.", + "description": "The request.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61296,27 +62587,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": { @@ -61360,7 +62630,7 @@ } }, "422": { - "description": "The request isn't a draft (`request_not_draft`).", + "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.", @@ -61452,14 +62722,14 @@ } } }, - "/v1/sign/requests/{id}/cancel": { - "post": { - "operationId": "cancelSignRequest", + "/v1/sign/requests/{id}/signed_document": { + "get": { + "operationId": "getSignedDocument", "tags": [ "Signature requests" ], - "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": "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": [] @@ -61485,7 +62755,7 @@ ], "responses": { "200": { - "description": "The canceled request.", + "description": "The sealed PDF.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61507,9 +62777,10 @@ } }, "content": { - "application/json": { + "application/pdf": { "schema": { - "$ref": "#/components/schemas/SignRequest" + "type": "string", + "format": "binary" } } } @@ -61599,7 +62870,7 @@ } }, "422": { - "description": "The request is already completed, declined or expired (`request_not_cancelable`).", + "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.", @@ -61668,7 +62939,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.", @@ -61691,14 +62962,14 @@ } } }, - "/v1/sign/requests/{id}/audit_trail": { - "get": { - "operationId": "listSignRequestEvents", + "/v1/sign/requests/{id}/signers/{signer_id}/sessions": { + "post": { + "operationId": "createSignSession", "tags": [ "Signature requests" ], - "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": "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": [] @@ -61718,13 +62989,36 @@ "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 audit trail.", + "201": { + "description": "The session, with its link.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61748,7 +63042,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignEventList" + "$ref": "#/components/schemas/SignSession" } } } @@ -61817,7 +63111,207 @@ } }, "404": { - "description": "No signature request 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.", + "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": "Sign is temporarily unavailable (`sign_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/sign/settings": { + "get": { + "operationId": "getSignSettings", + "tags": [ + "Sign settings" + ], + "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": [] + } + ], + "responses": { + "200": { + "description": "The settings.", + "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/SignSettings" + } + } + } + }, + "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.", @@ -61906,43 +63400,38 @@ } } } - } - } - }, - "/v1/sign/requests/{id}/remind": { - "post": { - "operationId": "remindSignRequest", + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, + "patch": { + "operationId": "updateSignSettings", "tags": [ - "Signature requests" + "Sign settings" ], - "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": "Update the Sign settings", + "description": "Changes the fields given; the others keep their values.", "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/UpdateSignSettingsRequest" + } + } } - ], + }, "responses": { "200": { - "description": "The request.", + "description": "The settings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -61966,7 +63455,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignRequest" + "$ref": "#/components/schemas/SignSettings" } } } @@ -62034,29 +63523,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 `sent`, or nobody can be reminded now (`reminder_not_due`).", + "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.", @@ -62145,43 +63613,40 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/sign/requests/{id}/signed_document": { - "get": { - "operationId": "getSignedDocument", + "/v1/sign/templates": { + "post": { + "operationId": "createSignTemplate", "tags": [ - "Signature requests" + "Sign templates" ], - "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": "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 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/CreateSignTemplateRequest" + } + } } - ], + }, "responses": { - "200": { - "description": "The sealed PDF.", + "201": { + "description": "The template.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62203,16 +63668,15 @@ } }, "content": { - "application/pdf": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$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.", @@ -62274,8 +63738,8 @@ } } }, - "404": { - "description": "No signature request 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.", @@ -62296,7 +63760,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`), 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.", @@ -62385,17 +63849,20 @@ } } } - } - } - }, - "/v1/sign/requests/{id}/signers/{signer_id}/sessions": { - "post": { - "operationId": "createSignSession", + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, + "get": { + "operationId": "listSignTemplates", "tags": [ - "Signature requests" + "Sign templates" ], - "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": "List templates", + "description": "Newest first. Pass the last `id` as `after` while `has_more` is true.", "security": [ { "bearerAuth": [] @@ -62404,47 +63871,34 @@ "parameters": [ { "schema": { - "type": "string", - "description": "The signature request's id.", - "examples": [ - "sreq_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "1-100, default 20." }, - "required": true, - "description": "The signature request's id.", - "name": "id", - "in": "path" + "required": false, + "description": "1-100, default 20.", + "name": "limit", + "in": "query" }, { "schema": { "type": "string", - "description": "The signer's id (`signers[].id` of the request).", - "examples": [ - "ssgn_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "maxLength": 64, + "description": "Cursor: the `id` of the last item of the previous page." }, - "required": true, - "description": "The signer's id (`signers[].id` of the request).", - "name": "signer_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" } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSignSessionRequest" - } - } - } - }, "responses": { - "201": { - "description": "The session, with its link.", + "200": { + "description": "A page of templates.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62468,7 +63922,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignSession" + "$ref": "#/components/schemas/SignTemplateList" } } } @@ -62536,29 +63990,8 @@ } } }, - "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": "A query parameter failed validation (`validation_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62627,7 +64060,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.", @@ -62650,22 +64083,40 @@ } } }, - "/v1/sign/settings": { + "/v1/sign/templates/{id}": { "get": { - "operationId": "getSignSettings", + "operationId": "getSignTemplate", "tags": [ - "Sign settings" + "Sign templates" ], - "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": "Retrieve a template", + "description": "A template with its documents, roles and fields. `ready` says whether requests can be made from it.", "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" + } + ], "responses": { "200": { - "description": "The settings.", + "description": "The template.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62689,7 +64140,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignSettings" + "$ref": "#/components/schemas/SignTemplate" } } } @@ -62757,6 +64208,27 @@ } } }, + "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" + } + } + } + }, "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": { @@ -62826,38 +64298,51 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } }, "patch": { - "operationId": "updateSignSettings", + "operationId": "updateSignTemplate", "tags": [ - "Sign settings" + "Sign templates" ], - "summary": "Update the Sign settings", - "description": "Changes the fields given; the others keep their values.", + "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": [] } ], + "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/UpdateSignSettingsRequest" + "$ref": "#/components/schemas/UpdateSignTemplateRequest" } } } }, "responses": { "200": { - "description": "The settings.", + "description": "The template.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -62881,13 +64366,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignSettings" + "$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.", @@ -62949,8 +64434,50 @@ } } }, + "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" + } + } + } + }, + "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`), or an origin isn't `https://host[:port]` (`invalid_origin`).", + "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.", @@ -63019,7 +64546,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.", @@ -63039,40 +64566,41 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - } - }, - "/v1/sign/templates": { - "post": { - "operationId": "createSignTemplate", + } + }, + "delete": { + "operationId": "deleteSignTemplate", "tags": [ "Sign templates" ], - "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 template", + "description": "Requests made from it, and its documents, stay.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSignTemplateRequest" - } - } + "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" } - }, + ], "responses": { - "201": { - "description": "The template.", + "200": { + "description": "The deleted template.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63096,13 +64624,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignTemplate" + "$ref": "#/components/schemas/DeletedSignTemplate" } } } }, "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.", @@ -63164,29 +64692,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 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.", @@ -63255,7 +64762,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.", @@ -63275,20 +64782,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, - "get": { - "operationId": "listSignTemplates", + } + } + }, + "/v1/sign/templates/{id}/requests": { + "post": { + "operationId": "createSignRequestFromTemplate", "tags": [ "Sign templates" ], - "summary": "List templates", - "description": "Newest first. Pass the last `id` as `after` while `has_more` is true.", + "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": [] @@ -63297,34 +64801,49 @@ "parameters": [ { "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "1-100, default 20." + "type": "string", + "description": "The template's id.", + "examples": [ + "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] }, - "required": false, - "description": "1-100, default 20.", - "name": "limit", - "in": "query" + "required": true, + "description": "The template's id.", + "name": "id", + "in": "path" }, { "schema": { "type": "string", - "maxLength": 64, - "description": "Cursor: the `id` of the last item of the previous page." + "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": "Cursor: the `id` of the last item of the previous page.", - "name": "after", - "in": "query" + "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": { - "200": { - "description": "A page of templates.", + "201": { + "description": "The signature request.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63335,6 +64854,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": { @@ -63348,13 +64876,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignTemplateList" + "$ref": "#/components/schemas/SignRequest" } } } }, "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.", @@ -63416,8 +64944,50 @@ } } }, + "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": "A query parameter failed validation (`validation_error`).", + "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.", @@ -63486,7 +65056,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.", @@ -63509,14 +65079,14 @@ } } }, - "/v1/sign/templates/{id}": { - "get": { - "operationId": "getSignTemplate", + "/v1/sign/templates/{id}/builder_sessions": { + "post": { + "operationId": "createSignBuilderSession", "tags": [ "Sign templates" ], - "summary": "Retrieve a template", - "description": "A template with its documents, roles and fields. `ready` says whether requests can be made from it.", + "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": [] @@ -63540,9 +65110,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignBuilderSessionRequest" + } + } + } + }, "responses": { - "200": { - "description": "The template.", + "201": { + "description": "The builder session, with its link.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -63566,7 +65146,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignTemplate" + "$ref": "#/components/schemas/SignBuilderSession" } } } @@ -63704,7 +65284,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.", @@ -63725,14 +65305,16 @@ } } } - }, - "patch": { - "operationId": "updateSignTemplate", + } + }, + "/v1/verifications": { + "post": { + "operationId": "startVerification", "tags": [ - "Sign templates" + "Verifications" ], - "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": "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": [] @@ -63742,15 +65324,17 @@ { "schema": { "type": "string", - "description": "The template'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": [ - "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + "welcome-user-123" ] }, - "required": true, - "description": "The template'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" @@ -63761,14 +65345,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateSignTemplateRequest" + "$ref": "#/components/schemas/StartVerificationRequest" } } } }, "responses": { - "200": { - "description": "The template.", + "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.", @@ -63779,6 +65363,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": { @@ -63792,13 +65385,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignTemplate" + "$ref": "#/components/schemas/Verification" } } } }, "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.", @@ -63839,8 +65432,8 @@ } } }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "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.", @@ -63860,8 +65453,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.", @@ -63881,8 +65474,8 @@ } } }, - "413": { - "description": "The request body is larger than 40 MB (`payload_too_large`).", + "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.", @@ -63892,6 +65485,12 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } } }, "content": { @@ -63903,7 +65502,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 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.", @@ -63924,7 +65523,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.", @@ -63972,7 +65571,7 @@ } }, "503": { - "description": "Sign is temporarily unavailable (`sign_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.", @@ -63994,13 +65593,13 @@ } } }, - "delete": { - "operationId": "deleteSignTemplate", + "get": { + "operationId": "listVerifications", "tags": [ - "Sign templates" + "Verifications" ], - "summary": "Delete a template", - "description": "Requests made from it, and its documents, stay.", + "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": [] @@ -64010,15 +65609,55 @@ { "schema": { "type": "string", - "description": "The template's id.", - "examples": [ - "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" - ] + "minLength": 1, + "maxLength": 320, + "description": "Only verifications to this destination." }, - "required": true, - "description": "The template'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" @@ -64026,7 +65665,7 @@ ], "responses": { "200": { - "description": "The deleted template.", + "description": "A page of verifications.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64050,7 +65689,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSignTemplate" + "$ref": "#/components/schemas/VerificationList" } } } @@ -64118,8 +65757,8 @@ } } }, - "404": { - "description": "No template 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.", @@ -64211,45 +65850,32 @@ } } }, - "/v1/sign/templates/{id}/requests": { + "/v1/verifications/check": { "post": { - "operationId": "createSignRequestFromTemplate", + "operationId": "checkVerification", "tags": [ - "Sign templates" + "Verifications" ], - "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": "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": [] } ], "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.", + "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" }, @@ -64262,14 +65888,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateSignRequestFromTemplateRequest" + "$ref": "#/components/schemas/CheckVerificationRequest" } } } }, "responses": { - "201": { - "description": "The signature request.", + "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.", @@ -64280,15 +65906,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": { @@ -64302,7 +65919,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignRequest" + "$ref": "#/components/schemas/VerificationCheck" } } } @@ -64371,28 +65988,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`).", + "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.", @@ -64413,7 +66009,7 @@ } }, "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": "The body failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64434,7 +66030,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.", @@ -64482,7 +66078,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.", @@ -64505,14 +66101,14 @@ } } }, - "/v1/sign/templates/{id}/builder_sessions": { - "post": { - "operationId": "createSignBuilderSession", + "/v1/verifications/{id}": { + "get": { + "operationId": "getVerification", "tags": [ - "Sign templates" + "Verifications" ], - "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": "Retrieve a verification", + "description": "A verification's status and destination. Its code is never returned.", "security": [ { "bearerAuth": [] @@ -64522,13 +66118,13 @@ { "schema": { "type": "string", - "description": "The template's id.", + "description": "The verification's id.", "examples": [ - "stpl_01k7c8m4q2f3e9v6t5r1n0p8yz" + "vrf_01k8e2m4q2f3e9v6t5r1n0p8yz" ] }, "required": true, - "description": "The template's id.", + "description": "The verification's id.", "name": "id", "in": "path" }, @@ -64536,19 +66132,9 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSignBuilderSessionRequest" - } - } - } - }, "responses": { - "201": { - "description": "The builder session, with its link.", + "200": { + "description": "The verification.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64572,7 +66158,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignBuilderSession" + "$ref": "#/components/schemas/Verification" } } } @@ -64641,7 +66227,7 @@ } }, "404": { - "description": "No template 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.", @@ -64710,7 +66296,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.", @@ -64733,14 +66319,14 @@ } } }, - "/v1/verifications": { + "/v1/verifications/{id}/cancel": { "post": { - "operationId": "startVerification", + "operationId": "cancelVerification", "tags": [ "Verifications" ], - "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": "Cancel a verification", + "description": "Cancels a pending verification: its code stops working. Canceling it again returns it as is.", "security": [ { "bearerAuth": [] @@ -64750,35 +66336,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 verification's id.", "examples": [ - "welcome-user-123" + "vrf_01k8e2m4q2f3e9v6t5r1n0p8yz" ] }, - "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 verification's id.", + "name": "id", + "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": "The canceled verification.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -64789,15 +66363,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": { @@ -64817,7 +66382,7 @@ } }, "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.", @@ -64858,8 +66423,8 @@ } } }, - "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.", + "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.", @@ -64879,8 +66444,8 @@ } } }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "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.", @@ -64900,8 +66465,29 @@ } } }, - "409": { - "description": "Requests with this `Idempotency-Key` are racing (`request_in_progress`).", + "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.", + "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.", @@ -64927,8 +66513,139 @@ } } }, - "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`).", + "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/verify/settings": { + "get": { + "operationId": "getVerifySettings", + "tags": [ + "Verify settings" + ], + "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": [] + } + ], + "responses": { + "200": { + "description": "The settings.", + "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/VerifySettings" + } + } + } + }, + "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.", @@ -64949,7 +66666,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.", @@ -64997,7 +66714,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.", @@ -65017,81 +66734,38 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] }, - "get": { - "operationId": "listVerifications", + "patch": { + "operationId": "updateVerifySettings", "tags": [ - "Verifications" + "Verify settings" ], - "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": "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", - "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." - }, - "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/UpdateVerifySettingsRequest" + } + } } - ], + }, "responses": { "200": { - "description": "A page of verifications.", + "description": "The settings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65115,7 +66789,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerificationList" + "$ref": "#/components/schemas/VerifySettings" } } } @@ -65184,7 +66858,7 @@ } }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "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.", @@ -65273,55 +66947,40 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/verifications/check": { + "/v1/forms": { "post": { - "operationId": "checkVerification", + "operationId": "createForm", "tags": [ - "Verifications" + "Forms" ], - "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": "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", - "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.", - "examples": [ - "welcome-user-123" - ] - }, - "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" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CheckVerificationRequest" + "$ref": "#/components/schemas/CreateFormRequest" } } } }, "responses": { - "200": { - "description": "Whether the code is right, and the verification after the check.", + "201": { + "description": "The form.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65345,13 +67004,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerificationCheck" + "$ref": "#/components/schemas/Form" } } } }, "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 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.", @@ -65413,8 +67072,8 @@ } } }, - "404": { - "description": "No such verification 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.", @@ -65435,7 +67094,7 @@ } }, "422": { - "description": "The body failed validation (`validation_error`, see `issues`).", + "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.", @@ -65456,7 +67115,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.", @@ -65504,7 +67163,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.", @@ -65524,35 +67183,48 @@ } } } - } - } - }, - "/v1/verifications/{id}": { + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, "get": { - "operationId": "getVerification", + "operationId": "listForms", "tags": [ - "Verifications" + "Forms" ], - "summary": "Retrieve a verification", - "description": "A verification's status and destination. Its code is never returned.", + "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 verification's id.", - "examples": [ - "vrf_01k8e2m4q2f3e9v6t5r1n0p8yz" - ] + "maxLength": 64, + "description": "Cursor: the `id` of the last item of the previous page." }, - "required": true, - "description": "The verification'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" @@ -65560,7 +67232,7 @@ ], "responses": { "200": { - "description": "The verification.", + "description": "A page of forms.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65584,7 +67256,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Verification" + "$ref": "#/components/schemas/FormList" } } } @@ -65652,8 +67324,8 @@ } } }, - "404": { - "description": "No such verification 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.", @@ -65722,7 +67394,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.", @@ -65745,14 +67417,14 @@ } } }, - "/v1/verifications/{id}/cancel": { - "post": { - "operationId": "cancelVerification", + "/v1/forms/{id}": { + "get": { + "operationId": "getForm", "tags": [ - "Verifications" + "Forms" ], - "summary": "Cancel a verification", - "description": "Cancels a pending verification: its code stops working. Canceling it again returns it as is.", + "summary": "Get a form", + "description": "A form's settings, with the `endpoint` it posts to and its `hosted_url`.", "security": [ { "bearerAuth": [] @@ -65762,13 +67434,13 @@ { "schema": { "type": "string", - "description": "The verification's id.", + "description": "The form's id.", "examples": [ - "vrf_01k8e2m4q2f3e9v6t5r1n0p8yz" + "form_01kd2m8n3p4q5r6s7t8v9w0x1y" ] }, "required": true, - "description": "The verification's id.", + "description": "The form's id.", "name": "id", "in": "path" }, @@ -65778,7 +67450,7 @@ ], "responses": { "200": { - "description": "The canceled verification.", + "description": "The form.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -65802,7 +67474,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Verification" + "$ref": "#/components/schemas/Form" } } } @@ -65871,28 +67543,7 @@ } }, "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.", - "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.", + "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.", @@ -65961,7 +67612,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.", @@ -65982,24 +67633,50 @@ } } } - } - }, - "/v1/verify/settings": { - "get": { - "operationId": "getVerifySettings", + }, + "patch": { + "operationId": "updateForm", "tags": [ - "Verify settings" + "Forms" ], - "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": "Update a form", + "description": "Only the fields given change, within each group (`spam`, `contacts`, …) too.", "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" + } + } + } + }, "responses": { "200": { - "description": "The settings.", + "description": "The form.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66023,13 +67700,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerifySettings" + "$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.", @@ -66091,6 +67768,96 @@ } } }, + "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" + } + } + } + }, + "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.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "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`).", + "headers": { + "X-Request-Id": { + "description": "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": { @@ -66140,7 +67907,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.", @@ -66160,38 +67927,41 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } }, - "patch": { - "operationId": "updateVerifySettings", + "delete": { + "operationId": "deleteForm", "tags": [ - "Verify settings" + "Forms" ], - "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": "Delete a form", + "description": "Deletes the form with every submission and uploaded file. Its endpoint answers 404 from then on.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateVerifySettingsRequest" - } - } + "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" } - }, + ], "responses": { "200": { - "description": "The settings.", + "description": "The form was deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66215,7 +67985,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerifySettings" + "$ref": "#/components/schemas/DeletedForm" } } } @@ -66283,8 +68053,8 @@ } } }, - "422": { - "description": "The body failed validation (`validation_error`), or a sender isn't the organization's (`invalid_settings`).", + "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.", @@ -66353,7 +68123,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.", @@ -66373,40 +68143,76 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/forms": { - "post": { - "operationId": "createForm", + "/v1/forms/{id}/submissions": { + "get": { + "operationId": "listFormSubmissions", "tags": [ - "Forms" + "Form submissions" ], - "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 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": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateFormRequest" - } - } + "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": { - "201": { - "description": "The form.", + "200": { + "description": "A page of submissions.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66430,13 +68236,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Form" + "$ref": "#/components/schemas/FormSubmissionList" } } } }, "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.", @@ -66498,8 +68304,8 @@ } } }, - "415": { - "description": "The body is not sent as `application/json` (`unsupported_media_type`).", + "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.", @@ -66520,7 +68326,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": "A query parameter failed validation (`validation_error`, see `issues`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66589,7 +68395,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.", @@ -66609,20 +68415,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, + } + } + }, + "/v1/forms/{id}/submissions/{submission_id}": { "get": { - "operationId": "listForms", + "operationId": "getFormSubmission", "tags": [ - "Forms" + "Form submissions" ], - "summary": "List forms", - "description": "The environment's forms, newest first.", + "summary": "Get a submission", + "description": "One submission of the form, with its data, files and Risk's assessment.", "security": [ { "bearerAuth": [] @@ -66631,26 +68434,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" @@ -66658,7 +68464,7 @@ ], "responses": { "200": { - "description": "A page of forms.", + "description": "The submission.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66682,7 +68488,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FormList" + "$ref": "#/components/schemas/FormSubmission" } } } @@ -66750,8 +68556,8 @@ } } }, - "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "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.", @@ -66820,7 +68626,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.", @@ -66841,16 +68647,14 @@ } } } - } - }, - "/v1/forms/{id}": { - "get": { - "operationId": "getForm", + }, + "delete": { + "operationId": "deleteFormSubmission", "tags": [ - "Forms" + "Form submissions" ], - "summary": "Get a form", - "description": "A form's settings, with the `endpoint` it posts to and its `hosted_url`.", + "summary": "Delete a submission", + "description": "Deletes the submission and its files.", "security": [ { "bearerAuth": [] @@ -66870,13 +68674,26 @@ "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 form.", + "description": "The submission was deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -66900,7 +68717,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Form" + "$ref": "#/components/schemas/DeletedFormSubmission" } } } @@ -66969,7 +68786,7 @@ } }, "404": { - "description": "No form with this id exists in the API key's environment (`not_found`).", + "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.", @@ -67038,7 +68855,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.", @@ -67059,14 +68876,16 @@ } } } - }, - "patch": { - "operationId": "updateForm", + } + }, + "/v1/forms/{id}/submissions/{submission_id}/release": { + "post": { + "operationId": "releaseFormSubmission", "tags": [ - "Forms" + "Form submissions" ], - "summary": "Update a form", - "description": "Only the fields given change, within each group (`spam`, `contacts`, …) too.", + "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": [] @@ -67086,23 +68905,26 @@ "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/UpdateFormRequest" - } - } - } - }, "responses": { "200": { - "description": "The form.", + "description": "The released submission.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -67126,13 +68948,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Form" + "$ref": "#/components/schemas/FormSubmission" } } } }, "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.", @@ -67195,55 +69017,7 @@ } }, "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" - } - } - } - }, - "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`).", + "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.", @@ -67264,7 +69038,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": "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.", @@ -67333,7 +69107,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.", @@ -67354,14 +69128,16 @@ } } } - }, - "delete": { - "operationId": "deleteForm", + } + }, + "/v1/forms/{id}/submissions/{submission_id}/files/{file_id}": { + "get": { + "operationId": "getFormSubmissionFile", "tags": [ - "Forms" + "Form submissions" ], - "summary": "Delete a form", - "description": "Deletes the form with every submission and uploaded file. Its endpoint answers 404 from then on.", + "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": [] @@ -67381,13 +69157,39 @@ "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 form was deleted.", + "description": "The file's bytes.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -67409,9 +69211,10 @@ } }, "content": { - "application/json": { + "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/DeletedForm" + "type": "string", + "format": "binary" } } } @@ -67480,7 +69283,7 @@ } }, "404": { - "description": "No form 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.", @@ -67572,73 +69375,32 @@ } } }, - "/v1/forms/{id}/submissions": { - "get": { - "operationId": "listFormSubmissions", + "/v1/scheduling/hosts": { + "post": { + "operationId": "createSchedulingHost", "tags": [ - "Form submissions" + "Scheduling hosts" ], - "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": "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 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" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateHostRequest" + } + } } - ], + }, "responses": { - "200": { - "description": "A page of submissions.", + "201": { + "description": "The host.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -67662,13 +69424,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FormSubmissionList" + "$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.", @@ -67709,6 +69471,27 @@ } } }, + "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": { @@ -67730,8 +69513,8 @@ } } }, - "404": { - "description": "No form 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.", @@ -67752,7 +69535,7 @@ } }, "422": { - "description": "A query parameter failed validation (`validation_error`, see `issues`).", + "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.", @@ -67841,17 +69624,20 @@ } } } - } - } - }, - "/v1/forms/{id}/submissions/{submission_id}": { + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, "get": { - "operationId": "getFormSubmission", + "operationId": "listSchedulingHosts", "tags": [ - "Form submissions" + "Scheduling hosts" ], - "summary": "Get a submission", - "description": "One submission of the form, with its data, files and Risk's assessment.", + "summary": "List hosts", + "description": "The environment's hosts, newest first.", "security": [ { "bearerAuth": [] @@ -67860,29 +69646,24 @@ "parameters": [ { "schema": { - "type": "string", - "description": "The form's id.", - "examples": [ - "form_01kd2m8n3p4q5r6s7t8v9w0x1y" - ] + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 }, - "required": true, - "description": "The form's id.", - "name": "id", - "in": "path" + "required": false, + "name": "limit", + "in": "query" }, { "schema": { "type": "string", - "description": "The submission's id.", - "examples": [ - "fsub_01kd2m9a0b1c2d3e4f5g6h7j8k" - ] + "description": "The last id of the previous page." }, - "required": true, - "description": "The submission's id.", - "name": "submission_id", - "in": "path" + "required": false, + "description": "The last id of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -67890,7 +69671,7 @@ ], "responses": { "200": { - "description": "The submission.", + "description": "A page of hosts.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -67914,7 +69695,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FormSubmission" + "$ref": "#/components/schemas/SchedulingHostList" } } } @@ -67982,8 +69763,8 @@ } } }, - "404": { - "description": "No such form, or no submission with this id in it (`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.", @@ -68073,14 +69854,16 @@ } } } - }, - "delete": { - "operationId": "deleteFormSubmission", + } + }, + "/v1/scheduling/hosts/{host}": { + "get": { + "operationId": "getSchedulingHost", "tags": [ - "Form submissions" + "Scheduling hosts" ], - "summary": "Delete a submission", - "description": "Deletes the submission and its files.", + "summary": "Retrieve a host", + "description": "A host by its id or your uid.", "security": [ { "bearerAuth": [] @@ -68090,27 +69873,14 @@ { "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.", + "description": "The host's id or uid.", "examples": [ - "fsub_01kd2m9a0b1c2d3e4f5g6h7j8k" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The submission's id.", - "name": "submission_id", + "description": "The host's id or uid.", + "name": "host", "in": "path" }, { @@ -68119,7 +69889,7 @@ ], "responses": { "200": { - "description": "The submission was deleted.", + "description": "The host.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -68143,7 +69913,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedFormSubmission" + "$ref": "#/components/schemas/SchedulingHost" } } } @@ -68212,7 +69982,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.", @@ -68302,16 +70072,14 @@ } } } - } - }, - "/v1/forms/{id}/submissions/{submission_id}/release": { - "post": { - "operationId": "releaseFormSubmission", + }, + "patch": { + "operationId": "updateSchedulingHost", "tags": [ - "Form submissions" + "Scheduling hosts" ], - "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": "Update a host", + "description": "Changes what's given. A `disabled` host keeps its bookings but gets no new ones.", "security": [ { "bearerAuth": [] @@ -68321,36 +70089,33 @@ { "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.", + "description": "The host's id or uid.", "examples": [ - "fsub_01kd2m9a0b1c2d3e4f5g6h7j8k" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The submission's id.", - "name": "submission_id", + "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/UpdateHostRequest" + } + } + } + }, "responses": { "200": { - "description": "The released submission.", + "description": "The host.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -68374,13 +70139,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FormSubmission" + "$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.", @@ -68443,7 +70208,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.", @@ -68464,7 +70250,7 @@ } }, "422": { - "description": "Only held and spam submissions can be released (`submission_not_releasable`).", + "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.", @@ -68554,16 +70340,14 @@ } } } - } - }, - "/v1/forms/{id}/submissions/{submission_id}/files/{file_id}": { - "get": { - "operationId": "getFormSubmissionFile", + }, + "delete": { + "operationId": "deleteSchedulingHost", "tags": [ - "Form submissions" + "Scheduling hosts" ], - "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": "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": [] @@ -68573,40 +70357,14 @@ { "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" - }, - { - "schema": { - "type": "string", - "description": "The file's id (`files[].id` of the submission).", + "description": "The host's id or uid.", "examples": [ - "ffile_01kd2m9a0b1c2d3e4f5g6h7j8m" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The file's id (`files[].id` of the submission).", - "name": "file_id", + "description": "The host's id or uid.", + "name": "host", "in": "path" }, { @@ -68615,7 +70373,7 @@ ], "responses": { "200": { - "description": "The file's bytes.", + "description": "Deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -68637,10 +70395,9 @@ } }, "content": { - "application/octet-stream": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/DeletedSchedulingHost" } } } @@ -68709,7 +70466,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.", @@ -68778,7 +70556,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.", @@ -68801,32 +70579,50 @@ } } }, - "/v1/scheduling/hosts": { + "/v1/scheduling/bookings/{id}/reassign": { "post": { - "operationId": "createSchedulingHost", + "operationId": "reassignSchedulingBooking", "tags": [ - "Scheduling hosts" + "Scheduling bookings" ], - "summary": "Create a host", - "description": "A person who can be booked: one of your users, such as a doctor or a tutor.", + "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": [] } ], + "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" + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateHostRequest" + "$ref": "#/components/schemas/ReassignSchedulingBookingRequest" } } } }, "responses": { - "201": { - "description": "The host.", + "200": { + "description": "The booking.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -68850,7 +70646,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingHost" + "$ref": "#/components/schemas/SchedulingBooking" } } } @@ -68897,8 +70693,8 @@ } } }, - "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`).", + "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.", @@ -68918,8 +70714,8 @@ } } }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "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.", @@ -68961,7 +70757,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 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.", @@ -69050,20 +70846,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, + } + } + }, + "/v1/scheduling/bookings/{id}/reassign_candidates": { "get": { - "operationId": "listSchedulingHosts", + "operationId": "listSchedulingReassignCandidates", "tags": [ - "Scheduling hosts" + "Scheduling bookings" ], - "summary": "List hosts", - "description": "The environment's hosts, newest first.", + "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": [] @@ -69072,23 +70865,25 @@ "parameters": [ { "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 20 + "type": "string", + "description": "The booking's id.", + "examples": [ + "book_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] }, - "required": false, - "name": "limit", - "in": "query" + "required": true, + "description": "The booking's id.", + "name": "id", + "in": "path" }, { "schema": { "type": "string", - "description": "The last id of the previous page." + "description": "The host who'd leave, when it could be several." }, "required": false, - "description": "The last id of the previous page.", - "name": "after", + "description": "The host who'd leave, when it could be several.", + "name": "from_host", "in": "query" }, { @@ -69097,7 +70892,7 @@ ], "responses": { "200": { - "description": "A page of hosts.", + "description": "The candidates.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -69121,7 +70916,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingHostList" + "$ref": "#/components/schemas/SchedulingReassignCandidateList" } } } @@ -69189,6 +70984,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" + } + } + } + }, "422": { "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", "headers": { @@ -69282,40 +71098,32 @@ } } }, - "/v1/scheduling/hosts/{host}": { - "get": { - "operationId": "getSchedulingHost", + "/v1/scheduling/reassignments/preview": { + "post": { + "operationId": "previewSchedulingReassignment", "tags": [ - "Scheduling hosts" + "Scheduling reassignments" ], - "summary": "Retrieve a host", - "description": "A host by its id or your uid.", + "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 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/PreviewSchedulingReassignmentRequest" + } + } } - ], + }, "responses": { "200": { - "description": "The host.", + "description": "What would happen.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -69339,13 +71147,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingHost" + "$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.", @@ -69428,6 +71236,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 selection or strategy doesn't make sense (`invalid_reassignment`).", + "headers": { + "X-Request-Id": { + "description": "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": { @@ -69497,15 +71347,22 @@ } } } - } - }, - "patch": { - "operationId": "updateSchedulingHost", + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + } + }, + "/v1/scheduling/reassignments": { + "post": { + "operationId": "createSchedulingReassignment", "tags": [ - "Scheduling hosts" + "Scheduling reassignments" ], - "summary": "Update a host", - "description": "Changes what's given. A `disabled` host keeps its bookings but gets no new ones.", + "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": [] @@ -69515,15 +71372,17 @@ { "schema": { "type": "string", - "description": "The host's id or uid.", + "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": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" + "welcome-user-123" ] }, - "required": true, - "description": "The host's id or uid.", - "name": "host", - "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" @@ -69534,14 +71393,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateHostRequest" + "$ref": "#/components/schemas/CreateSchedulingReassignmentRequest" } } } }, "responses": { - "200": { - "description": "The host.", + "202": { + "description": "The run, waiting to start.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -69552,6 +71411,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": { @@ -69565,7 +71433,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingHost" + "$ref": "#/components/schemas/SchedulingReassignment" } } } @@ -69654,6 +71522,27 @@ } } }, + "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": { @@ -69676,7 +71565,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 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.", @@ -69767,31 +71656,39 @@ } } }, - "delete": { - "operationId": "deleteSchedulingHost", + "get": { + "operationId": "listSchedulingReassignments", "tags": [ - "Scheduling hosts" + "Scheduling reassignments" ], - "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 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 host's id or uid.", - "examples": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" - ] + "description": "The last id of the previous page." }, - "required": true, - "description": "The host's id or uid.", - "name": "host", - "in": "path" + "required": false, + "description": "The last id of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -69799,7 +71696,7 @@ ], "responses": { "200": { - "description": "Deleted.", + "description": "The runs.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -69823,7 +71720,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSchedulingHost" + "$ref": "#/components/schemas/SchedulingReassignmentList" } } } @@ -69891,27 +71788,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" - } - } - } - }, "422": { "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", "headers": { @@ -70005,14 +71881,14 @@ } } }, - "/v1/scheduling/bookings/{id}/reassign": { - "post": { - "operationId": "reassignSchedulingBooking", + "/v1/scheduling/reassignments/{id}": { + "get": { + "operationId": "getSchedulingReassignment", "tags": [ - "Scheduling bookings" + "Scheduling reassignments" ], - "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.", + "summary": "Retrieve a bulk reassignment", + "description": "How it's going.", "security": [ { "bearerAuth": [] @@ -70022,13 +71898,10 @@ { "schema": { "type": "string", - "description": "The booking's id.", - "examples": [ - "book_01kd2m9a0b1c2d3e4f5g6h7j8m" - ] + "description": "The reassignment's id." }, "required": true, - "description": "The booking's id.", + "description": "The reassignment's id.", "name": "id", "in": "path" }, @@ -70036,19 +71909,9 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReassignSchedulingBookingRequest" - } - } - } - }, "responses": { "200": { - "description": "The booking.", + "description": "The run.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -70072,13 +71935,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingBooking" + "$ref": "#/components/schemas/SchedulingReassignment" } } } }, "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.", @@ -70161,48 +72024,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 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.", - "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": { @@ -70275,14 +72096,14 @@ } } }, - "/v1/scheduling/bookings/{id}/reassign_candidates": { + "/v1/scheduling/reassignments/{id}/items": { "get": { - "operationId": "listSchedulingReassignCandidates", + "operationId": "listSchedulingReassignmentItems", "tags": [ - "Scheduling bookings" + "Scheduling reassignments" ], - "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.", + "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": [] @@ -70292,24 +72113,32 @@ { "schema": { "type": "string", - "description": "The booking's id.", - "examples": [ - "book_01kd2m9a0b1c2d3e4f5g6h7j8m" - ] + "description": "The reassignment's id." }, "required": true, - "description": "The booking's id.", + "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 host who'd leave, when it could be several." + "description": "The last id of the previous page." }, "required": false, - "description": "The host who'd leave, when it could be several.", - "name": "from_host", + "description": "The last id of the previous page.", + "name": "after", "in": "query" }, { @@ -70318,7 +72147,7 @@ ], "responses": { "200": { - "description": "The candidates.", + "description": "The items.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -70342,7 +72171,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingReassignCandidateList" + "$ref": "#/components/schemas/SchedulingReassignmentItemList" } } } @@ -70524,32 +72353,37 @@ } } }, - "/v1/scheduling/reassignments/preview": { + "/v1/scheduling/reassignments/{id}/cancel": { "post": { - "operationId": "previewSchedulingReassignment", + "operationId": "cancelSchedulingReassignment", "tags": [ "Scheduling 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.", + "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": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PreviewSchedulingReassignmentRequest" - } - } + "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": "What would happen.", + "description": "The run.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -70573,13 +72407,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingReassignmentPreview" + "$ref": "#/components/schemas/SchedulingReassignment" } } } }, "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.", @@ -70662,48 +72496,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 selection or strategy doesn't make sense (`invalid_reassignment`).", - "headers": { - "X-Request-Id": { - "description": "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": { @@ -70773,22 +72565,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/scheduling/reassignments": { + "/v1/scheduling/reassignments/{id}/undo": { "post": { - "operationId": "createSchedulingReassignment", + "operationId": "undoSchedulingReassignment", "tags": [ "Scheduling 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`.", + "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": [] @@ -70798,17 +72585,12 @@ { "schema": { "type": "string", - "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": [ - "welcome-user-123" - ] + "description": "The reassignment's id." }, - "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" + "required": true, + "description": "The reassignment's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -70819,14 +72601,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateSchedulingReassignmentRequest" + "$ref": "#/components/schemas/UndoSchedulingReassignmentRequest" } } } }, "responses": { "202": { - "description": "The run, waiting to start.", + "description": "The new run.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -70837,15 +72619,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": { @@ -70948,27 +72721,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": { @@ -70991,7 +72743,7 @@ } }, "422": { - "description": "The selection or strategy doesn't make sense, or covers too many bookings (`invalid_reassignment`).", + "description": "It's still running (`reassignment_not_finished`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -71081,48 +72833,34 @@ } } } - }, - "get": { - "operationId": "listSchedulingReassignments", + } + }, + "/v1/scheduling/holiday_calendars": { + "post": { + "operationId": "createSchedulingHolidayCalendar", "tags": [ - "Scheduling reassignments" + "Scheduling holiday calendars" ], - "summary": "List bulk reassignments", - "description": "Newest first, with how each is going.", + "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": "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" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSchedulingHolidayCalendarRequest" + } + } } - ], + }, "responses": { - "200": { - "description": "The runs.", + "201": { + "description": "The calendar.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -71146,13 +72884,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingReassignmentList" + "$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.", @@ -71214,8 +72952,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": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", + "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.", @@ -71304,40 +73084,28 @@ } } } - } - } - }, - "/v1/scheduling/reassignments/{id}": { + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, "get": { - "operationId": "getSchedulingReassignment", + "operationId": "listSchedulingHolidayCalendars", "tags": [ - "Scheduling reassignments" + "Scheduling holiday calendars" ], - "summary": "Retrieve a bulk reassignment", - "description": "How it's going.", + "summary": "List holiday calendars", + "description": "By name. A feed's URL is never shown again.", "security": [ { "bearerAuth": [] } ], - "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 run.", + "description": "The calendars.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -71361,7 +73129,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingReassignment" + "$ref": "#/components/schemas/SchedulingHolidayCalendarList" } } } @@ -71429,27 +73197,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" - } - } - } - }, "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": { @@ -71519,17 +73266,22 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/scheduling/reassignments/{id}/items": { + "/v1/scheduling/holiday_calendars/{id}": { "get": { - "operationId": "listSchedulingReassignmentItems", + "operationId": "getSchedulingHolidayCalendar", "tags": [ - "Scheduling reassignments" + "Scheduling holiday calendars" ], - "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.", + "summary": "Retrieve a holiday calendar", + "description": "With when it was last read, and why not if it couldn't be.", "security": [ { "bearerAuth": [] @@ -71539,41 +73291,20 @@ { "schema": { "type": "string", - "description": "The reassignment's id." + "description": "The holiday calendar's id." }, "required": true, - "description": "The reassignment's id.", + "description": "The holiday calendar'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": { "200": { - "description": "The items.", + "description": "The calendar.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -71597,7 +73328,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingReassignmentItemList" + "$ref": "#/components/schemas/SchedulingHolidayCalendar" } } } @@ -71686,27 +73417,6 @@ } } }, - "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": { @@ -71777,16 +73487,14 @@ } } } - } - }, - "/v1/scheduling/reassignments/{id}/cancel": { - "post": { - "operationId": "cancelSchedulingReassignment", + }, + "patch": { + "operationId": "updateSchedulingHolidayCalendar", "tags": [ - "Scheduling reassignments" + "Scheduling holiday calendars" ], - "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.", + "summary": "Update a holiday calendar", + "description": "Who it applies to, who covers and what happens to bookings. Takes effect shortly.", "security": [ { "bearerAuth": [] @@ -71796,10 +73504,10 @@ { "schema": { "type": "string", - "description": "The reassignment's id." + "description": "The holiday calendar's id." }, "required": true, - "description": "The reassignment's id.", + "description": "The holiday calendar's id.", "name": "id", "in": "path" }, @@ -71807,9 +73515,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSchedulingHolidayCalendarRequest" + } + } + } + }, "responses": { "200": { - "description": "The run.", + "description": "The calendar.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -71833,13 +73551,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingReassignment" + "$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.", @@ -71922,6 +73640,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 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": { @@ -71992,16 +73752,14 @@ } } } - } - }, - "/v1/scheduling/reassignments/{id}/undo": { - "post": { - "operationId": "undoSchedulingReassignment", + }, + "delete": { + "operationId": "deleteSchedulingHolidayCalendar", "tags": [ - "Scheduling reassignments" + "Scheduling holiday calendars" ], - "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`.", + "summary": "Delete a holiday calendar", + "description": "Hosts can be booked on its days again, shortly: past entries stay.", "security": [ { "bearerAuth": [] @@ -72011,10 +73769,10 @@ { "schema": { "type": "string", - "description": "The reassignment's id." + "description": "The holiday calendar's id." }, "required": true, - "description": "The reassignment's id.", + "description": "The holiday calendar's id.", "name": "id", "in": "path" }, @@ -72022,19 +73780,9 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UndoSchedulingReassignmentRequest" - } - } - } - }, "responses": { - "202": { - "description": "The new run.", + "200": { + "description": "It's going.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -72058,13 +73806,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingReassignment" + "$ref": "#/components/schemas/DeletedSchedulingHolidayCalendar" } } } }, "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.", @@ -72147,48 +73895,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": "It's still running (`reassignment_not_finished`).", - "headers": { - "X-Request-Id": { - "description": "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": { @@ -72261,32 +73967,37 @@ } } }, - "/v1/scheduling/holiday_calendars": { - "post": { - "operationId": "createSchedulingHolidayCalendar", + "/v1/scheduling/holiday_calendars/{id}/holidays": { + "get": { + "operationId": "listSchedulingHolidays", "tags": [ "Scheduling 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.", + "summary": "List a calendar's holidays", + "description": "What's coming, with names, as last read. Empty until its first read.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSchedulingHolidayCalendarRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The holiday calendar's id." + }, + "required": true, + "description": "The holiday calendar's id.", + "name": "id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { - "201": { - "description": "The calendar.", + "200": { + "description": "The holidays.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -72310,13 +74021,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingHolidayCalendar" + "$ref": "#/components/schemas/SchedulingHolidayList" } } } }, "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.", @@ -72399,48 +74110,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": "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": { @@ -72510,28 +74179,35 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, - "get": { - "operationId": "listSchedulingHolidayCalendars", + } + } + }, + "/v1/scheduling/out_of_office": { + "post": { + "operationId": "createSchedulingOutOfOffice", "tags": [ - "Scheduling holiday calendars" + "Scheduling out of office" ], - "summary": "List holiday calendars", - "description": "By name. A feed's URL is never shown again.", + "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": [] } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSchedulingOutOfOfficeRequest" + } + } + } + }, "responses": { - "200": { - "description": "The calendars.", + "201": { + "description": "An entry per host.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -72555,13 +74231,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingHolidayCalendarList" + "$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.", @@ -72623,6 +74299,69 @@ } } }, + "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": { + "X-Request-Id": { + "description": "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": { @@ -72698,31 +74437,86 @@ "$ref": "#/components/parameters/CossVersion" } ] - } - }, - "/v1/scheduling/holiday_calendars/{id}": { + }, "get": { - "operationId": "getSchedulingHolidayCalendar", + "operationId": "listSchedulingOutOfOffice", "tags": [ - "Scheduling holiday calendars" + "Scheduling out of office" ], - "summary": "Retrieve a holiday calendar", - "description": "With when it was last read, and why not if it couldn't be.", + "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 holiday calendar's id." + "description": "The last id of the previous page." }, - "required": true, - "description": "The holiday calendar's id.", - "name": "id", - "in": "path" + "required": false, + "description": "The last id of the previous page.", + "name": "after", + "in": "query" + }, + { + "schema": { + "type": "string", + "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": [ + "2027-01-04T15:00:00.000Z" + ] + }, + "required": false, + "description": "Entries ending after this.", + "name": "start_after", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Entries starting before this.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + }, + "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" @@ -72730,7 +74524,7 @@ ], "responses": { "200": { - "description": "The calendar.", + "description": "A page of entries.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -72754,7 +74548,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingHolidayCalendar" + "$ref": "#/components/schemas/SchedulingOutOfOfficeList" } } } @@ -72843,6 +74637,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": { @@ -72913,14 +74728,16 @@ } } } - }, - "patch": { - "operationId": "updateSchedulingHolidayCalendar", + } + }, + "/v1/scheduling/out_of_office/{id}": { + "get": { + "operationId": "getSchedulingOutOfOffice", "tags": [ - "Scheduling holiday calendars" + "Scheduling out of office" ], - "summary": "Update a holiday calendar", - "description": "Who it applies to, who covers and what happens to bookings. Takes effect shortly.", + "summary": "Retrieve an out of office entry", + "description": "One entry.", "security": [ { "bearerAuth": [] @@ -72930,10 +74747,13 @@ { "schema": { "type": "string", - "description": "The holiday calendar's id." + "description": "The entry's id.", + "examples": [ + "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] }, "required": true, - "description": "The holiday calendar's id.", + "description": "The entry's id.", "name": "id", "in": "path" }, @@ -72941,19 +74761,9 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateSchedulingHolidayCalendarRequest" - } - } - } - }, "responses": { "200": { - "description": "The calendar.", + "description": "The entry.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -72977,13 +74787,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingHolidayCalendar" + "$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.", @@ -73066,48 +74876,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 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": { @@ -73179,13 +74947,13 @@ } } }, - "delete": { - "operationId": "deleteSchedulingHolidayCalendar", + "patch": { + "operationId": "updateSchedulingOutOfOffice", "tags": [ - "Scheduling holiday calendars" + "Scheduling out of office" ], - "summary": "Delete a holiday calendar", - "description": "Hosts can be booked on its days again, shortly: past entries stay.", + "summary": "Update an out of office entry", + "description": "Changes what's given. An entry stays with its host.", "security": [ { "bearerAuth": [] @@ -73195,10 +74963,13 @@ { "schema": { "type": "string", - "description": "The holiday calendar's id." + "description": "The entry's id.", + "examples": [ + "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] }, "required": true, - "description": "The holiday calendar's id.", + "description": "The entry's id.", "name": "id", "in": "path" }, @@ -73206,9 +74977,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSchedulingOutOfOfficeRequest" + } + } + } + }, "responses": { "200": { - "description": "It's going.", + "description": "The entry.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -73232,13 +75013,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSchedulingHolidayCalendar" + "$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.", @@ -73321,6 +75102,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 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": { @@ -73391,16 +75214,14 @@ } } } - } - }, - "/v1/scheduling/holiday_calendars/{id}/holidays": { - "get": { - "operationId": "listSchedulingHolidays", + }, + "delete": { + "operationId": "deleteSchedulingOutOfOffice", "tags": [ - "Scheduling holiday calendars" + "Scheduling out of office" ], - "summary": "List a calendar's holidays", - "description": "What's coming, with names, as last read. Empty until its first read.", + "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": [] @@ -73410,10 +75231,13 @@ { "schema": { "type": "string", - "description": "The holiday calendar's id." + "description": "The entry's id.", + "examples": [ + "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] }, "required": true, - "description": "The holiday calendar's id.", + "description": "The entry's id.", "name": "id", "in": "path" }, @@ -73423,7 +75247,7 @@ ], "responses": { "200": { - "description": "The holidays.", + "description": "Deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -73447,7 +75271,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingHolidayList" + "$ref": "#/components/schemas/DeletedSchedulingOutOfOffice" } } } @@ -73608,32 +75432,80 @@ } } }, - "/v1/scheduling/out_of_office": { - "post": { - "operationId": "createSchedulingOutOfOffice", + "/v1/scheduling/out_of_office/by_external_id/{external_id}": { + "put": { + "operationId": "putSchedulingOutOfOffice", "tags": [ "Scheduling 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.", + "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": [] } ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "Your own id for the entry.", + "examples": [ + "hr_leave_4471" + ] + }, + "required": true, + "description": "Your own id for the entry.", + "name": "external_id", + "in": "path" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateSchedulingOutOfOfficeRequest" + "$ref": "#/components/schemas/PutSchedulingOutOfOfficeRequest" } } } }, "responses": { + "200": { + "description": "The entry, replaced.", + "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" + } + } + } + }, "201": { - "description": "An entry per host.", + "description": "The entry, new.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -73657,7 +75529,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingOutOfOfficeList" + "$ref": "#/components/schemas/SchedulingOutOfOffice" } } } @@ -73857,91 +75729,45 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, + } + } + }, + "/v1/scheduling/out_of_office/{id}/conflicts": { "get": { - "operationId": "listSchedulingOutOfOffice", + "operationId": "listSchedulingOutOfOfficeConflicts", "tags": [ "Scheduling out of office" ], - "summary": "List out of office entries", - "description": "The environment's entries, newest first.", + "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": [] } ], "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" - }, - { - "schema": { - "type": "string", - "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": [ - "2027-01-04T15:00:00.000Z" - ] - }, - "required": false, - "description": "Entries ending after this.", - "name": "start_after", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "Entries starting before this.", + "description": "The entry's id.", "examples": [ - "2027-01-04T15:00:00.000Z" + "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, - "required": false, - "description": "Entries starting before this.", - "name": "start_before", - "in": "query" + "required": true, + "description": "The entry's id.", + "name": "id", + "in": "path" }, { "schema": { - "type": "string", - "description": "The entries one request made." + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 100 }, "required": false, - "description": "The entries one request made.", - "name": "group", + "name": "limit", "in": "query" }, { @@ -73950,7 +75776,7 @@ ], "responses": { "200": { - "description": "A page of entries.", + "description": "The bookings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -73974,7 +75800,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingOutOfOfficeList" + "$ref": "#/components/schemas/SchedulingOutOfOfficeConflictList" } } } @@ -74156,14 +75982,14 @@ } } }, - "/v1/scheduling/out_of_office/{id}": { - "get": { - "operationId": "getSchedulingOutOfOffice", + "/v1/scheduling/hosts/{host}/schedules": { + "post": { + "operationId": "createSchedulingSchedule", "tags": [ - "Scheduling out of office" + "Scheduling schedules" ], - "summary": "Retrieve an out of office entry", - "description": "One entry.", + "summary": "Create a schedule", + "description": "A host's weekly hours and date overrides, in local time. The first one is the host's default.", "security": [ { "bearerAuth": [] @@ -74173,23 +75999,33 @@ { "schema": { "type": "string", - "description": "The entry's id.", + "description": "The host's id or uid.", "examples": [ - "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The entry's id.", - "name": "id", + "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/UpsertScheduleRequest" + } + } + } + }, "responses": { - "200": { - "description": "The entry.", + "201": { + "description": "The schedule.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -74213,13 +76049,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingOutOfOffice" + "$ref": "#/components/schemas/SchedulingSchedule" } } } }, "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.", @@ -74302,6 +76138,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 request failed validation (`validation_error`, see `issues`), a range ends before it starts or a date has two overrides (`invalid_schedule`), or `time_zone` isn't an IANA name (`invalid_time_zone`).", + "headers": { + "X-Request-Id": { + "description": "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": { @@ -74373,13 +76251,13 @@ } } }, - "patch": { - "operationId": "updateSchedulingOutOfOffice", + "get": { + "operationId": "listSchedulingSchedules", "tags": [ - "Scheduling out of office" + "Scheduling schedules" ], - "summary": "Update an out of office entry", - "description": "Changes what's given. An entry stays with its host.", + "summary": "List a host's schedules", + "description": "All of the host's schedules, oldest first.", "security": [ { "bearerAuth": [] @@ -74389,33 +76267,23 @@ { "schema": { "type": "string", - "description": "The entry's id.", + "description": "The host's id or uid.", "examples": [ - "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The entry's id.", - "name": "id", + "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/UpdateSchedulingOutOfOfficeRequest" - } - } - } - }, "responses": { "200": { - "description": "The entry.", + "description": "The schedules.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -74439,13 +76307,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingOutOfOffice" + "$ref": "#/components/schemas/SchedulingScheduleList" } } } }, "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.", @@ -74528,48 +76396,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 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": { @@ -74640,14 +76466,16 @@ } } } - }, - "delete": { - "operationId": "deleteSchedulingOutOfOffice", + } + }, + "/v1/scheduling/hosts/{host}/schedules/{schedule_id}": { + "get": { + "operationId": "getSchedulingSchedule", "tags": [ - "Scheduling out of office" + "Scheduling schedules" ], - "summary": "Delete an out of office entry", - "description": "The host can be booked in that time again, and any calendar event for it goes.", + "summary": "Retrieve a schedule", + "description": "One of the host's schedules.", "security": [ { "bearerAuth": [] @@ -74657,14 +76485,27 @@ { "schema": { "type": "string", - "description": "The entry's id.", + "description": "The host's id or uid.", "examples": [ - "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The entry's id.", - "name": "id", + "description": "The host's id or uid.", + "name": "host", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "The schedule's id.", + "examples": [ + "sched_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] + }, + "required": true, + "description": "The schedule's id.", + "name": "schedule_id", "in": "path" }, { @@ -74673,7 +76514,7 @@ ], "responses": { "200": { - "description": "Deleted.", + "description": "The schedule.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -74697,7 +76538,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSchedulingOutOfOffice" + "$ref": "#/components/schemas/SchedulingSchedule" } } } @@ -74856,16 +76697,14 @@ } } } - } - }, - "/v1/scheduling/out_of_office/by_external_id/{external_id}": { + }, "put": { - "operationId": "putSchedulingOutOfOffice", + "operationId": "updateSchedulingSchedule", "tags": [ - "Scheduling out of office" + "Scheduling schedules" ], - "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.", + "summary": "Replace a schedule", + "description": "Replaces its weekly rules, and its overrides when given. Existing bookings stay: a schedule only decides what's offered next.", "security": [ { "bearerAuth": [] @@ -74875,14 +76714,27 @@ { "schema": { "type": "string", - "description": "Your own id for the entry.", + "description": "The host's id or uid.", "examples": [ - "hr_leave_4471" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "Your own id for the entry.", - "name": "external_id", + "description": "The host's id or uid.", + "name": "host", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "The schedule's id.", + "examples": [ + "sched_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] + }, + "required": true, + "description": "The schedule's id.", + "name": "schedule_id", "in": "path" }, { @@ -74894,44 +76746,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PutSchedulingOutOfOfficeRequest" + "$ref": "#/components/schemas/UpsertScheduleRequest" } } } }, "responses": { "200": { - "description": "The entry, replaced.", - "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" - } - } - } - }, - "201": { - "description": "The entry, new.", + "description": "The schedule.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -74955,7 +76777,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingOutOfOffice" + "$ref": "#/components/schemas/SchedulingSchedule" } } } @@ -75066,7 +76888,7 @@ } }, "422": { - "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", + "description": "The request failed validation (`validation_error`, see `issues`), a range ends before it starts or a date has two overrides (`invalid_schedule`), or `time_zone` isn't an IANA name (`invalid_time_zone`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -75156,16 +76978,14 @@ } } } - } - }, - "/v1/scheduling/out_of_office/{id}/conflicts": { - "get": { - "operationId": "listSchedulingOutOfOfficeConflicts", + }, + "delete": { + "operationId": "deleteSchedulingSchedule", "tags": [ - "Scheduling out of office" + "Scheduling schedules" ], - "summary": "List an entry's conflicts", - "description": "Live bookings of the host that fall in the entry, soonest first. Entries never change them.", + "summary": "Delete a schedule", + "description": "Refused for the host's default schedule.", "security": [ { "bearerAuth": [] @@ -75175,26 +76995,28 @@ { "schema": { "type": "string", - "description": "The entry's id.", + "description": "The host's id or uid.", "examples": [ - "ooo_01kd2m9a0b1c2d3e4f5g6h7j8m" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The entry's id.", - "name": "id", + "description": "The host's id or uid.", + "name": "host", "in": "path" }, { "schema": { - "type": "integer", - "minimum": 1, - "maximum": 500, - "default": 100 + "type": "string", + "description": "The schedule's id.", + "examples": [ + "sched_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] }, - "required": false, - "name": "limit", - "in": "query" + "required": true, + "description": "The schedule's id.", + "name": "schedule_id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -75202,7 +77024,7 @@ ], "responses": { "200": { - "description": "The bookings.", + "description": "Deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -75226,7 +77048,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingOutOfOfficeConflictList" + "$ref": "#/components/schemas/DeletedSchedulingSchedule" } } } @@ -75316,7 +77138,7 @@ } }, "422": { - "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", + "description": "This is the host's default schedule (`invalid_schedule`): make another one the default first.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -75408,50 +77230,32 @@ } } }, - "/v1/scheduling/hosts/{host}/schedules": { + "/v1/scheduling/event_types": { "post": { - "operationId": "createSchedulingSchedule", + "operationId": "createSchedulingEventType", "tags": [ - "Scheduling schedules" + "Scheduling event types" ], - "summary": "Create a schedule", - "description": "A host's weekly hours and date overrides, in local time. The first one is the host's default.", + "summary": "Create an event type", + "description": "What can be booked: `one_on_one` with one host, `pool` for any free host, `collective` when every required host must be free, or `group` for a session with seats.", "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/UpsertScheduleRequest" + "$ref": "#/components/schemas/CreateEventTypeRequest" } } } }, "responses": { "201": { - "description": "The schedule.", + "description": "The event type.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -75475,7 +77279,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingSchedule" + "$ref": "#/components/schemas/SchedulingEventType" } } } @@ -75522,6 +77326,27 @@ } } }, + "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": { @@ -75586,7 +77411,7 @@ } }, "422": { - "description": "The request failed validation (`validation_error`, see `issues`), a range ends before it starts or a date has two overrides (`invalid_schedule`), or `time_zone` isn't an IANA name (`invalid_time_zone`).", + "description": "The request failed validation (`validation_error`, see `issues`), the settings don't fit together (`invalid_event_type`), `grid_time_zone` isn't an IANA name (`invalid_time_zone`), another event type has the `uid` (`uid_taken`), or its text was refused (`content_refused`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -75675,33 +77500,46 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] }, "get": { - "operationId": "listSchedulingSchedules", + "operationId": "listSchedulingEventTypes", "tags": [ - "Scheduling schedules" + "Scheduling event types" ], - "summary": "List a host's schedules", - "description": "All of the host's schedules, oldest first.", + "summary": "List event types", + "description": "The environment's event types, 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 host's id or uid.", - "examples": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" - ] + "description": "The last id of the previous page." }, - "required": true, - "description": "The host's id or uid.", - "name": "host", - "in": "path" + "required": false, + "description": "The last id of the previous page.", + "name": "after", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -75709,7 +77547,7 @@ ], "responses": { "200": { - "description": "The schedules.", + "description": "A page of event types.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -75733,7 +77571,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingScheduleList" + "$ref": "#/components/schemas/SchedulingEventTypeList" } } } @@ -75801,8 +77639,8 @@ } } }, - "404": { - "description": "Nothing with this id or uid exists in the 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.", @@ -75894,14 +77732,14 @@ } } }, - "/v1/scheduling/hosts/{host}/schedules/{schedule_id}": { + "/v1/scheduling/event_types/{event_type}": { "get": { - "operationId": "getSchedulingSchedule", + "operationId": "getSchedulingEventType", "tags": [ - "Scheduling schedules" + "Scheduling event types" ], - "summary": "Retrieve a schedule", - "description": "One of the host's schedules.", + "summary": "Retrieve an event type", + "description": "An event type by its id or your uid.", "security": [ { "bearerAuth": [] @@ -75911,27 +77749,14 @@ { "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 schedule's id.", + "description": "The event type's id or uid.", "examples": [ - "sched_01kd2m9a0b1c2d3e4f5g6h7j8m" + "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The schedule's id.", - "name": "schedule_id", + "description": "The event type's id or uid.", + "name": "event_type", "in": "path" }, { @@ -75940,7 +77765,7 @@ ], "responses": { "200": { - "description": "The schedule.", + "description": "The event type.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -75964,7 +77789,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingSchedule" + "$ref": "#/components/schemas/SchedulingEventType" } } } @@ -76124,13 +77949,13 @@ } } }, - "put": { - "operationId": "updateSchedulingSchedule", + "patch": { + "operationId": "updateSchedulingEventType", "tags": [ - "Scheduling schedules" + "Scheduling event types" ], - "summary": "Replace a schedule", - "description": "Replaces its weekly rules, and its overrides when given. Existing bookings stay: a schedule only decides what's offered next.", + "summary": "Update an event type", + "description": "Changes what's given. Existing bookings keep what they were booked with. Set `status` to `archived` to stop new bookings.", "security": [ { "bearerAuth": [] @@ -76140,27 +77965,14 @@ { "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 schedule's id.", + "description": "The event type's id or uid.", "examples": [ - "sched_01kd2m9a0b1c2d3e4f5g6h7j8m" + "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The schedule's id.", - "name": "schedule_id", + "description": "The event type's id or uid.", + "name": "event_type", "in": "path" }, { @@ -76172,14 +77984,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpsertScheduleRequest" + "$ref": "#/components/schemas/UpdateEventTypeRequest" } } } }, "responses": { "200": { - "description": "The schedule.", + "description": "The event type.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76203,7 +78015,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingSchedule" + "$ref": "#/components/schemas/SchedulingEventType" } } } @@ -76250,6 +78062,27 @@ } } }, + "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": { @@ -76314,7 +78147,7 @@ } }, "422": { - "description": "The request failed validation (`validation_error`, see `issues`), a range ends before it starts or a date has two overrides (`invalid_schedule`), or `time_zone` isn't an IANA name (`invalid_time_zone`).", + "description": "The request failed validation (`validation_error`, see `issues`), the settings don't fit together (`invalid_event_type`), `grid_time_zone` isn't an IANA name (`invalid_time_zone`), another event type has the `uid` (`uid_taken`), or its text was refused (`content_refused`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76404,14 +78237,16 @@ } } } - }, - "delete": { - "operationId": "deleteSchedulingSchedule", + } + }, + "/v1/scheduling/event_types/{event_type}/slots": { + "get": { + "operationId": "listSchedulingSlots", "tags": [ - "Scheduling schedules" + "Scheduling availability" ], - "summary": "Delete a schedule", - "description": "Refused for the host's default schedule.", + "summary": "List available slots", + "description": "Starts an attendee can book between `start` and `end` (at most 62 days), as UTC instants to show in their own time zone. A host whose calendar can't be read isn't offered.", "security": [ { "bearerAuth": [] @@ -76421,28 +78256,71 @@ { "schema": { "type": "string", - "description": "The host's id or uid.", + "description": "The event type's id or uid.", "examples": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" + "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The host's id or uid.", - "name": "host", + "description": "The event type's id or uid.", + "name": "event_type", "in": "path" }, { "schema": { "type": "string", - "description": "The schedule's id.", + "description": "An ISO 8601 instant with an offset.", "examples": [ - "sched_01kd2m9a0b1c2d3e4f5g6h7j8m" + "2027-01-04T15:00:00.000Z" ] }, "required": true, - "description": "The schedule's id.", - "name": "schedule_id", - "in": "path" + "description": "An ISO 8601 instant with an offset.", + "name": "start", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + }, + "required": true, + "description": "An ISO 8601 instant with an offset.", + "name": "end", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Only this host's slots (id or uid)." + }, + "required": false, + "description": "Only this host's slots (id or uid).", + "name": "host", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 5, + "maximum": 1440 + }, + "required": false, + "name": "duration_minutes", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "A JSON object narrowing a pool, such as `{\"licensed_in\":\"CA\"}`." + }, + "required": false, + "description": "A JSON object narrowing a pool, such as `{\"licensed_in\":\"CA\"}`.", + "name": "attributes", + "in": "query" }, { "$ref": "#/components/parameters/CossVersion" @@ -76450,7 +78328,7 @@ ], "responses": { "200": { - "description": "Deleted.", + "description": "The slots.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76474,7 +78352,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSchedulingSchedule" + "$ref": "#/components/schemas/SchedulingSlotList" } } } @@ -76521,6 +78399,27 @@ } } }, + "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": { @@ -76564,7 +78463,7 @@ } }, "422": { - "description": "This is the host's default schedule (`invalid_schedule`): make another one the default first.", + "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.", @@ -76656,32 +78555,83 @@ } } }, - "/v1/scheduling/event_types": { - "post": { - "operationId": "createSchedulingEventType", + "/v1/scheduling/event_types/{event_type}/slots/explain": { + "get": { + "operationId": "explainSchedulingSlot", "tags": [ - "Scheduling event types" + "Scheduling availability" ], - "summary": "Create an event type", - "description": "What can be booked: `one_on_one` with one host, `pool` for any free host, `collective` when every required host must be free, or `group` for a session with seats.", + "summary": "Explain a slot", + "description": "Why one start can or can't be booked right now, host by host, decided the same way a booking is. For support and debugging: it says when a host is busy, so don't show it to attendees.", "security": [ { "bearerAuth": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateEventTypeRequest" - } - } + "parameters": [ + { + "schema": { + "type": "string", + "description": "The event type's id or uid.", + "examples": [ + "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m" + ] + }, + "required": true, + "description": "The event type's id or uid.", + "name": "event_type", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + }, + "required": true, + "description": "An ISO 8601 instant with an offset.", + "name": "start", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Only this host (id or uid)." + }, + "required": false, + "description": "Only this host (id or uid).", + "name": "host", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 5, + "maximum": 1440 + }, + "required": false, + "name": "duration_minutes", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "A JSON object narrowing a pool, such as `{\"licensed_in\":\"CA\"}`." + }, + "required": false, + "description": "A JSON object narrowing a pool, such as `{\"licensed_in\":\"CA\"}`.", + "name": "attributes", + "in": "query" + }, + { + "$ref": "#/components/parameters/CossVersion" } - }, + ], "responses": { - "201": { - "description": "The event type.", + "200": { + "description": "Whether it can be booked, and why.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76705,13 +78655,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingEventType" + "$ref": "#/components/schemas/SchedulingSlotExplanation" } } } }, "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.", @@ -76815,29 +78765,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`), the settings don't fit together (`invalid_event_type`), `grid_time_zone` isn't an IANA name (`invalid_time_zone`), another event type has the `uid` (`uid_taken`), or its text was refused (`content_refused`).", + "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.", @@ -76926,54 +78855,35 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, - "get": { - "operationId": "listSchedulingEventTypes", + } + } + }, + "/v1/scheduling/availability/search": { + "post": { + "operationId": "searchSchedulingAvailability", "tags": [ - "Scheduling event types" + "Scheduling availability" ], - "summary": "List event types", - "description": "The environment's event types, newest first.", + "summary": "Search availability across hosts", + "description": "For \"who's free\" pages: each matching host's soonest slots, soonest first, across up to 500 hosts.", "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" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AvailabilitySearchRequest" + } + } } - ], + }, "responses": { "200": { - "description": "A page of event types.", + "description": "Hosts with slots.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -76997,13 +78907,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingEventTypeList" + "$ref": "#/components/schemas/SchedulingAvailabilitySearchResult" } } } }, "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.", @@ -77044,6 +78954,27 @@ } } }, + "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": { @@ -77065,8 +78996,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": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", + "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.", @@ -77155,17 +79128,22 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/scheduling/event_types/{event_type}": { - "get": { - "operationId": "getSchedulingEventType", + "/v1/scheduling/bookings": { + "post": { + "operationId": "createSchedulingBooking", "tags": [ - "Scheduling event types" + "Scheduling bookings" ], - "summary": "Retrieve an event type", - "description": "An event type by its id or your uid.", + "summary": "Create a booking", + "description": "Books a slot, or joins a group session. Without a `host`, a pool chooses fairly among those free. An event type that requires approval answers with a `pending_approval` booking.", "security": [ { "bearerAuth": [] @@ -77175,23 +79153,35 @@ { "schema": { "type": "string", - "description": "The event type's id or uid.", + "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": [ - "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m" + "welcome-user-123" ] }, - "required": true, - "description": "The event type's id or uid.", - "name": "event_type", - "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/CreateBookingRequest" + } + } + } + }, "responses": { - "200": { - "description": "The event type.", + "201": { + "description": "The booking.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -77202,6 +79192,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": { @@ -77215,13 +79214,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingEventType" + "$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.", @@ -77262,6 +79261,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": { @@ -77304,8 +79324,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).", + "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.", @@ -77315,12 +79335,6 @@ "req_01k7c8m4q2f3e9v6t5r1n0p8yz" ] } - }, - "Retry-After": { - "description": "Seconds to wait before retrying.", - "schema": { - "type": "integer" - } } }, "content": { @@ -77331,8 +79345,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.", @@ -77352,8 +79366,8 @@ } } }, - "503": { - "description": "The API key could not be verified right now; retry (`service_unavailable`).", + "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`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -77372,52 +79386,9 @@ } } } - } - } - }, - "patch": { - "operationId": "updateSchedulingEventType", - "tags": [ - "Scheduling event types" - ], - "summary": "Update an event type", - "description": "Changes what's given. Existing bookings keep what they were booked with. Set `status` to `archived` to stop new bookings.", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "The event type's id or uid.", - "examples": [ - "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m" - ] - }, - "required": true, - "description": "The event type's id or uid.", - "name": "event_type", - "in": "path" }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateEventTypeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The event 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.", @@ -77428,26 +79399,23 @@ ] } }, - "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/SchedulingEventType" + "$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`).", + "500": { + "description": "Unexpected server error (`api_error` / `internal_error`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -77467,8 +79435,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.", @@ -77487,9 +79455,115 @@ } } } + } + } + }, + "get": { + "operationId": "listSchedulingBookings", + "tags": [ + "Scheduling bookings" + ], + "summary": "List bookings", + "description": "The environment's bookings, newest first. Answers aren't included.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "required": false, + "name": "limit", + "in": "query" }, - "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`).", + { + "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" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "host", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "event_type", + "in": "query" + }, + { + "schema": { + "type": "string", + "enum": [ + "held", + "pending_approval", + "confirmed", + "cancelled", + "declined" + ] + }, + "required": false, + "name": "status", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "attendee_uid", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + }, + "required": false, + "description": "An ISO 8601 instant with an offset.", + "name": "start_after", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "An ISO 8601 instant with an offset.", + "examples": [ + "2027-01-04T15:00:00.000Z" + ] + }, + "required": false, + "description": "An ISO 8601 instant with an offset.", + "name": "start_before", + "in": "query" + }, + { + "$ref": "#/components/parameters/CossVersion" + } + ], + "responses": { + "200": { + "description": "A page of bookings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -77499,18 +79573,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/SchedulingBookingList" } } } }, - "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.", @@ -77530,8 +79613,8 @@ } } }, - "404": { - "description": "Nothing with this id or uid exists in the environment (`not_found`).", + "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.", @@ -77551,8 +79634,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.", @@ -77573,7 +79656,7 @@ } }, "422": { - "description": "The request failed validation (`validation_error`, see `issues`), the settings don't fit together (`invalid_event_type`), `grid_time_zone` isn't an IANA name (`invalid_time_zone`), another event type has the `uid` (`uid_taken`), or its text was refused (`content_refused`).", + "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.", @@ -77665,14 +79748,14 @@ } } }, - "/v1/scheduling/event_types/{event_type}/slots": { + "/v1/scheduling/bookings/{id}": { "get": { - "operationId": "listSchedulingSlots", + "operationId": "getSchedulingBooking", "tags": [ - "Scheduling availability" + "Scheduling bookings" ], - "summary": "List available slots", - "description": "Starts an attendee can book between `start` and `end` (at most 62 days), as UTC instants to show in their own time zone. A host whose calendar can't be read isn't offered.", + "summary": "Retrieve a booking", + "description": "A booking with its attendees and their answers.", "security": [ { "bearerAuth": [] @@ -77682,79 +79765,23 @@ { "schema": { "type": "string", - "description": "The event type's id or uid.", + "description": "The booking's id.", "examples": [ - "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m" + "book_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The event type's id or uid.", - "name": "event_type", + "description": "The booking's id.", + "name": "id", "in": "path" }, - { - "schema": { - "type": "string", - "description": "An ISO 8601 instant with an offset.", - "examples": [ - "2027-01-04T15:00:00.000Z" - ] - }, - "required": true, - "description": "An ISO 8601 instant with an offset.", - "name": "start", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "An ISO 8601 instant with an offset.", - "examples": [ - "2027-01-04T15:00:00.000Z" - ] - }, - "required": true, - "description": "An ISO 8601 instant with an offset.", - "name": "end", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "Only this host's slots (id or uid)." - }, - "required": false, - "description": "Only this host's slots (id or uid).", - "name": "host", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 5, - "maximum": 1440 - }, - "required": false, - "name": "duration_minutes", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "A JSON object narrowing a pool, such as `{\"licensed_in\":\"CA\"}`." - }, - "required": false, - "description": "A JSON object narrowing a pool, such as `{\"licensed_in\":\"CA\"}`.", - "name": "attributes", - "in": "query" - }, { "$ref": "#/components/parameters/CossVersion" } ], "responses": { "200": { - "description": "The slots.", + "description": "The booking.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -77778,7 +79805,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingSlotList" + "$ref": "#/components/schemas/SchedulingBooking" } } } @@ -77825,27 +79852,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": { @@ -77888,27 +79894,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": { @@ -77981,14 +79966,14 @@ } } }, - "/v1/scheduling/event_types/{event_type}/slots/explain": { - "get": { - "operationId": "explainSchedulingSlot", + "/v1/scheduling/bookings/{id}/reschedule": { + "post": { + "operationId": "rescheduleSchedulingBooking", "tags": [ - "Scheduling availability" + "Scheduling bookings" ], - "summary": "Explain a slot", - "description": "Why one start can or can't be booked right now, host by host, decided the same way a booking is. For support and debugging: it says when a host is busy, so don't show it to attendees.", + "summary": "Reschedule a booking", + "description": "Moves it to a new time, keeping its id. The old time stays booked if the new one isn't free.", "security": [ { "bearerAuth": [] @@ -77998,66 +79983,33 @@ { "schema": { "type": "string", - "description": "The event type's id or uid.", + "description": "The booking's id.", "examples": [ - "evtype_01kd2m9a0b1c2d3e4f5g6h7j8m" + "book_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The event type's id or uid.", - "name": "event_type", + "description": "The booking's id.", + "name": "id", "in": "path" }, - { - "schema": { - "type": "string", - "description": "An ISO 8601 instant with an offset.", - "examples": [ - "2027-01-04T15:00:00.000Z" - ] - }, - "required": true, - "description": "An ISO 8601 instant with an offset.", - "name": "start", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "Only this host (id or uid)." - }, - "required": false, - "description": "Only this host (id or uid).", - "name": "host", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 5, - "maximum": 1440 - }, - "required": false, - "name": "duration_minutes", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "A JSON object narrowing a pool, such as `{\"licensed_in\":\"CA\"}`." - }, - "required": false, - "description": "A JSON object narrowing a pool, such as `{\"licensed_in\":\"CA\"}`.", - "name": "attributes", - "in": "query" - }, { "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RescheduleBookingRequest" + } + } + } + }, "responses": { "200": { - "description": "Whether it can be booked, and why.", + "description": "The booking.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -78081,13 +80033,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingSlotExplanation" + "$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.", @@ -78128,8 +80080,8 @@ } } }, - "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`).", + "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.", @@ -78149,8 +80101,8 @@ } } }, - "403": { - "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", + "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.", @@ -78170,8 +80122,8 @@ } } }, - "404": { - "description": "Nothing with this id or uid exists in the 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.", @@ -78192,7 +80144,7 @@ } }, "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`).", + "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.", @@ -78284,32 +80236,50 @@ } } }, - "/v1/scheduling/availability/search": { + "/v1/scheduling/bookings/{id}/cancel": { "post": { - "operationId": "searchSchedulingAvailability", + "operationId": "cancelSchedulingBooking", "tags": [ - "Scheduling availability" + "Scheduling bookings" ], - "summary": "Search availability across hosts", - "description": "For \"who's free\" pages: each matching host's soonest slots, soonest first, across up to 500 hosts.", + "summary": "Cancel a booking", + "description": "Frees the time and removes the calendar event. A cancelled booking stays charged.", "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" + } + ], "requestBody": { - "required": true, + "required": false, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AvailabilitySearchRequest" + "$ref": "#/components/schemas/CancelSchedulingBookingRequest" } } } }, "responses": { "200": { - "description": "Hosts with slots.", + "description": "The booking.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -78333,13 +80303,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingAvailabilitySearchResult" + "$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`).", + "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.", @@ -78380,27 +80350,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": { @@ -78443,29 +80392,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`), 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`).", + "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.", @@ -78554,22 +80482,17 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/scheduling/bookings": { - "post": { - "operationId": "createSchedulingBooking", + "/v1/scheduling/bookings/{id}/events": { + "get": { + "operationId": "listSchedulingBookingEvents", "tags": [ "Scheduling bookings" ], - "summary": "Create a booking", - "description": "Books a slot, or joins a group session. Without a `host`, a pool chooses fairly among those free. An event type that requires approval answers with a `pending_approval` booking.", + "summary": "List a booking's history", + "description": "What's happened to a booking, oldest first, and who did it. There's no personal data in here.", "security": [ { "bearerAuth": [] @@ -78579,35 +80502,44 @@ { "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.", + "description": "The booking's id.", "examples": [ - "welcome-user-123" + "book_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, + "required": true, + "description": "The booking's id.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + }, "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" + "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" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateBookingRequest" - } - } - } - }, "responses": { - "201": { - "description": "The booking.", + "200": { + "description": "The events.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -78618,15 +80550,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": { @@ -78640,13 +80563,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingBooking" + "$ref": "#/components/schemas/SchedulingBookingEventList" } } } }, "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.", @@ -78687,27 +80610,6 @@ } } }, - "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": { @@ -78750,69 +80652,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": { - "X-Request-Id": { - "description": "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 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.", - "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": { @@ -78883,105 +80722,34 @@ } } } - }, - "get": { - "operationId": "listSchedulingBookings", + } + }, + "/v1/scheduling/bookings/{id}/approve": { + "post": { + "operationId": "approveSchedulingBooking", "tags": [ "Scheduling bookings" ], - "summary": "List bookings", - "description": "The environment's bookings, newest first. Answers aren't included.", + "summary": "Approve a booking", + "description": "Confirms a `pending_approval` booking.", "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" - }, - { - "schema": { - "type": "string" - }, - "required": false, - "name": "host", - "in": "query" - }, - { - "schema": { - "type": "string" - }, - "required": false, - "name": "event_type", - "in": "query" - }, { "schema": { "type": "string", - "enum": [ - "held", - "pending_approval", - "confirmed", - "cancelled", - "declined" - ] - }, - "required": false, - "name": "status", - "in": "query" - }, - { - "schema": { - "type": "string" - }, - "required": false, - "name": "attendee_uid", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "An ISO 8601 instant with an offset.", - "examples": [ - "2027-01-04T15:00:00.000Z" - ] - }, - "required": false, - "description": "An ISO 8601 instant with an offset.", - "name": "start_after", - "in": "query" - }, - { - "schema": { - "type": "string", - "description": "An ISO 8601 instant with an offset.", + "description": "The booking's id.", "examples": [ - "2027-01-04T15:00:00.000Z" + "book_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, - "required": false, - "description": "An ISO 8601 instant with an offset.", - "name": "start_before", - "in": "query" + "required": true, + "description": "The booking's id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -78989,7 +80757,7 @@ ], "responses": { "200": { - "description": "A page of bookings.", + "description": "The booking.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -79013,7 +80781,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingBookingList" + "$ref": "#/components/schemas/SchedulingBooking" } } } @@ -79081,8 +80849,29 @@ } } }, + "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" + } + } + } + }, "422": { - "description": "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", + "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.", @@ -79174,14 +80963,14 @@ } } }, - "/v1/scheduling/bookings/{id}": { - "get": { - "operationId": "getSchedulingBooking", + "/v1/scheduling/bookings/{id}/decline": { + "post": { + "operationId": "declineSchedulingBooking", "tags": [ "Scheduling bookings" ], - "summary": "Retrieve a booking", - "description": "A booking with its attendees and their answers.", + "summary": "Decline a booking", + "description": "Refuses a `pending_approval` booking and frees the time.", "security": [ { "bearerAuth": [] @@ -79320,6 +81109,27 @@ } } }, + "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": { @@ -79392,14 +81202,14 @@ } } }, - "/v1/scheduling/bookings/{id}/reschedule": { - "post": { - "operationId": "rescheduleSchedulingBooking", + "/v1/scheduling/bookings/{id}/notifications": { + "get": { + "operationId": "listSchedulingBookingNotifications", "tags": [ "Scheduling bookings" ], - "summary": "Reschedule a booking", - "description": "Moves it to a new time, keeping its id. The old time stays booked if the new one isn't free.", + "summary": "List a booking's notifications", + "description": "What's been queued for this booking and what became of it, newest first, including why something wasn't sent. It never says who it went to.", "security": [ { "bearerAuth": [] @@ -79419,23 +81229,34 @@ "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" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RescheduleBookingRequest" - } - } - } - }, "responses": { "200": { - "description": "The booking.", + "description": "The notifications.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -79459,13 +81280,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingBooking" + "$ref": "#/components/schemas/SchedulingBookingNotificationList" } } } }, "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.", @@ -79548,48 +81369,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": { - "X-Request-Id": { - "description": "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": { @@ -79662,14 +81441,14 @@ } } }, - "/v1/scheduling/bookings/{id}/cancel": { + "/v1/scheduling/bookings/{id}/no_show": { "post": { - "operationId": "cancelSchedulingBooking", + "operationId": "markSchedulingBookingNoShow", "tags": [ "Scheduling bookings" ], - "summary": "Cancel a booking", - "description": "Frees the time and removes the calendar event. A cancelled booking stays charged.", + "summary": "Mark a no-show", + "description": "Records that the attendee didn't come, once the booking has started.", "security": [ { "bearerAuth": [] @@ -79693,16 +81472,6 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CancelSchedulingBookingRequest" - } - } - } - }, "responses": { "200": { "description": "The booking.", @@ -79911,14 +81680,14 @@ } } }, - "/v1/scheduling/bookings/{id}/events": { - "get": { - "operationId": "listSchedulingBookingEvents", + "/v1/scheduling/bookings/{id}/location": { + "patch": { + "operationId": "updateSchedulingBookingLocation", "tags": [ "Scheduling bookings" ], - "summary": "List a booking's history", - "description": "What's happened to a booking, oldest first, and who did it. There's no personal data in here.", + "summary": "Set a booking's location", + "description": "Where it happens, such as the meeting link you made after the `scheduling.booking.confirmed` webhook.", "security": [ { "bearerAuth": [] @@ -79938,34 +81707,23 @@ "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" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateBookingLocationRequest" + } + } + } + }, "responses": { "200": { - "description": "The events.", + "description": "The booking.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -79989,13 +81747,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingBookingEventList" + "$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.", @@ -80078,6 +81836,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 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": { @@ -80150,14 +81950,14 @@ } } }, - "/v1/scheduling/bookings/{id}/approve": { + "/v1/scheduling/holds": { "post": { - "operationId": "approveSchedulingBooking", + "operationId": "createSchedulingHold", "tags": [ "Scheduling bookings" ], - "summary": "Approve a booking", - "description": "Confirms a `pending_approval` booking.", + "summary": "Hold a slot", + "description": "Keeps a slot for a while (the event type's `hold_ttl_seconds`) so you can take payment, then confirm it. Answers with a `held` booking, which isn't charged and sends nothing.", "security": [ { "bearerAuth": [] @@ -80167,23 +81967,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/CreateHoldRequest" + } + } + } + }, "responses": { - "200": { - "description": "The booking.", + "201": { + "description": "The held booking.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -80194,6 +82006,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": { @@ -80213,7 +82034,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.", @@ -80254,6 +82075,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": { @@ -80296,8 +82138,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.", @@ -80389,14 +82273,14 @@ } } }, - "/v1/scheduling/bookings/{id}/decline": { + "/v1/scheduling/holds/{id}/confirm": { "post": { - "operationId": "declineSchedulingBooking", + "operationId": "confirmSchedulingHold", "tags": [ "Scheduling bookings" ], - "summary": "Decline a booking", - "description": "Refuses a `pending_approval` booking and frees the time.", + "summary": "Confirm a hold", + "description": "Turns a hold into a booking. Always succeeds before the hold expires. Repeating it is safe.", "security": [ { "bearerAuth": [] @@ -80420,6 +82304,16 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConfirmHoldRequest" + } + } + } + }, "responses": { "200": { "description": "The booking.", @@ -80452,7 +82346,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.", @@ -80535,6 +82429,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" + } + } + } + }, "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": { @@ -80628,14 +82543,14 @@ } } }, - "/v1/scheduling/bookings/{id}/notifications": { - "get": { - "operationId": "listSchedulingBookingNotifications", + "/v1/scheduling/holds/{id}": { + "delete": { + "operationId": "releaseSchedulingHold", "tags": [ "Scheduling bookings" ], - "summary": "List a booking's notifications", - "description": "What's been queued for this booking and what became of it, newest first, including why something wasn't sent. It never says who it went to.", + "summary": "Release a hold", + "description": "Gives the slot back early.", "security": [ { "bearerAuth": [] @@ -80655,34 +82570,13 @@ "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": { "200": { - "description": "The notifications.", + "description": "The cancelled booking.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -80706,7 +82600,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingBookingNotificationList" + "$ref": "#/components/schemas/SchedulingBooking" } } } @@ -80795,6 +82689,27 @@ } } }, + "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": { @@ -80867,14 +82782,14 @@ } } }, - "/v1/scheduling/bookings/{id}/no_show": { + "/v1/scheduling/series": { "post": { - "operationId": "markSchedulingBookingNoShow", + "operationId": "createSchedulingSeries", "tags": [ - "Scheduling bookings" + "Scheduling series" ], - "summary": "Mark a no-show", - "description": "Records that the attendee didn't come, once the booking has started.", + "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": [] @@ -80884,23 +82799,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 booking.", + "201": { + "description": "The series and its bookings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -80911,6 +82838,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": { @@ -80924,13 +82860,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.", @@ -80971,6 +82907,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": { @@ -81013,8 +82970,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.", @@ -81106,50 +83105,32 @@ } } }, - "/v1/scheduling/bookings/{id}/location": { - "patch": { - "operationId": "updateSchedulingBookingLocation", + "/v1/scheduling/series/preview": { + "post": { + "operationId": "previewSchedulingSeries", "tags": [ - "Scheduling bookings" + "Scheduling series" ], - "summary": "Set a booking's location", - "description": "Where it happens, such as the meeting link you made after the `scheduling.booking.confirmed` webhook.", + "summary": "Preview a recurring series", + "description": "Which occurrences would be free, without booking anything.", "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" - } - ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateBookingLocationRequest" + "$ref": "#/components/schemas/CreateSeriesRequest" } } } }, "responses": { "200": { - "description": "The booking.", + "description": "Each occurrence.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -81173,7 +83154,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingBooking" + "$ref": "#/components/schemas/SchedulingSeriesPreview" } } } @@ -81220,6 +83201,27 @@ } } }, + "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": { @@ -81284,7 +83286,7 @@ } }, "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 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.", @@ -81373,17 +83375,22 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/scheduling/holds": { + "/v1/scheduling/series/{id}/cancel": { "post": { - "operationId": "createSchedulingHold", + "operationId": "cancelSchedulingSeries", "tags": [ - "Scheduling bookings" + "Scheduling series" ], - "summary": "Hold a slot", - "description": "Keeps a slot for a while (the event type's `hold_ttl_seconds`) so you can take payment, then confirm it. Answers with a `held` booking, which isn't charged and sends nothing.", + "summary": "Cancel a recurring series", + "description": "Cancels the occurrences from `from_index` on, or all of them.", "security": [ { "bearerAuth": [] @@ -81393,17 +83400,15 @@ { "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.", + "description": "The series' id.", "examples": [ - "welcome-user-123" + "series_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, - "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" + "required": true, + "description": "The series' id.", + "name": "id", + "in": "path" }, { "$ref": "#/components/parameters/CossVersion" @@ -81414,14 +83419,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateHoldRequest" + "$ref": "#/components/schemas/CancelSeriesRequest" } } } }, "responses": { - "201": { - "description": "The held booking.", + "200": { + "description": "The cancelled bookings.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -81432,15 +83437,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": { @@ -81454,7 +83450,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingBooking" + "$ref": "#/components/schemas/SchedulingCancelledSeries" } } } @@ -81501,27 +83497,6 @@ } } }, - "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": { @@ -81564,27 +83539,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": { @@ -81606,27 +83560,6 @@ } } }, - "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`).", - "headers": { - "X-Request-Id": { - "description": "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": { @@ -81699,14 +83632,14 @@ } } }, - "/v1/scheduling/holds/{id}/confirm": { - "post": { - "operationId": "confirmSchedulingHold", + "/v1/scheduling/attendees/{uid}": { + "delete": { + "operationId": "eraseSchedulingAttendee", "tags": [ "Scheduling bookings" ], - "summary": "Confirm a hold", - "description": "Turns a hold into a booking. Always succeeds before the hold expires. Repeating it is safe.", + "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": [] @@ -81716,33 +83649,23 @@ { "schema": { "type": "string", - "description": "The booking's id.", + "description": "Your id for the attendee.", "examples": [ - "book_01kd2m9a0b1c2d3e4f5g6h7j8m" + "patient_77" ] }, "required": true, - "description": "The booking's 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/ConfirmHoldRequest" - } - } - } - }, "responses": { "200": { - "description": "The booking.", + "description": "How many were erased.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -81766,13 +83689,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingBooking" + "$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.", @@ -81834,69 +83757,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" - } - } - } - }, - "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": { @@ -81969,40 +83829,32 @@ } } }, - "/v1/scheduling/holds/{id}": { - "delete": { - "operationId": "releaseSchedulingHold", + "/v1/scheduling/attendees/erase": { + "post": { + "operationId": "eraseSchedulingAttendeeByEmail", "tags": [ "Scheduling bookings" ], - "summary": "Release a hold", - "description": "Gives the slot back early.", + "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": "The booking's id.", - "examples": [ - "book_01kd2m9a0b1c2d3e4f5g6h7j8m" - ] - }, - "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/EraseSchedulingAttendeeByEmailRequest" + } + } } - ], + }, "responses": { "200": { - "description": "The cancelled booking.", + "description": "How many were erased.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -82026,13 +83878,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingBooking" + "$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.", @@ -82094,29 +83946,8 @@ } } }, - "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" - } - } - } - }, - "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`).", + "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.", @@ -82205,17 +84036,22 @@ } } } - } + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] } }, - "/v1/scheduling/series": { + "/v1/scheduling/hosts/{host}/calendar_connections/authorize": { "post": { - "operationId": "createSchedulingSeries", + "operationId": "authorizeSchedulingCalendar", "tags": [ - "Scheduling series" + "Scheduling calendars" ], - "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": "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": [] @@ -82225,17 +84061,15 @@ { "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.", + "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 booking instead of creating another, for 24 hours. A refused request keeps no key. 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" @@ -82246,14 +84080,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateSeriesRequest" + "$ref": "#/components/schemas/AuthorizeCalendarRequest" } } } }, "responses": { - "201": { - "description": "The series and its bookings.", + "200": { + "description": "Where to send the host.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -82264,15 +84098,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": { @@ -82286,7 +84111,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingSeries" + "$ref": "#/components/schemas/SchedulingCalendarAuthorization" } } } @@ -82333,27 +84158,6 @@ } } }, - "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": { @@ -82396,27 +84200,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": { @@ -82439,7 +84222,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 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.", @@ -82531,32 +84314,50 @@ } } }, - "/v1/scheduling/series/preview": { + "/v1/scheduling/hosts/{host}/calendar_connections": { "post": { - "operationId": "previewSchedulingSeries", + "operationId": "createSchedulingCalendarConnection", "tags": [ - "Scheduling series" + "Scheduling calendars" ], - "summary": "Preview a recurring series", - "description": "Which occurrences would be free, without booking anything.", + "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": [] } ], + "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/CreateSeriesRequest" + "$ref": "#/components/schemas/CreateCalendarConnectionRequest" } } } }, "responses": { - "200": { - "description": "Each occurrence.", + "201": { + "description": "The connection.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -82580,7 +84381,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingSeriesPreview" + "$ref": "#/components/schemas/SchedulingCalendarConnection" } } } @@ -82627,27 +84428,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": { @@ -82712,7 +84492,7 @@ } }, "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`).", + "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.", @@ -82801,22 +84581,15 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - } - }, - "/v1/scheduling/series/{id}/cancel": { - "post": { - "operationId": "cancelSchedulingSeries", + } + }, + "get": { + "operationId": "listSchedulingCalendarConnections", "tags": [ - "Scheduling series" + "Scheduling calendars" ], - "summary": "Cancel a recurring series", - "description": "Cancels the occurrences from `from_index` on, or all of them.", + "summary": "List a host's calendar connections", + "description": "With each connection's calendars and which of them count.", "security": [ { "bearerAuth": [] @@ -82826,33 +84599,23 @@ { "schema": { "type": "string", - "description": "The series' id.", + "description": "The host's id or uid.", "examples": [ - "series_01kd2m9a0b1c2d3e4f5g6h7j8m" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The series' id.", - "name": "id", + "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/CancelSeriesRequest" - } - } - } - }, "responses": { "200": { - "description": "The cancelled bookings.", + "description": "The connections.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -82876,13 +84639,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCancelledSeries" + "$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.", @@ -82965,27 +84728,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" - } - } - } - }, "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": { @@ -83058,14 +84800,14 @@ } } }, - "/v1/scheduling/attendees/{uid}": { + "/v1/scheduling/hosts/{host}/calendar_connections/{id}": { "delete": { - "operationId": "eraseSchedulingAttendee", + "operationId": "deleteSchedulingCalendarConnection", "tags": [ - "Scheduling bookings" + "Scheduling calendars" ], - "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": "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": [] @@ -83075,14 +84817,27 @@ { "schema": { "type": "string", - "description": "Your id for the attendee.", + "description": "The host's id or uid.", "examples": [ - "patient_77" + "host_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "Your id for the attendee.", - "name": "uid", + "description": "The host's id or uid.", + "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" }, { @@ -83091,7 +84846,7 @@ ], "responses": { "200": { - "description": "How many were erased.", + "description": "Disconnected.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -83115,7 +84870,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingAttendeeErasure" + "$ref": "#/components/schemas/DeletedSchedulingCalendarConnection" } } } @@ -83183,6 +84938,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" + } + } + } + }, + "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": { @@ -83255,32 +85052,63 @@ } } }, - "/v1/scheduling/attendees/erase": { - "post": { - "operationId": "eraseSchedulingAttendeeByEmail", + "/v1/scheduling/hosts/{host}/calendars/{calendar_id}": { + "patch": { + "operationId": "updateSchedulingCalendar", "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": "Choose how a calendar counts", + "description": "Whether its busy times block slots, and whether bookings are written to it.", "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/EraseSchedulingAttendeeByEmailRequest" + "$ref": "#/components/schemas/UpdateCalendarRequest" } } } }, "responses": { "200": { - "description": "How many were erased.", + "description": "The host's connections.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -83304,7 +85132,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingAttendeeErasureByEmail" + "$ref": "#/components/schemas/SchedulingCalendarConnectionList" } } } @@ -83372,6 +85200,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": { @@ -83462,58 +85311,35 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] + } } }, - "/v1/scheduling/hosts/{host}/calendar_connections/authorize": { + "/v1/scheduling/calendar_delegations": { "post": { - "operationId": "authorizeSchedulingCalendar", + "operationId": "createSchedulingCalendarDelegation", "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": "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" - }, - { - "$ref": "#/components/parameters/CossVersion" - } - ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AuthorizeCalendarRequest" + "$ref": "#/components/schemas/CreateCalendarDelegationRequest" } } } }, "responses": { - "200": { - "description": "Where to send the host.", + "201": { + "description": "The delegation.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -83537,7 +85363,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarAuthorization" + "$ref": "#/components/schemas/SchedulingCalendarDelegation" } } } @@ -83584,8 +85410,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.", @@ -83605,8 +85431,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.", @@ -83648,7 +85474,7 @@ } }, "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`).", + "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.", @@ -83737,53 +85563,28 @@ } } } - } - } - }, - "/v1/scheduling/hosts/{host}/calendar_connections": { - "post": { - "operationId": "createSchedulingCalendarConnection", + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + }, + "get": { + "operationId": "listSchedulingCalendarDelegations", "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 calendar delegations", + "description": "With how many hosts each has connected.", "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/CreateCalendarConnectionRequest" - } - } - } - }, "responses": { - "201": { - "description": "The connection.", + "200": { + "description": "The delegations.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -83807,13 +85608,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarConnection" + "$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.", @@ -83875,69 +85676,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" - } - } - } - }, - "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": { @@ -84007,15 +85745,22 @@ } } } - } - }, + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + } + }, + "/v1/scheduling/calendar_delegations/{id}": { "get": { - "operationId": "listSchedulingCalendarConnections", + "operationId": "getSchedulingCalendarDelegation", "tags": [ "Scheduling calendars" ], - "summary": "List a host's calendar connections", - "description": "With each connection's calendars and which of them count.", + "summary": "Get a calendar delegation", + "description": "Its status, what to authorize, and how many hosts it has connected.", "security": [ { "bearerAuth": [] @@ -84025,14 +85770,14 @@ { "schema": { "type": "string", - "description": "The host's id or uid.", + "description": "The delegation's id.", "examples": [ - "host_01kd2m9a0b1c2d3e4f5g6h7j8m" + "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The host's id or uid.", - "name": "host", + "description": "The delegation's id.", + "name": "id", "in": "path" }, { @@ -84041,7 +85786,7 @@ ], "responses": { "200": { - "description": "The connections.", + "description": "The delegation.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -84065,7 +85810,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarConnectionList" + "$ref": "#/components/schemas/SchedulingCalendarDelegation" } } } @@ -84224,16 +85969,14 @@ } } } - } - }, - "/v1/scheduling/hosts/{host}/calendar_connections/{id}": { - "delete": { - "operationId": "deleteSchedulingCalendarConnection", + }, + "patch": { + "operationId": "updateSchedulingCalendarDelegation", "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": "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": [] @@ -84243,26 +85986,13 @@ { "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 connection's id.", + "description": "The delegation's id.", "examples": [ - "calconn_01kd2m9a0b1c2d3e4f5g6h7j8m" + "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The connection's id.", + "description": "The delegation's id.", "name": "id", "in": "path" }, @@ -84270,9 +86000,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCalendarDelegationRequest" + } + } + } + }, "responses": { "200": { - "description": "Disconnected.", + "description": "The delegation.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -84296,13 +86036,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSchedulingCalendarConnection" + "$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.", @@ -84343,6 +86083,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": { @@ -84385,8 +86146,29 @@ } } }, + "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`.", + "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.", @@ -84476,16 +86258,14 @@ } } } - } - }, - "/v1/scheduling/hosts/{host}/calendars/{calendar_id}": { - "patch": { - "operationId": "updateSchedulingCalendar", + }, + "delete": { + "operationId": "deleteSchedulingCalendarDelegation", "tags": [ "Scheduling calendars" ], - "summary": "Choose how a calendar counts", - "description": "Whether its busy times block slots, and whether bookings are written to it.", + "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": [] @@ -84495,46 +86275,23 @@ { "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.", + "description": "The delegation's id.", "examples": [ - "cal_01kd2m9a0b1c2d3e4f5g6h7j8m" + "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The calendar's id.", - "name": "calendar_id", + "description": "The delegation's id.", + "name": "id", "in": "path" }, { "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateCalendarRequest" - } - } - } - }, "responses": { "200": { - "description": "The host's connections.", + "description": "Being removed.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -84558,13 +86315,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarConnectionList" + "$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.", @@ -84647,27 +86404,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" - } - } - } - }, "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": { @@ -84740,31 +86476,49 @@ } } }, - "/v1/scheduling/calendar_delegations": { + "/v1/scheduling/calendar_delegations/{id}/verify": { "post": { - "operationId": "createSchedulingCalendarDelegation", + "operationId": "verifySchedulingCalendarDelegation", "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": "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": [] } ], + "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" + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateCalendarDelegationRequest" + "$ref": "#/components/schemas/VerifyCalendarDelegationRequest" } } } }, "responses": { - "201": { + "200": { "description": "The delegation.", "headers": { "X-Request-Id": { @@ -84878,6 +86632,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": { @@ -84989,28 +86764,35 @@ } } } - }, - "parameters": [ - { - "$ref": "#/components/parameters/CossVersion" - } - ] - }, - "get": { - "operationId": "listSchedulingCalendarDelegations", + } + } + }, + "/v1/scheduling/oauth_apps": { + "post": { + "operationId": "createSchedulingOauthApp", "tags": [ "Scheduling calendars" ], - "summary": "List calendar delegations", - "description": "With how many hosts each has connected.", + "summary": "Add your own OAuth app", + "description": "New consents in this environment use it, so the consent screen shows your brand. Connections made before it stay on coss.com's app until their host consents again. On the Scale plan and above. Secrets are never returned.", "security": [ { "bearerAuth": [] } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOauthAppRequest" + } + } + } + }, "responses": { - "200": { - "description": "The delegations.", + "201": { + "description": "The app.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -85034,13 +86816,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarDelegationList" + "$ref": "#/components/schemas/SchedulingOauthApp" } } } }, "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.", @@ -85081,6 +86863,27 @@ } } }, + "402": { + "description": "The plan doesn't include this (`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": { @@ -85102,6 +86905,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 app isn't valid (`invalid_oauth_app`), or the provider refused its client ID and secret (`oauth_app_refused`), and nothing was stored.", + "headers": { + "X-Request-Id": { + "description": "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": { @@ -85177,42 +87022,22 @@ "$ref": "#/components/parameters/CossVersion" } ] - } - }, - "/v1/scheduling/calendar_delegations/{id}": { + }, "get": { - "operationId": "getSchedulingCalendarDelegation", + "operationId": "listSchedulingOauthApps", "tags": [ "Scheduling calendars" ], - "summary": "Get a calendar delegation", - "description": "Its status, what to authorize, and how many hosts it has connected.", + "summary": "List your OAuth apps", + "description": "At most one per provider, with how many connections use each.", "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 delegation.", + "description": "The apps.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -85236,7 +87061,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarDelegation" + "$ref": "#/components/schemas/SchedulingOauthAppList" } } } @@ -85304,27 +87129,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" - } - } - } - }, "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": { @@ -85394,15 +87198,22 @@ } } } - } - }, - "patch": { - "operationId": "updateSchedulingCalendarDelegation", + }, + "parameters": [ + { + "$ref": "#/components/parameters/CossVersion" + } + ] + } + }, + "/v1/scheduling/oauth_apps/{id}": { + "get": { + "operationId": "getSchedulingOauthApp", "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": "Get an OAuth app", + "description": "Its status, what to register at the provider, and how many connections use it.", "security": [ { "bearerAuth": [] @@ -85412,13 +87223,13 @@ { "schema": { "type": "string", - "description": "The delegation's id.", + "description": "The OAuth app's id.", "examples": [ - "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m" + "calapp_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The delegation's id.", + "description": "The OAuth app's id.", "name": "id", "in": "path" }, @@ -85426,19 +87237,9 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateCalendarDelegationRequest" - } - } - } - }, "responses": { "200": { - "description": "The delegation.", + "description": "The app.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -85462,13 +87263,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarDelegation" + "$ref": "#/components/schemas/SchedulingOauthApp" } } } }, "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.", @@ -85509,27 +87310,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": { @@ -85572,48 +87352,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": { @@ -85685,13 +87423,13 @@ } } }, - "delete": { - "operationId": "deleteSchedulingCalendarDelegation", + "patch": { + "operationId": "updateSchedulingOauthApp", "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": "Update an OAuth app", + "description": "A new client secret is tried on a connection before it replaces the old one, and ends an outage if it works. Its client ID can't change: a different app needs its hosts to consent again.", "security": [ { "bearerAuth": [] @@ -85701,13 +87439,13 @@ { "schema": { "type": "string", - "description": "The delegation's id.", + "description": "The OAuth app's id.", "examples": [ - "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m" + "calapp_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The delegation's id.", + "description": "The OAuth app's id.", "name": "id", "in": "path" }, @@ -85715,9 +87453,19 @@ "$ref": "#/components/parameters/CossVersion" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOauthAppRequest" + } + } + } + }, "responses": { "200": { - "description": "Being removed.", + "description": "The app.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -85741,13 +87489,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeletedSchedulingCalendarDelegation" + "$ref": "#/components/schemas/SchedulingOauthApp" } } } }, "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.", @@ -85830,6 +87578,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 app isn't valid (`invalid_oauth_app`), or the provider refused its client ID and secret (`oauth_app_refused`), and nothing was stored.", + "headers": { + "X-Request-Id": { + "description": "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": { @@ -85900,16 +87690,14 @@ } } } - } - }, - "/v1/scheduling/calendar_delegations/{id}/verify": { - "post": { - "operationId": "verifySchedulingCalendarDelegation", + }, + "delete": { + "operationId": "deleteSchedulingOauthApp", "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": "Delete an OAuth app", + "description": "Refused while connections use it (`oauth_app_in_use`), since only it can refresh their access: disconnect them first. New consents go back to coss.com's app.", "security": [ { "bearerAuth": [] @@ -85919,13 +87707,13 @@ { "schema": { "type": "string", - "description": "The delegation's id.", + "description": "The OAuth app's id.", "examples": [ - "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m" + "calapp_01kd2m9a0b1c2d3e4f5g6h7j8m" ] }, "required": true, - "description": "The delegation's id.", + "description": "The OAuth app's id.", "name": "id", "in": "path" }, @@ -85933,19 +87721,9 @@ "$ref": "#/components/parameters/CossVersion" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyCalendarDelegationRequest" - } - } - } - }, "responses": { "200": { - "description": "The delegation.", + "description": "Deleted.", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", @@ -85969,13 +87747,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchedulingCalendarDelegation" + "$ref": "#/components/schemas/DeletedSchedulingOauthApp" } } } }, "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.", @@ -86016,27 +87794,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": { @@ -86079,29 +87836,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 credentials or mailbox aren't valid (`invalid_calendar_delegation`), or the provider refused them (`calendar_delegation_refused`, and `message` says what to fix).", + "description": "Connections still use it (`oauth_app_in_use`).", "headers": { "X-Request-Id": { "description": "Unique id of this request. Include it when contacting support.", diff --git a/apps/api/src/middleware/request-log.ts b/apps/api/src/middleware/request-log.ts index 0bdd4b584..7ba736b8d 100644 --- a/apps/api/src/middleware/request-log.ts +++ b/apps/api/src/middleware/request-log.ts @@ -50,10 +50,11 @@ const CONTENT_RESPONSES = ["/v1/received_emails", "/v1/inboxes", "/v1/sms/receiv const FORM_SUBMISSIONS = /^\/v1\/forms\/[^/]+\/submissions(\/|$)/; /** * Scheduling's attendee details and answers: these requests carry them both ways, always. A calendar - * delegation's requests carry a service account's private key or an app's client secret. + * delegation's requests carry a service account's private key or an app's client secret, and so do + * a customer's own OAuth apps. */ const SCHEDULING_PERSONAL = - /^\/v1\/scheduling\/((bookings|holds|series|attendees|booker_sessions|calendar_delegations|out_of_office|reassignments|holiday_calendars)(\/|$)|hosts\/[^/]+\/calendar_connections)/; + /^\/v1\/scheduling\/((bookings|holds|series|attendees|booker_sessions|calendar_delegations|oauth_apps|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 diff --git a/apps/api/src/routes/scheduling-callbacks.ts b/apps/api/src/routes/scheduling-callbacks.ts index 87c824936..91c34a0cc 100644 --- a/apps/api/src/routes/scheduling-callbacks.ts +++ b/apps/api/src/routes/scheduling-callbacks.ts @@ -9,10 +9,12 @@ import { completeCalendarAuthorization, handleCalendarPush, handleZoomDeauthorization, + oauthAppCredentials, verifyZoomEvent, + zoomOauthAppForEvents, zoomUrlValidation, } from "@coss/scheduling"; -import { Hono } from "hono"; +import { type Context, Hono } from "hono"; import type { AppEnv } from "../types.ts"; export const SCHEDULING_CALLBACK_PATH = "/scheduling"; @@ -67,9 +69,8 @@ export function createSchedulingCallbackRoutes(opts: { calendars: CalendarDeps } return c.text(PAGE_TEXT[outcome], outcome === "connected" ? 200 : 400); }); - router.post("/zoom/events", async (c) => { - const secret = opts.calendars.apps.zoom?.webhookSecret; - if (!secret) return c.body(null, 404); + // Whichever app sent it, it only ever reaches that app's connections. + const zoomEvent = async (c: Context, secret: string, oauthAppId: string | null) => { const body = await c.req.text(); const verified = verifyZoomEvent({ secret, @@ -87,9 +88,23 @@ export function createSchedulingCallbackRoutes(opts: { calendars: CalendarDeps } return c.json(zoomUrlValidation(event.payload.plainToken, secret)); } if (event.event === "app_deauthorized" && event.payload?.user_id) { - await handleZoomDeauthorization(event.payload.user_id, opts.calendars); + await handleZoomDeauthorization(event.payload.user_id, opts.calendars, oauthAppId); } return c.body(null, 200); + }; + + router.post("/zoom/events", async (c) => { + const secret = opts.calendars.apps.zoom?.webhookSecret; + if (!secret) return c.body(null, 404); + return zoomEvent(c, secret, null); + }); + + // A customer's own Zoom app, with its own Secret Token: never coss.com's, or anyone else's. + router.post("/zoom/events/:appId", async (c) => { + const app = await zoomOauthAppForEvents(c.req.param("appId")); + const secret = app && oauthAppCredentials(app, opts.calendars.secretsKeys).webhookSecret; + if (!app || !secret) return c.body(null, 404); + return zoomEvent(c, secret, app.id); }); router.post("/push/google", async (c) => { diff --git a/apps/api/src/routes/scheduling-client.ts b/apps/api/src/routes/scheduling-client.ts index d37f67ccc..ab3b67511 100644 --- a/apps/api/src/routes/scheduling-client.ts +++ b/apps/api/src/routes/scheduling-client.ts @@ -32,6 +32,7 @@ import { getOutOfOffice, getReassignment, getSettingsRow, + hasOauthApp, listConnections, listEventTypeSlots, listManagedBookings, @@ -65,6 +66,7 @@ import { updateHost, updateOutOfOffice, updateSchedule, + zoomConnectable, } from "@coss/scheduling"; import { AuthorizeCalendarRequest, @@ -366,13 +368,19 @@ export function createSchedulingClientRoutes(opts: { }); } if (session.kind === "host") { - const host = await getHost(env, session.hostId as string); - const settings = await getSettingsRow(env); + const [host, zoomAvailable, ownOauthApp, settings] = await Promise.all([ + getHost(env, session.hostId as string), + deps.calendars ? zoomConnectable(env, deps.calendars) : false, + hasOauthApp(env), + 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), + zoom_available: zoomAvailable, can_reassign: settings?.hostReassignment === "eligible", + own_oauth_app: ownOauthApp, + privacy_url: settings?.privacyUrl ?? null, }); } return c.json(base); diff --git a/apps/api/src/routes/scheduling.ts b/apps/api/src/routes/scheduling.ts index 3a2a85e71..f1de5ef32 100644 --- a/apps/api/src/routes/scheduling.ts +++ b/apps/api/src/routes/scheduling.ts @@ -24,6 +24,7 @@ import { createHostSession, createManagerSession, createManageSession, + createOauthApp, createOutOfOffice, createReassignment, createSchedule, @@ -33,6 +34,7 @@ import { deleteHold, deleteHolidayCalendar, deleteHost, + deleteOauthApp, deleteOutOfOffice, deleteSchedule, disconnectCalendar, @@ -44,6 +46,7 @@ import { getEventType, getHolidayCalendar, getHost, + getOauthApp, getOutOfOffice, getReassignment, getSchedule, @@ -59,6 +62,7 @@ import { listHolidayCalendars, listHolidays, listHosts, + listOauthApps, listOutOfOffice, listOutOfOfficeConflicts, listReassignCandidates, @@ -86,6 +90,7 @@ import { updateEventType, updateHolidayCalendar, updateHost, + updateOauthApp, updateOutOfOffice, updateSchedule, updateSettings, @@ -116,6 +121,7 @@ import { CreateHoldRequest, CreateHolidayCalendarRequest, CreateHostRequest, + CreateOauthAppRequest, CreateOutOfOfficeRequest, CreateReassignmentRequest, CreateSeriesRequest, @@ -123,6 +129,7 @@ import { DeletedCalendarDelegation, DeletedHolidayCalendar, DeletedHost, + DeletedOauthApp, DeletedOutOfOffice, DeletedSchedule, EraseAttendeeByEmailRequest, @@ -139,6 +146,8 @@ import { ListBookingEventsQuery, ListBookingsQuery, ListOutOfOfficeQuery, + OauthAppList, + OauthAppObject, OutOfOfficeConflictList, OutOfOfficeList, OutOfOfficeObject, @@ -169,6 +178,7 @@ import { UpdateEventTypeRequest, UpdateHolidayCalendarRequest, UpdateHostRequest, + UpdateOauthAppRequest, UpdateOutOfOfficeRequest, UpdateSettingsRequest, UpsertScheduleRequest, @@ -255,6 +265,9 @@ const CalendarParams = HostParams.extend({ const DelegationParams = z.object({ id: param("The delegation's id.", "caldlg_01kd2m9a0b1c2d3e4f5g6h7j8m"), }); +const OauthAppParams = z.object({ + id: param("The OAuth app's id.", "calapp_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"), @@ -1329,6 +1342,117 @@ export const deleteCalendarDelegationRoute = createRoute({ }, }); +const refusedOauthApp = errorDoc( + "The app isn't valid (`invalid_oauth_app`), or the provider refused its client ID and secret " + + "(`oauth_app_refused`), and nothing was stored.", +); + +export const createOauthAppRoute = createRoute({ + method: "post", + path: "/scheduling/oauth_apps", + operationId: "createSchedulingOauthApp", + tags: [TAG_CALENDARS], + summary: "Add your own OAuth app", + description: + "New consents in this environment use it, so the consent screen shows your brand. Connections " + + "made before it stay on coss.com's app until their host consents again. On the Scale plan and " + + "above. Secrets are never returned.", + security: [{ bearerAuth: [] }], + middleware: writing("update", "POST /v1/scheduling/oauth_apps"), + request: { + body: { + required: true, + content: { "application/json": { schema: CreateOauthAppRequest } }, + }, + }, + responses: { + 201: jsonDoc("The app.", OauthAppObject), + ...commonErrors, + ...bodyErrors, + 402: errorDoc("The plan doesn't include this (`plan_feature_unavailable`)."), + 422: refusedOauthApp, + }, +}); + +export const listOauthAppsRoute = createRoute({ + method: "get", + path: "/scheduling/oauth_apps", + operationId: "listSchedulingOauthApps", + tags: [TAG_CALENDARS], + summary: "List your OAuth apps", + description: "At most one per provider, with how many connections use each.", + security: [{ bearerAuth: [] }], + middleware: read(), + responses: { + 200: jsonDoc("The apps.", OauthAppList), + ...commonErrors, + }, +}); + +export const getOauthAppRoute = createRoute({ + method: "get", + path: "/scheduling/oauth_apps/{id}", + operationId: "getSchedulingOauthApp", + tags: [TAG_CALENDARS], + summary: "Get an OAuth app", + description: "Its status, what to register at the provider, and how many connections use it.", + security: [{ bearerAuth: [] }], + middleware: read(), + request: { params: OauthAppParams }, + responses: { + 200: jsonDoc("The app.", OauthAppObject), + ...commonErrors, + 404: notFound, + }, +}); + +export const updateOauthAppRoute = createRoute({ + method: "patch", + path: "/scheduling/oauth_apps/{id}", + operationId: "updateSchedulingOauthApp", + tags: [TAG_CALENDARS], + summary: "Update an OAuth app", + description: + "A new client secret is tried on a connection before it replaces the old one, and ends an outage " + + "if it works. Its client ID can't change: a different app needs its hosts to consent again.", + security: [{ bearerAuth: [] }], + middleware: writing("update", "PATCH /v1/scheduling/oauth_apps/{id}"), + request: { + params: OauthAppParams, + body: { + required: true, + content: { "application/json": { schema: UpdateOauthAppRequest } }, + }, + }, + responses: { + 200: jsonDoc("The app.", OauthAppObject), + ...commonErrors, + ...bodyErrors, + 404: notFound, + 422: refusedOauthApp, + }, +}); + +export const deleteOauthAppRoute = createRoute({ + method: "delete", + path: "/scheduling/oauth_apps/{id}", + operationId: "deleteSchedulingOauthApp", + tags: [TAG_CALENDARS], + summary: "Delete an OAuth app", + description: + "Refused while connections use it (`oauth_app_in_use`), since only it can refresh their access: " + + "disconnect them first. New consents go back to coss.com's app.", + security: [{ bearerAuth: [] }], + middleware: acting("update"), + request: { params: OauthAppParams }, + responses: { + 200: jsonDoc("Deleted.", DeletedOauthApp), + ...commonErrors, + 404: notFound, + 422: errorDoc("Connections still use it (`oauth_app_in_use`)."), + }, +}); + export const setSimulatedBusyRoute = createRoute({ method: "put", path: "/scheduling/hosts/{host}/simulated_busy", @@ -2910,6 +3034,50 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA ); }); + router.openapi(createOauthAppRoute, async (c) => { + const app = await createOauthApp(c.var.auth, c.req.valid("json"), { + ...calendars(), + plans: c.var.plans ?? undefined, + }); + await recordApiAudit(c, { + action: "scheduling_oauth_app.created", + target: { type: "scheduling_oauth_app", id: app.id, label: app.provider }, + }); + return c.json(app, 201); + }); + + router.openapi(listOauthAppsRoute, async (c) => { + const data = await listOauthApps(c.var.auth, calendars()); + return c.json({ object: "list" as const, data, has_more: false }, 200); + }); + + router.openapi(getOauthAppRoute, async (c) => + c.json(await getOauthApp(c.var.auth, c.req.valid("param").id, calendars()), 200), + ); + + router.openapi(updateOauthAppRoute, async (c) => { + const app = await updateOauthApp( + c.var.auth, + c.req.valid("param").id, + c.req.valid("json"), + calendars(), + ); + await recordApiAudit(c, { + action: "scheduling_oauth_app.updated", + target: { type: "scheduling_oauth_app", id: app.id, label: app.provider }, + }); + return c.json(app, 200); + }); + + router.openapi(deleteOauthAppRoute, async (c) => { + const { id } = await deleteOauthApp(c.var.auth, c.req.valid("param").id); + await recordApiAudit(c, { + action: "scheduling_oauth_app.deleted", + target: { type: "scheduling_oauth_app", id }, + }); + return c.json({ object: "scheduling.oauth_app" as const, id, deleted: true as const }, 200); + }); + router.openapi(setSimulatedBusyRoute, async (c) => { await setSimulatedBusy( c.var.auth, diff --git a/apps/api/src/webhook-events.ts b/apps/api/src/webhook-events.ts index a5291a830..82fb35420 100644 --- a/apps/api/src/webhook-events.ts +++ b/apps/api/src/webhook-events.ts @@ -14,6 +14,7 @@ import { SchedulingBookingWebhookEvent, SchedulingCalendarDelegationWebhookEvent, SchedulingCalendarWebhookEvent, + SchedulingOauthAppWebhookEvent, SchedulingOutOfOfficeWebhookEvent, SchedulingReassignmentWebhookEvent, } from "@coss/scheduling/schemas"; @@ -47,6 +48,7 @@ const WEBHOOK_EVENTS = { SchedulingCalendarWebhookEvent, SchedulingOutOfOfficeWebhookEvent, SchedulingReassignmentWebhookEvent, + SchedulingOauthAppWebhookEvent, }; 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 548fa9294..7f910f27c 100644 --- a/apps/api/test/agent-surface-exemptions.ts +++ b/apps/api/test/agent-surface-exemptions.ts @@ -20,6 +20,9 @@ const SECRET = "A signing secret is shown once, when it's created: never handed const DELEGATION = "Takes a Workspace or tenant's credentials, reaching every mailbox on a domain: set up by its admin, not an agent."; +const OAUTH_APP = + "Takes an OAuth app's client secret, set up by whoever owns that app at the provider, not an agent."; + export const AGENT_SURFACE_EXEMPTIONS: Readonly> = { "POST /v1/emails/batch": SENDS, "GET /v1/emails/batch/{id}": READ, @@ -72,6 +75,11 @@ export const AGENT_SURFACE_EXEMPTIONS: Readonly> = { "PATCH /v1/scheduling/calendar_delegations/{id}": DELEGATION, "POST /v1/scheduling/calendar_delegations/{id}/verify": DELEGATION, "DELETE /v1/scheduling/calendar_delegations/{id}": REMOVES, + "POST /v1/scheduling/oauth_apps": OAUTH_APP, + "GET /v1/scheduling/oauth_apps": READ, + "GET /v1/scheduling/oauth_apps/{id}": READ, + "PATCH /v1/scheduling/oauth_apps/{id}": OAUTH_APP, + "DELETE /v1/scheduling/oauth_apps/{id}": REMOVES, "POST /v1/scheduling/hosts/{host}/calendar_connections": CHANGE, "DELETE /v1/scheduling/hosts/{host}/calendar_connections/{id}": REMOVES, "PATCH /v1/scheduling/hosts/{host}/calendars/{calendar_id}": CHANGE, diff --git a/apps/api/test/app.test.ts b/apps/api/test/app.test.ts index ad0bfb347..b86c95de0 100644 --- a/apps/api/test/app.test.ts +++ b/apps/api/test/app.test.ts @@ -319,6 +319,8 @@ describe("GET /openapi.json", () => { "/v1/scheduling/hosts/{host}/sessions", "/v1/scheduling/hosts/{host}/simulated_busy", "/v1/scheduling/manager_sessions", + "/v1/scheduling/oauth_apps", + "/v1/scheduling/oauth_apps/{id}", "/v1/scheduling/out_of_office", "/v1/scheduling/out_of_office/by_external_id/{external_id}", "/v1/scheduling/out_of_office/{id}", diff --git a/apps/api/test/contract-types.ts b/apps/api/test/contract-types.ts index 705500861..49e370d1d 100644 --- a/apps/api/test/contract-types.ts +++ b/apps/api/test/contract-types.ts @@ -232,6 +232,7 @@ import type { CreateHoldRequest, CreateHolidayCalendarRequest, CreateHostRequest, + CreateOauthAppRequest, CreateOutOfOfficeRequest, CreateReassignmentRequest, CreateSeriesRequest, @@ -243,6 +244,7 @@ import type { HolidayCalendarObject, HostObject, ListOutOfOfficeQuery, + OauthAppObject, OutOfOfficeConflict, OutOfOfficeObject, PreviewReassignmentRequest, @@ -257,6 +259,7 @@ import type { SchedulingBookingWebhookEvent, SchedulingCalendarDelegationWebhookEvent, SchedulingCalendarWebhookEvent, + SchedulingOauthAppWebhookEvent, SchedulingOutOfOfficeWebhookEvent, SchedulingReassignmentWebhookEvent, SeriesPreview, @@ -273,6 +276,7 @@ import type { UpdateEventTypeRequest, UpdateHolidayCalendarRequest, UpdateHostRequest, + UpdateOauthAppRequest, UpdateOutOfOfficeRequest, UpdateSettingsRequest, UpsertScheduleRequest, @@ -340,6 +344,7 @@ import type { CreateSchedulingHoldParams, CreateSchedulingHolidayCalendarParams, CreateSchedulingHostParams, + CreateSchedulingOauthAppParams, CreateSchedulingOutOfOfficeParams, CreateSchedulingReassignmentParams, CreateSchedulingSeriesParams, @@ -411,6 +416,7 @@ import type { SchedulingEventType, SchedulingHolidayCalendar, SchedulingHost, + SchedulingOauthApp, SchedulingOutOfOffice, SchedulingOutOfOfficeConflict, SchedulingReassignCandidate, @@ -554,6 +560,7 @@ import type { UpdateSchedulingEventTypeParams, UpdateSchedulingHolidayCalendarParams, UpdateSchedulingHostParams, + UpdateSchedulingOauthAppParams, UpdateSchedulingOutOfOfficeParams, UpdateSchedulingSettingsParams, UpdateSegmentParams, @@ -818,7 +825,8 @@ export type Contract = [ | z.output | z.output | z.output - | z.output, + | z.output + | z.output, WebhookEvent > >, @@ -1272,6 +1280,9 @@ export type SubaccountContract = [ Assert< MutuallyAssignable, SchedulingCalendarDelegation> >, + Assert, CreateSchedulingOauthAppParams>>, + Assert, UpdateSchedulingOauthAppParams>>, + Assert, SchedulingOauthApp>>, Assert< MutuallyAssignable, SetSchedulingSimulatedBusyParams> >, diff --git a/apps/api/test/scheduling-oauth-apps.test.ts b/apps/api/test/scheduling-oauth-apps.test.ts new file mode 100644 index 000000000..1945aba7c --- /dev/null +++ b/apps/api/test/scheduling-oauth-apps.test.ts @@ -0,0 +1,415 @@ +import { describe, expect, test } from "bun:test"; +import { createHash, createHmac, randomUUID } from "node:crypto"; +import { createPlanGate } from "@coss/billing"; +import { createTestContract } from "@coss/billing/testing"; +import { + type CalendarDeps, + completeCalendarAuthorization, + createHost, + listConnections, + type SchedulingEnvironment, + startCalendarAuthorization, +} from "@coss/scheduling"; +import { FakeGoogleCalendarApi, FakeZoomApi, testSchedulingDeps } from "@coss/scheduling/testing"; +import { createTestTenant } from "@coss/tenancy/testing"; +import { secretsKeyRing } from "@coss/webhooks"; +import { createTestApp, errorOf } from "./helpers.ts"; + +const keys = secretsKeyRing([createHash("sha256").update("api oauth apps").digest()]); + +function calendars(fetcher: NonNullable): CalendarDeps { + return { + secretsKeys: keys, + apps: { + google: { clientId: "coss-google", clientSecret: "coss-secret" }, + microsoft: null, + zoom: { clientId: "coss-zoom", clientSecret: "coss-zs", webhookSecret: "coss-token" }, + }, + publicUrl: "https://api.example.test", + fetcher, + }; +} + +async function appWith( + fetcher: NonNullable, + options: { plans?: ReturnType; deps?: CalendarDeps } = {}, +) { + const tenant = await createTestTenant(); + const deps = options.deps ?? calendars(fetcher); + const { app } = createTestApp({ + scheduling: testSchedulingDeps({ calendars: deps }), + ...(options.plans ? { plans: options.plans } : {}), + }); + const headers = { + authorization: `Bearer ${tenant.liveKey}`, + "content-type": "application/json", + }; + const env: SchedulingEnvironment = { + organizationId: tenant.organization.id, + projectId: tenant.project.id, + environmentId: tenant.production.id, + mode: "live", + }; + return { app, tenant, deps, headers, env }; +} + +const googleApp = JSON.stringify({ + provider: "google", + client_id: "acme-google", + client_secret: "acme-secret", + redirect_uri: "https://acme.example/oauth/google", +}); + +const signed = (secret: string, body: string) => { + const timestamp = String(Math.floor(Date.now() / 1000)); + const hash = createHmac("sha256", secret).update(`v0:${timestamp}:${body}`).digest("hex"); + return { + "x-zm-request-timestamp": timestamp, + "x-zm-signature": `v0=${hash}`, + "content-type": "application/json", + }; +}; + +describe("/v1/scheduling/oauth_apps", () => { + test("adding, reading, updating and deleting, and a secret never comes back", async () => { + const api = new FakeGoogleCalendarApi(); + const { app, headers } = await appWith(api.fetch); + const created = await app.request("/v1/scheduling/oauth_apps", { + method: "POST", + headers, + body: googleApp, + }); + expect(created.status).toBe(201); + const body = (await created.json()) as Record; + expect(JSON.stringify(body)).not.toContain("acme-secret"); + expect(body).toMatchObject({ + object: "scheduling.oauth_app", + provider: "google", + redirect_uri: "https://acme.example/oauth/google", + coss_callback_url: "https://api.example.test/scheduling/oauth/google/callback", + status: "active", + zoom_event_url: null, + }); + + const listed = (await (await app.request("/v1/scheduling/oauth_apps", { headers })).json()) as { + data: { id: string }[]; + }; + expect(listed.data.map((row) => row.id)).toEqual([body.id as string]); + + const patched = await app.request(`/v1/scheduling/oauth_apps/${body.id}`, { + method: "PATCH", + headers, + body: JSON.stringify({ completion_url: "https://acme.example/done" }), + }); + expect(((await patched.json()) as { completion_url: string }).completion_url).toBe( + "https://acme.example/done", + ); + + const removed = await app.request(`/v1/scheduling/oauth_apps/${body.id}`, { + method: "DELETE", + headers, + }); + expect(await removed.json()).toEqual({ + object: "scheduling.oauth_app", + id: body.id, + deleted: true, + }); + }); + + test("a wrong secret is a 422, and an address that isn't https too", async () => { + const api = new FakeGoogleCalendarApi(); + api.refusedSecret = "acme-secret"; + const { app, headers } = await appWith(api.fetch); + const refused = await app.request("/v1/scheduling/oauth_apps", { + method: "POST", + headers, + body: googleApp, + }); + expect(refused.status).toBe(422); + expect((await errorOf(refused)).code).toBe("oauth_app_refused"); + + api.refusedSecret = null; + const insecure = await app.request("/v1/scheduling/oauth_apps", { + method: "POST", + headers, + body: JSON.stringify({ + provider: "google", + client_id: "c", + client_secret: "s", + redirect_uri: "http://acme.example/oauth", + }), + }); + expect(insecure.status).toBe(422); + expect((await errorOf(insecure)).code).toBe("invalid_oauth_app"); + }); + + test("a tenant that isn't one can't reach the token URL", async () => { + const urls: string[] = []; + const { app, headers } = await appWith(async (url) => { + urls.push(url); + return new Response(JSON.stringify({ error: "invalid_grant" }), { status: 400 }); + }); + const res = await app.request("/v1/scheduling/oauth_apps", { + method: "POST", + headers, + body: JSON.stringify({ + provider: "microsoft", + client_id: "c", + client_secret: "s", + tenant_id: "evil.example/../../other?x=", + }), + }); + expect(res.status).toBe(422); + expect(urls).toHaveLength(0); + }); + + test("adding one needs the Scale plan, in a sandbox too", async () => { + const api = new FakeGoogleCalendarApi(); + const { app, headers, tenant } = await appWith(api.fetch, { plans: createPlanGate() }); + const create = (key: string) => + app.request("/v1/scheduling/oauth_apps", { + method: "POST", + headers: { ...headers, authorization: `Bearer ${key}` }, + body: googleApp, + }); + for (const key of [tenant.liveKey, tenant.sandboxKey]) { + const refused = await create(key); + expect(refused.status).toBe(402); + const error = await errorOf(refused); + expect(error.code).toBe("plan_feature_unavailable"); + expect(error.message).toContain("Scale"); + } + expect((await app.request("/v1/scheduling/oauth_apps", { headers })).status).toBe(200); + + await createTestContract( + tenant.organization.id, + { version: 1, plan: "scale" }, + { effectiveFrom: new Date("2026-01-01T00:00:00Z") }, + ); + expect((await create(tenant.liveKey)).status).toBe(201); + }); + + test("a sandbox key can't see a live environment's app", async () => { + const api = new FakeGoogleCalendarApi(); + const { app, headers, tenant } = await appWith(api.fetch); + const created = (await ( + await app.request("/v1/scheduling/oauth_apps", { method: "POST", headers, body: googleApp }) + ).json()) as { id: string }; + const res = await app.request(`/v1/scheduling/oauth_apps/${created.id}`, { + headers: { authorization: `Bearer ${tenant.sandboxKey}` }, + }); + expect(res.status).toBe(404); + }); +}); + +describe("the forwarded callback", () => { + test("a consent comes back through coss.com's callback and lands on the completion URL", async () => { + const api = new FakeGoogleCalendarApi(); + const { app, headers, deps, env } = await appWith(api.fetch); + await app.request("/v1/scheduling/oauth_apps", { + method: "POST", + headers, + body: JSON.stringify({ + provider: "google", + client_id: "acme-google", + client_secret: "acme-secret", + redirect_uri: "https://acme.example/oauth/google", + completion_url: "https://acme.example/connected", + }), + }); + const host = await createHost(env, { name: "Dr. Reyes", time_zone: "UTC" }); + const { url } = await startCalendarAuthorization( + env, + host.id, + { provider: "google", returnUrl: null }, + deps, + ); + const state = new URL(url).searchParams.get("state") as string; + // What the customer's forwarding page does: the same query string, on to coss.com's callback. + const res = await app.request( + `/scheduling/oauth/google/callback?code=code&state=${encodeURIComponent(state)}`, + ); + expect(res.status).toBe(302); + expect(res.headers.get("location")).toBe( + "https://acme.example/connected?coss_calendar=connected", + ); + expect(api.tokenRequests.at(-1)?.redirectUri).toBe("https://acme.example/oauth/google"); + }); +}); + +describe("a customer's Zoom events", () => { + async function connected() { + const api = new FakeZoomApi(); + // The test database is shared, and coss.com's own events reach any organization's connections. + api.userId = `zoom_${randomUUID()}`; + const fixture = await appWith(api.fetch); + const created = (await ( + await fixture.app.request("/v1/scheduling/oauth_apps", { + method: "POST", + headers: fixture.headers, + body: JSON.stringify({ + provider: "zoom", + client_id: "acme-zoom", + client_secret: "acme-zs", + webhook_secret: "acme-token", + }), + }) + ).json()) as { id: string; zoom_event_url: string }; + const host = await createHost(fixture.env, { name: "Dr. Reyes", time_zone: "UTC" }); + const { url } = await startCalendarAuthorization( + fixture.env, + host.id, + { provider: "zoom", returnUrl: null }, + fixture.deps, + ); + await completeCalendarAuthorization( + { + provider: "zoom", + state: new URL(url).searchParams.get("state") as string, + code: "code", + error: null, + }, + fixture.deps, + ); + return { ...fixture, api, host, created }; + } + + const deauthorized = (userId: string) => + JSON.stringify({ event: "app_deauthorized", payload: { user_id: userId } }); + + test("its own URL is on the wire, and its own token signs events there", async () => { + const { app, api, host, env, created } = await connected(); + expect(created.zoom_event_url).toBe( + `https://api.example.test/scheduling/zoom/events/${created.id}`, + ); + const body = deauthorized(api.userId); + const res = await app.request(`/scheduling/zoom/events/${created.id}`, { + method: "POST", + headers: signed("acme-token", body), + body, + }); + expect(res.status).toBe(200); + expect(await listConnections(env, host.id)).toHaveLength(0); + }); + + test("Zoom's URL validation answers with the app's own token", async () => { + const { app, created } = await connected(); + const body = JSON.stringify({ + event: "endpoint.url_validation", + payload: { plainToken: "plain" }, + }); + const res = await app.request(`/scheduling/zoom/events/${created.id}`, { + method: "POST", + headers: signed("acme-token", body), + body, + }); + expect(await res.json()).toEqual({ + plainToken: "plain", + encryptedToken: createHmac("sha256", "acme-token").update("plain").digest("hex"), + }); + }); + + test("another token's signature changes nothing, coss.com's included", async () => { + const { app, api, host, env, created } = await connected(); + const body = deauthorized(api.userId); + for (const secret of ["coss-token", "someone-elses"]) { + const res = await app.request(`/scheduling/zoom/events/${created.id}`, { + method: "POST", + headers: signed(secret, body), + body, + }); + expect(res.status).toBe(401); + } + expect(await listConnections(env, host.id)).toHaveLength(1); + }); + + test("coss.com's own events can't reach a connection through a customer's app", async () => { + const { app, api, host, env } = await connected(); + const body = deauthorized(api.userId); + const res = await app.request("/scheduling/zoom/events", { + method: "POST", + headers: signed("coss-token", body), + body, + }); + expect(res.status).toBe(200); + expect(await listConnections(env, host.id)).toHaveLength(1); + }); + + test("an app that doesn't exist, or has no token, is a 404", async () => { + const api = new FakeZoomApi(); + const { app, headers } = await appWith(api.fetch); + const noToken = (await ( + await app.request("/v1/scheduling/oauth_apps", { + method: "POST", + headers, + body: JSON.stringify({ provider: "zoom", client_id: "c", client_secret: "s" }), + }) + ).json()) as { id: string }; + const body = deauthorized(api.userId); + for (const id of [noToken.id, "calapp_nope", "not-an-id"]) { + const res = await app.request(`/scheduling/zoom/events/${id}`, { + method: "POST", + headers: signed("anything", body), + body, + }); + expect(res.status).toBe(404); + } + }); +}); + +describe("the hosted page's session", () => { + test("says when the customer speaks for themselves, and offers Zoom through their own app", async () => { + const api = new FakeZoomApi(); + const { app, headers, env } = await appWith(api.fetch, { + deps: { ...calendars(api.fetch), apps: { google: null, microsoft: null } }, + }); + const host = await createHost(env, { name: "Dr. Reyes", time_zone: "UTC" }); + const read = async () => { + const session = (await ( + await app.request(`/v1/scheduling/hosts/${host.id}/sessions`, { + method: "POST", + headers, + body: "{}", + }) + ).json()) as { token: string }; + return (await ( + await app.request("/scheduling/client/session", { + headers: { authorization: `Bearer ${session.token}` }, + }) + ).json()) as { zoom_available: boolean; own_oauth_app: boolean; privacy_url: string | null }; + }; + expect(await read()).toMatchObject({ + zoom_available: false, + own_oauth_app: false, + privacy_url: null, + }); + await app.request("/v1/scheduling/oauth_apps", { + method: "POST", + headers, + body: JSON.stringify({ provider: "zoom", client_id: "c", client_secret: "s" }), + }); + await app.request("/v1/scheduling/settings", { + method: "PATCH", + headers, + body: JSON.stringify({ privacy_url: "https://acme.example/privacy" }), + }); + expect(await read()).toMatchObject({ + zoom_available: true, + own_oauth_app: true, + privacy_url: "https://acme.example/privacy", + }); + }); + + test("a privacy URL has to be https, since it becomes a link", async () => { + const api = new FakeGoogleCalendarApi(); + const { app, headers } = await appWith(api.fetch); + for (const privacy_url of ["javascript:alert(1)", "http://acme.example/privacy"]) { + const res = await app.request("/v1/scheduling/settings", { + method: "PATCH", + headers, + body: JSON.stringify({ privacy_url }), + }); + expect(res.status).toBe(422); + } + }); +}); diff --git a/apps/api/test/sdk-contract.test.ts b/apps/api/test/sdk-contract.test.ts index 45c8e5979..f9fb21041 100644 --- a/apps/api/test/sdk-contract.test.ts +++ b/apps/api/test/sdk-contract.test.ts @@ -5,6 +5,7 @@ */ import { describe, expect, test } from "bun:test"; +import { createHash } from "node:crypto"; import { createCreditGrant, runDueBillingPeriods } from "@coss/billing"; import { createTestContract, recordTestUsage } from "@coss/billing/testing"; import { db } from "@coss/db"; @@ -27,13 +28,14 @@ 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 { FakeGoogleCalendarApi, testSchedulingDeps } from "@coss/scheduling/testing"; import { createTestTenant } from "@coss/tenancy/testing"; import { API_VERSIONS, LATEST_API_VERSION } from "@coss/versioning"; import { enqueueWebhookMessage, runDueLogDrainDeliveries, runDueWebhookDeliveries, + secretsKeyRing, } from "@coss/webhooks"; import { TEST_SECRETS_KEYS, testSender, webhookReceiver } from "@coss/webhooks/testing"; import { @@ -2294,6 +2296,41 @@ describe("coss.scheduling.outOfOffice against the API", () => { }); describe("coss.scheduling against the API", () => { + test("your own OAuth apps", async () => { + const tenant = await createTestTenant(); + const { app } = createTestApp({ + scheduling: testSchedulingDeps({ + calendars: { + secretsKeys: secretsKeyRing([createHash("sha256").update("sdk oauth apps").digest()]), + apps: { google: null, microsoft: null }, + publicUrl: "https://api.example.test", + fetcher: new FakeGoogleCalendarApi().fetch, + }, + }), + }); + const { scheduling } = clientFor(app, tenant.sandboxKey); + + const oauthApp = await scheduling.oauthApps.create({ + provider: "google", + client_id: "acme-google", + client_secret: "acme-secret", + redirect_uri: "https://example.com/oauth/google", + }); + expect(oauthApp.error).toBeNull(); + expect(oauthApp.data?.redirect_uri).toBe("https://example.com/oauth/google"); + const oauthApps = await scheduling.oauthApps.list(); + expect(oauthApps.data?.data.map((app) => app.id)).toEqual([oauthApp.data?.id as string]); + const updatedApp = await scheduling.oauthApps.update(oauthApp.data?.id as string, { + completion_url: "https://example.com/connected", + }); + expect(updatedApp.data?.completion_url).toBe("https://example.com/connected"); + expect((await scheduling.oauthApps.get(oauthApp.data?.id as string)).data?.status).toBe( + "active", + ); + const removedApp = await scheduling.oauthApps.delete(oauthApp.data?.id as string); + expect(removedApp.data?.deleted).toBe(true); + }); + test("host, schedule, event type, slots, a booking's life, a hold, a session", async () => { const tenant = await createTestTenant(); const { app } = createTestApp(); diff --git a/apps/scheduling/src/app/h/[sessionId]/calendars.tsx b/apps/scheduling/src/app/h/[sessionId]/calendars.tsx index 51472b9aa..838220c50 100644 --- a/apps/scheduling/src/app/h/[sessionId]/calendars.tsx +++ b/apps/scheduling/src/app/h/[sessionId]/calendars.tsx @@ -145,6 +145,10 @@ function ConnectionCard(props: { export function Calendars(props: { call: ClientCall; session: HostSession }) { const { call, session } = props; + // A customer with their own app and name speaks for themselves here, as the consent screen does. + const customerSpeaks = session.own_oauth_app && Boolean(session.brand_name); + const consentName = customerSpeaks ? session.brand_name : "coss.com"; + const consentPrivacy = customerSpeaks ? session.privacy_url : "https://coss.com/legal/privacy"; const [connections, setConnections] = useState(null); const [problem, setProblem] = useState(null); const [caldavOpen, setCaldavOpen] = useState(false); @@ -381,18 +385,23 @@ export function Calendars(props: { call: ClientCall; session: HostSession }) { ) : null}

- Connecting a calendar lets coss.com see when you're busy and which calendars you have, - and add, change and remove the events for your bookings. Your other events only count as - busy times: coss.com never keeps their titles or details, and you can disconnect at any - time.{" "} - - Privacy Policy - + Connecting a calendar lets {consentName} see when you're busy and which calendars you + have, and add, change and remove the events for your bookings. Your other events only + count as busy times: {consentName} never keeps their titles or details, and you can + disconnect at any time. + {consentPrivacy ? ( + <> + {" "} + + Privacy Policy + + + ) : null}

)} diff --git a/apps/scheduling/src/app/h/[sessionId]/types.ts b/apps/scheduling/src/app/h/[sessionId]/types.ts index f2d6a88bb..8f2746c2b 100644 --- a/apps/scheduling/src/app/h/[sessionId]/types.ts +++ b/apps/scheduling/src/app/h/[sessionId]/types.ts @@ -7,4 +7,7 @@ export type HostSession = Branding & { zoom_available: boolean; /** Whether this environment lets hosts give their own bookings to someone else. */ can_reassign: boolean; + /** This environment has its own OAuth app, so what's said before a consent is the customer's. */ + own_oauth_app: boolean; + privacy_url: string | null; }; diff --git a/apps/site/content/docs/scheduling/calendars.mdx b/apps/site/content/docs/scheduling/calendars.mdx index d2da6fa15..b56854722 100644 --- a/apps/site/content/docs/scheduling/calendars.mdx +++ b/apps/site/content/docs/scheduling/calendars.mdx @@ -4,7 +4,8 @@ description: Keep hosts from being booked over something else, and put bookings --- With hundreds of hosts on Google Workspace or Microsoft 365, an admin can authorize a whole domain once -instead: see [calendar delegation](/docs/scheduling/delegation). +instead: see [calendar delegation](/docs/scheduling/delegation). To show your own brand on the consent screen, see +[your own OAuth apps](/docs/scheduling/oauth-apps). Otherwise, a host connects their calendars on [their own page](/docs/scheduling/embedding), or through your UI. For Google and Microsoft, ask for a consent URL and open it in a new window, because providers refuse diff --git a/apps/site/content/docs/scheduling/meta.json b/apps/site/content/docs/scheduling/meta.json index c0ce79535..067d4f083 100644 --- a/apps/site/content/docs/scheduling/meta.json +++ b/apps/site/content/docs/scheduling/meta.json @@ -11,6 +11,7 @@ "reassigning", "calendars", "delegation", + "oauth-apps", "zoom", "bookings", "pools-and-groups", diff --git a/apps/site/content/docs/scheduling/oauth-apps.mdx b/apps/site/content/docs/scheduling/oauth-apps.mdx new file mode 100644 index 000000000..6ce3cc274 --- /dev/null +++ b/apps/site/content/docs/scheduling/oauth-apps.mdx @@ -0,0 +1,182 @@ +--- +title: Your own OAuth apps +description: Connect hosts' calendars through your own Google, Microsoft or Zoom app, so the consent screen shows your brand. +--- + + + +When a host [connects a calendar](/docs/scheduling/calendars), Google, Microsoft or Zoom shows a consent screen +naming the app that's asking. By default that's coss.com's. With your own app, it's your name and logo. + +You register the app with the provider, and give coss.com its client ID and secret. Each environment has its own +apps, one per provider. You own the provider relationship: their review of your app, and what your privacy policy +tells people. Coss.com requests the same access as it does through its own app, and +[no more](/docs/scheduling/calendars#what-cosscom-reads-and-writes). + +With hundreds of hosts on one Google Workspace or Microsoft 365 domain, you may not need a consent screen at all: +see [calendar delegation](/docs/scheduling/delegation). + +## How connections move over + +- **New consents** in the environment use your app as soon as you add it. +- **Connected hosts stay on coss.com's app** until they connect again, because tokens only work with the app that + issued them. Nothing moves by itself, and nobody is disconnected. A calendar connection's `oauth_app` says which it + uses. +- If your app stops working, coss.com never falls back to its own. + +## Google + +1. In a Google Cloud project, enable the Google Calendar API, and create an OAuth client of type **Web application**. +2. Add these scopes to your consent screen, exactly as given: + - `https://www.googleapis.com/auth/calendar.events.freebusy` + - `https://www.googleapis.com/auth/calendar.events.owned` + - `https://www.googleapis.com/auth/calendar.calendarlist.readonly` +3. Add a page on your own domain as the client's authorized redirect URI, and [have it forward](#forwarding-page). +4. Add the app: + + ```ts twoslash + import { Coss } from "coss"; + const coss = new Coss(); + const clientSecret = ""; + // ---cut--- + const { data: app } = await coss.scheduling.oauthApps.create({ + provider: "google", + client_id: "1234567890-abc.apps.googleusercontent.com", + client_secret: clientSecret, + redirect_uri: "https://example.com/oauth/google", + completion_url: "https://example.com/calendar-connected", + }); + // app?.coss_callback_url is where your page forwards to. + ``` + + + An external Google app needs its own [brand verification](https://support.google.com/cloud/answer/13464321). Google + requires you to own every domain on the app, redirect URIs included, which is why it needs a page on your domain: you + can't verify `api.coss.com`. Until it's verified, Google shows a warning and limits it to 100 users. An **Internal** + app, only for your own Workspace, skips this. + + +Your privacy policy has to say how you use Google user data, and that coss.com handles it for you. Check it meets +Google's [API Services User Data Policy](https://developers.google.com/terms/api-services-user-data-policy). + +## Microsoft + +1. In [Microsoft Entra](https://entra.microsoft.com), register an application. For hosts outside your own tenant, make + it multi-tenant. +2. Add the delegated Microsoft Graph permissions `Calendars.ReadWrite`, `offline_access` and `User.Read`. +3. Under **Authentication**, add a **Web** redirect URI. Use `coss_callback_url` from step 5, or + [your own page](#forwarding-page). +4. Under **Certificates & secrets**, create a client secret and note when it expires. +5. Add the app: + + ```ts twoslash + import { Coss } from "coss"; + const coss = new Coss(); + const clientSecret = ""; + // ---cut--- + await coss.scheduling.oauthApps.create({ + provider: "microsoft", + client_id: "3f1c7a52-8e0b-4d6e-9a27-5c4b1e7d2f60", + client_secret: clientSecret, + secret_expires_at: "2028-01-01T00:00:00Z", + }); + ``` + +For an app that's only for your own tenant, also pass `tenant_id`: Microsoft refuses a single-tenant app otherwise. + +## Zoom + +1. In the [Zoom App Marketplace](https://marketplace.zoom.us/develop), create a general app that users manage. +2. Set the OAuth redirect URL to `coss_callback_url` from step 4, or [your own page](#forwarding-page). +3. Add these scopes to the app, since Zoom doesn't take them in the request: `meeting:read:list_meetings`, + `meeting:read:meeting`, `meeting:write:meeting`, `meeting:update:meeting`, `meeting:delete:meeting` and + `user:read:user`. +4. Add the app, with its **Secret Token**: + + ```ts twoslash + import { Coss } from "coss"; + const coss = new Coss(); + const clientSecret = ""; + const secretToken = ""; + // ---cut--- + const { data: app } = await coss.scheduling.oauthApps.create({ + provider: "zoom", + client_id: "abcDEF123", + client_secret: clientSecret, + webhook_secret: secretToken, + }); + // app?.zoom_event_url + ``` + +5. Under **Event Subscriptions**, subscribe to **User has uninstalled or deauthorized the app**, at `zoom_event_url`. + That address is your app's own, and its events only ever reach connections made through it. + + + Zoom only sends deauthorization events for apps published on its Marketplace. On an unpublished app, a host removing it + in Zoom shows up later as a connection that [needs reconnecting](/docs/scheduling/calendars), instead of straight away. + + +## Forwarding page + +A page on your domain takes the provider's redirect, and sends the browser on to `coss_callback_url` with the same query +string. It needs nothing else: `code` and `state` already say everything. In most frameworks it's a single redirect: + +```ts +export function GET(request: Request) { + const { search } = new URL(request.url); + return Response.redirect(`https://api.coss.com/scheduling/oauth/google/callback${search}`, 302); +} +``` + +Register the page with the provider, and pass it as `redirect_uri`. The browser passes through `api.coss.com` for a +moment, then lands on the consent's `return_url` or the app's `completion_url`, with `coss_calendar` set to `connected`, +`denied` or `invalid`. + +## On your host page + +Set a name and a privacy policy in [settings](/docs/scheduling/embedding), and what a host reads before connecting +names you and links your policy instead of coss.com's: + +```ts twoslash +import { Coss } from "coss"; +const coss = new Coss(); +// ---cut--- +await coss.scheduling.settings.update({ + brand_name: "Acme Health", + privacy_url: "https://example.com/privacy", +}); +``` + +## When access is lost + +- **The provider refuses your client ID or secret**, because it was deleted or has expired: the app becomes `failing`, + you get `scheduling.oauth_app.failing`, and your owners and admins get an email. Hosts aren't told, since they can't + fix it. New consents are refused. Calendars can't be read, so hosts stop being offered once what's known of them is + out of date, according to your `calendar_failure_mode`. +- **To fix it**, update the secret. It's tried on a host's connection before it replaces the old one, so a bad paste + changes nothing. Calendars recover on their own, you get `scheduling.oauth_app.restored`, and nobody has to reconnect. +- **During a long outage**, a booking's calendar event is retried for a while, then given up on with + `scheduling.booking.calendar_write_failed`. The booking stands, and that event isn't written once the secret is fixed. +- **One host revokes access**: that connection needs reconnecting, as with coss.com's app, and the app is fine. + +```ts twoslash +import { Coss } from "coss"; +const coss = new Coss(); +const newSecret = ""; +// ---cut--- +await coss.scheduling.oauthApps.update("calapp_01kd2m9a0b1c2d3e4f5g6h7j8m", { + client_secret: newSecret, + secret_expires_at: "2029-01-01T00:00:00Z", +}); +``` + +Owners and admins are warned 14 days before `secret_expires_at`. + +## Removing an app + +An app can't be deleted while connections use it (`oauth_app_in_use`), since only it can refresh their access. +[Disconnect them](/docs/scheduling/calendars) first, and new consents go back to coss.com's app. To move to a different +client ID, hosts have to consent again. + +If your plan changes, a self-serve downgrade is refused until your apps are deleted. If a contract ends, your apps keep +working, so no host loses their calendar, and you can still rotate a secret or delete an app, but not add one. diff --git a/apps/site/content/docs/scheduling/zoom.mdx b/apps/site/content/docs/scheduling/zoom.mdx index 3887abfbf..47f92713c 100644 --- a/apps/site/content/docs/scheduling/zoom.mdx +++ b/apps/site/content/docs/scheduling/zoom.mdx @@ -8,7 +8,7 @@ meeting when a booking is confirmed, moves it when the booking is rescheduled, a is cancelled. The join link goes to the attendee and the host, and onto the host's calendar event. Zoom has to approve coss.com's app before hosts outside Cal.com's own Zoom account can connect. Until then, a host -elsewhere who tries to connect sees an error from Zoom. +elsewhere who tries to connect sees an error from Zoom. You can also use [your own Zoom app](/docs/scheduling/oauth-apps). ## Adding the app @@ -16,7 +16,7 @@ Hosts add the app from the page you give them, not from the Zoom App Marketplace 1. Open your hosted page from your product. If you've embedded it, it's in your own settings. 2. Under **Calendars**, choose **Connect Zoom**. -3. Sign in to Zoom if you're asked, read what coss.com is asking for, and choose **Allow**. +3. Sign in to Zoom if you're asked, read what's being asked for, and choose **Allow**. 4. You're back on the page, with Zoom listed as connected. If you build your own page, ask for a consent URL and open it in a new window: @@ -70,7 +70,7 @@ A host can remove it in either place: - **On your hosted page:** under **Calendars**, choose **Disconnect** next to Zoom. This also revokes coss.com's access at Zoom. -- **In Zoom:** in the Zoom App Marketplace, go to **Manage**, then **Added Apps**, and remove coss.com. +- **In Zoom:** in the Zoom App Marketplace, go to **Manage**, then **Added Apps**, and remove the app. Either way, coss.com deletes the tokens it holds, and new bookings no longer get a Zoom link. Meetings already created stay on the host's Zoom account until the host deletes them, and their links keep working. diff --git a/apps/site/src/lib/changelog.ts b/apps/site/src/lib/changelog.ts index 2213f7d47..2b73c4aaa 100644 --- a/apps/site/src/lib/changelog.ts +++ b/apps/site/src/lib/changelog.ts @@ -14,6 +14,12 @@ export interface ChangelogEntry { } export const CHANGELOG: readonly ChangelogEntry[] = [ + { + date: "2026-11-05", + title: "Your own OAuth apps for Scheduling", + body: "Connect hosts' calendars through your own Google, Microsoft or Zoom app, so the consent screen shows your brand instead of ours.", + href: "/docs/scheduling/oauth-apps", + }, { date: "2026-11-04", title: "Scheduling reminders", diff --git a/apps/site/src/lib/sdk-samples.ts b/apps/site/src/lib/sdk-samples.ts index bd06af8e0..3cda251d1 100644 --- a/apps/site/src/lib/sdk-samples.ts +++ b/apps/site/src/lib/sdk-samples.ts @@ -561,6 +561,26 @@ export const SDK_SAMPLES: Record = { method: "scheduling.calendarDelegations.delete", args: (r) => [pathId(r)], }, + "POST /v1/scheduling/oauth_apps": { + method: "scheduling.oauthApps.create", + args: (r) => [tsValue(r.body ?? {})], + }, + "GET /v1/scheduling/oauth_apps": { + method: "scheduling.oauthApps.list", + args: () => [], + }, + "GET /v1/scheduling/oauth_apps/{id}": { + method: "scheduling.oauthApps.get", + args: (r) => [pathId(r)], + }, + "PATCH /v1/scheduling/oauth_apps/{id}": { + method: "scheduling.oauthApps.update", + args: (r) => [pathId(r), tsValue(r.body ?? {})], + }, + "DELETE /v1/scheduling/oauth_apps/{id}": { + method: "scheduling.oauthApps.delete", + args: (r) => [pathId(r)], + }, "POST /v1/scheduling/hosts/{host}/sessions": { method: "scheduling.sessions.createForHost", args: (r) => [tsValue(r.path.host), tsValue(r.body ?? {})], diff --git a/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/actions.ts b/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/actions.ts index dca5c0f5a..0b1c324c5 100644 --- a/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/actions.ts +++ b/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/actions.ts @@ -13,11 +13,13 @@ import { createHost, createHostSession, createManageSession, + createOauthApp, createSchedule, declineBooking, deleteCalendarDelegation, deleteHolidayCalendar, deleteHost, + deleteOauthApp, deleteOutOfOffice, getEventType, getHost, @@ -27,12 +29,14 @@ import { updateCalendarDelegation, updateEventType, updateHost, + updateOauthApp, updateSettings, verifyCalendarDelegation, } from "@coss/scheduling"; import { LOGO_MAX_BYTES } from "@coss/scheduling/branding"; import { CreateHolidayCalendarRequest, + CreateOauthAppRequest, EVENT_TYPE_KINDS, type ReassignmentObjectOutput, Reminders, @@ -40,6 +44,7 @@ import { UpdateEventTypeRequest, UpdateHostLocationsRequest, UpdateHostRequest, + UpdateOauthAppRequest, } from "@coss/scheduling/schemas"; import { getEnvironment } from "@coss/tenancy"; import { refresh } from "next/cache"; @@ -149,6 +154,90 @@ function secretExpiry(formData: FormData): string | undefined { } const delegationDeps = () => ({ ...schedulingCalendarDeps(), plans: planGate() }); +/** Environment-scoped like the API's, so the event reaches this environment's log drains. */ +function auditOauthApp( + context: OrgContext, + env: { projectId: string; environmentId: string }, + action: string, + id: string, + label?: string, +) { + return recordAudit(context.session, { + organizationId: context.organization.id, + projectId: env.projectId, + environmentId: env.environmentId, + action, + target: { type: "scheduling_oauth_app", id, ...(label ? { label } : {}) }, + }); +} + +export async function createOauthAppAction(_prev: ActionResult | null, formData: FormData) { + return runAction(async () => { + const { context, env } = await contextFor(formData, "update"); + const provider = formString(formData, "provider"); + const optional = (name: string, field: string) => { + const value = formString(formData, name).trim(); + return value ? { [field]: value } : {}; + }; + const secretExpiresAt = secretExpiry(formData); + const params = checked( + CreateOauthAppRequest.safeParse({ + provider, + client_id: formString(formData, "clientId").trim(), + client_secret: formString(formData, "clientSecret"), + ...optional("redirectUri", "redirect_uri"), + ...optional("completionUrl", "completion_url"), + ...(provider === "microsoft" ? optional("tenantId", "tenant_id") : {}), + ...(provider === "zoom" ? optional("webhookSecret", "webhook_secret") : {}), + ...(secretExpiresAt ? { secret_expires_at: secretExpiresAt } : {}), + }), + ); + const app = await friendly(() => + createOauthApp(env, params, { ...schedulingCalendarDeps(), plans: planGate() }), + ); + await auditOauthApp(context, env, "scheduling_oauth_app.created", app.id, app.provider); + refresh(); + return null; + }); +} + +export async function updateOauthAppAction(_prev: ActionResult | null, formData: FormData) { + return runAction(async () => { + const { context, env } = await contextFor(formData, "update"); + const provider = formString(formData, "provider"); + const clientSecret = formString(formData, "clientSecret"); + const webhookSecret = formString(formData, "webhookSecret"); + const params = checked( + UpdateOauthAppRequest.safeParse({ + ...(clientSecret ? { client_secret: clientSecret } : {}), + ...(webhookSecret ? { webhook_secret: webhookSecret } : {}), + // An empty field clears what it holds: these forms always send all of them. + ...(provider === "microsoft" + ? { tenant_id: formString(formData, "tenantId").trim() || null } + : {}), + redirect_uri: formString(formData, "redirectUri").trim() || null, + completion_url: formString(formData, "completionUrl").trim() || null, + secret_expires_at: secretExpiry(formData) ?? null, + }), + ); + const app = await friendly(() => + updateOauthApp(env, formString(formData, "oauthAppId"), params, schedulingCalendarDeps()), + ); + await auditOauthApp(context, env, "scheduling_oauth_app.updated", app.id, app.provider); + refresh(); + return null; + }); +} + +export async function deleteOauthAppAction(_prev: ActionResult | null, formData: FormData) { + return runAction(async () => { + const { context, env } = await contextFor(formData, "update"); + const { id } = await friendly(() => deleteOauthApp(env, formString(formData, "oauthAppId"))); + await auditOauthApp(context, env, "scheduling_oauth_app.deleted", id); + refresh(); + return null; + }); +} export async function createDelegationAction(_prev: ActionResult | null, formData: FormData) { return runAction(async () => { @@ -820,6 +909,7 @@ export async function saveSettingsAction(_prev: ActionResult | null, formData: F .split(/[\s,]+/) .filter(Boolean); const brandName = formString(formData, "brandName"); + const privacyUrl = formString(formData, "privacyUrl").trim(); const emailFrom = formString(formData, "emailFrom"); const emailReplyTo = formString(formData, "emailReplyTo"); const smsFrom = formString(formData, "smsFrom"); @@ -839,6 +929,7 @@ export async function saveSettingsAction(_prev: ActionResult | null, formData: F { allowed_origins: origins, brand_name: brandName || null, + privacy_url: privacyUrl || null, accent_color: useAccent ? accentColor : null, ...(newLogo ? { logo: { content: Buffer.from(await newLogo.arrayBuffer()).toString("base64") } } diff --git a/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/hosts/page.tsx b/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/hosts/page.tsx index 89c51d5ee..5b04cb247 100644 --- a/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/hosts/page.tsx +++ b/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/hosts/page.tsx @@ -1,5 +1,10 @@ import { entitlementsFor, PLANS } from "@coss/billing"; -import { countLiveHosts, failingCalendarDelegations, listHosts } from "@coss/scheduling"; +import { + countLiveHosts, + failingCalendarDelegations, + failingOauthApps, + listHosts, +} from "@coss/scheduling"; import { Alert, AlertDescription, AlertTitle } from "@coss/ui/components/alert"; import { Table, @@ -43,7 +48,10 @@ export default async function HostsPage({ const search = await searchParams; const after = typeof search.after === "string" ? search.after : undefined; const page = await listHosts(scope, { limit: PAGE_SIZE, after }); - const failing = await failingCalendarDelegations(scope); + const [failing, failingApps] = await Promise.all([ + failingCalendarDelegations(scope), + failingOauthApps(scope), + ]); const last = page.data.at(-1); const hostsPath = `${schedulingPath}/hosts`; const canUpdate = can({ scheduling: ["update"] }); @@ -92,6 +100,18 @@ export default async function HostsPage({ ) : null} + {failingApps.length > 0 ? ( + + Your OAuth app's credentials were refused + + Calendars connected through it can't be read, so hosts stop being offered once what's + known of them is out of date.{" "} + + Update the secret + + + + ) : null} ) { + return ( + <> + {Object.entries(props).map(([name, value]) => + value ? : null, + )} + + ); +} + +/** The fields an app and an edit share. React resets uncontrolled ones after an action. */ +function AddressFields(props: { + provider: Provider; + bind: (name: string) => { + name: string; + value: string; + onChange: (event: { target: { value: string } }) => void; + }; +}) { + const { provider, bind } = props; + return ( + <> + {provider === "microsoft" ? ( + + Directory (tenant) ID + + + Only for an app limited to your own tenant. Leave it empty for a multi-tenant app. + + + ) : null} + + Your forwarding page + + + {provider === "google" + ? "Required for an external Google app, since Google verifies the redirect's domain as yours. It sends the browser on to coss.com's redirect URI, with the same query string." + : "Optional. A page on your domain, registered as the redirect URI, that sends the browser on to coss.com's with the same query string."} + + + + After consent + + + Where the browser lands afterwards, unless a consent names its own. Empty: a plain page + saying what happened. + + + + Secret expires + + Owners and admins are warned 14 days before. + + + ); +} + +function useFormValues(initial: Record = {}) { + const [values, setValues] = useState>(initial); + const bind = (name: string) => ({ + name, + value: values[name] ?? "", + onChange: (event: { target: { value: string } }) => + setValues((current) => ({ ...current, [name]: event.target.value })), + }); + return { bind, setValues }; +} + +export function NewOauthAppDialog(props: EnvTarget & { provider: Provider; name: string }) { + const { provider, name, ...target } = props; + const [state, formAction, pending] = useActionToast(createOauthAppAction, { + success: "OAuth app added", + }); + const [open, setOpen] = useState(false); + const { bind, setValues } = useFormValues(); + useEffect(() => { + if (state?.ok) { + setOpen(false); + setValues({}); + } + }, [state, setValues]); + return ( + + }>Use your own + +
+ + + + Your own {name} app + + New consents in this environment use it. Hosts already connected stay on coss.com's + app until they connect again. + + + + + Client ID + + + + Client secret + + + It's tried with {name} first, encrypted, and never shown again. + + + {provider === "zoom" ? ( + + Secret Token + + + From your Zoom app's Features page, to verify its deauthorization events. + + + ) : null} + + {state && !state.ok ? : null} + + + }>Cancel + + + +
+
+ ); +} + +export function EditOauthAppDialog( + props: EnvTarget & { + oauthAppId: string; + provider: Provider; + name: string; + tenantId: string | null; + redirectUri: string | null; + completionUrl: string | null; + secretExpiresAt: string | null; + }, +) { + const { provider, name, tenantId, redirectUri, completionUrl, secretExpiresAt, ...target } = + props; + const [state, formAction, pending] = useActionToast(updateOauthAppAction, { + success: "OAuth app updated", + }); + const [open, setOpen] = useState(false); + const { bind, setValues } = useFormValues({ + tenantId: tenantId ?? "", + redirectUri: redirectUri ?? "", + completionUrl: completionUrl ?? "", + secretExpiresAt: secretExpiresAt?.slice(0, 10) ?? "", + }); + useEffect(() => { + if (state?.ok) { + setOpen(false); + setValues((current) => ({ ...current, clientSecret: "", webhookSecret: "" })); + } + }, [state, setValues]); + return ( + + }>Edit + +
+ + + + Your {name} app + + A different client ID needs its hosts to consent again, so that means a new app. + + + + + New client secret + + + It's tried with {name} on a host's connection before it replaces the old one. Leave + it empty to keep the current one. + + + {provider === "zoom" ? ( + + New Secret Token + + Leave it empty to keep the current one. + + ) : null} + + {state && !state.ok ? : null} + + + }>Cancel + + + +
+
+ ); +} + +export function DeleteOauthAppButton( + props: EnvTarget & { oauthAppId: string; name: string; connections: number }, +) { + const { name, connections, ...target } = props; + const [state, formAction, pending] = useActionToast(deleteOauthAppAction, { + success: "OAuth app deleted", + }); + return ( + + }>Delete + +
+ + + Delete your {name} app? + + {connections > 0 + ? `${connections === 1 ? "1 calendar connection uses" : `${connections} calendar connections use`} it, and only this app can refresh their access, so it can't be deleted until they're disconnected.` + : "New consents go back to coss.com's app."} + + +
+ {state && !state.ok ? : null} +
+ + }>Keep + + + +
+
+ ); +} diff --git a/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/oauth-apps/page.tsx b/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/oauth-apps/page.tsx new file mode 100644 index 000000000..07197888d --- /dev/null +++ b/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/oauth-apps/page.tsx @@ -0,0 +1,189 @@ +import { + GOOGLE_SCOPES, + listOauthApps, + MICROSOFT_SCOPES, + oauthRedirectUri, + ZOOM_APP_SCOPES, +} from "@coss/scheduling"; +import type { OauthAppObjectOutput } from "@coss/scheduling/schemas"; +import { Alert, AlertDescription, AlertTitle } from "@coss/ui/components/alert"; +import { Button } from "@coss/ui/components/button"; +import { KeyRoundIcon } from "lucide-react"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { CopyValue } from "@/components/copy-value"; +import { EmptyState } from "@/components/empty-state"; +import { PageHeader } from "@/components/page-header"; +import { Panel, PanelBody } from "@/components/panel"; +import { SpecItem, SpecList } from "@/components/spec-list"; +import { StatusDot, type StatusTone } from "@/components/status-dot"; +import { planHas } from "@/lib/billing"; +import { schedulingCalendarDeps } from "@/lib/scheduling"; +import { schedulingPageContext } from "../page-context"; +import { DeleteOauthAppButton, EditOauthAppDialog, NewOauthAppDialog } from "./oauth-app-actions"; + +export const metadata: Metadata = { title: "OAuth apps" }; + +const PROVIDERS = [ + { key: "google", name: "Google", scopes: GOOGLE_SCOPES }, + { key: "microsoft", name: "Microsoft", scopes: MICROSOFT_SCOPES }, + { key: "zoom", name: "Zoom", scopes: ZOOM_APP_SCOPES }, +] as const; + +function statusOf(app: OauthAppObjectOutput): { tone: StatusTone; label: string } { + if (app.status === "failing") return { tone: "error", label: "Refused by the provider" }; + return app.verified_at + ? { tone: "success", label: "Active" } + : { tone: "warning", label: "Waiting for a first consent" }; +} + +export default async function OauthAppsPage({ + params, +}: PageProps<"/[orgSlug]/[projectSlug]/[envSlug]/scheduling/oauth-apps">) { + const { organization, scope, can, target } = await schedulingPageContext(await params); + const deps = schedulingCalendarDeps(); + const [apps, planAllows] = await Promise.all([ + listOauthApps(scope, deps), + planHas(organization.id, "schedulingOauthApps"), + ]); + const canUpdate = can({ scheduling: ["update"] }); + return ( +
+ + {!planAllows && apps.length === 0 ? ( + + + } + size="sm" + variant="outline" + > + Compare plans + + } + compact + description="Bring-your-own OAuth apps are part of the Scale plan and above." + icon={KeyRoundIcon} + title="Upgrade to use your own OAuth apps" + /> + + + ) : ( + PROVIDERS.map(({ key, name, scopes }) => { + const app = apps.find((candidate) => candidate.provider === key); + const status = app ? statusOf(app) : null; + return ( + + +
+
+ {name} + + {status ? ( + {status.label} + ) : ( + "Using coss.com's app" + )} + +
+ {canUpdate && app ? ( +
+ + +
+ ) : null} + {canUpdate && !app && planAllows ? ( + + ) : null} +
+
+ {app?.status === "failing" ? ( + + + Calendars connected through this app can't be read + + {name} refused its client ID and secret + {app.last_error_code ? ` (${app.last_error_code})` : ""}. Hosts stop being + offered once what's known of their calendars is out of date, and new + connections are refused. Update the client secret, and nobody has to + reconnect. + + + + ) : null} + + + {app ? ( + <> + + + + {app.connections} + {app.tenant_id ? ( + + + + ) : null} + {app.secret_expires_at ? ( + + {app.secret_expires_at.slice(0, 10)} + + ) : null} + {app.redirect_uri ? ( + + + + ) : null} + {app.completion_url ? ( + + + + ) : null} + {app.zoom_event_url ? ( + + + + ) : null} + + ) : null} + + + + + + + + +
+ ); + }) + )} +
+ ); +} diff --git a/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/scheduling-actions.tsx b/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/scheduling-actions.tsx index 5d24ec082..58046defe 100644 --- a/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/scheduling-actions.tsx +++ b/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/scheduling-actions.tsx @@ -1128,6 +1128,7 @@ export function SettingsForm( settings: { allowed_origins: string[]; brand_name: string | null; + privacy_url: string | null; accent_color: string | null; logo: { data_url: string; width: number; height: number } | null; email_from: string | null; @@ -1186,6 +1187,19 @@ export function SettingsForm( Sites that can embed the hosted pages and call the client API, one per line. + + Privacy policy + + + With your own OAuth app and a name, hosts see this beside what connecting a calendar + does. + +
When a host's calendar can't be read diff --git a/apps/web/src/lib/billing-preflight.ts b/apps/web/src/lib/billing-preflight.ts index c02148e85..b0b8f3413 100644 --- a/apps/web/src/lib/billing-preflight.ts +++ b/apps/web/src/lib/billing-preflight.ts @@ -7,22 +7,28 @@ import { getOrganizationSsoProvider, hasScimConnection } from "@coss/tenancy"; /** How much of each feature the organization uses now (what a downgrade would leave behind). */ async function featureUsage(organizationId: string): Promise> { - const [sso, scim, drain, tracking, subaccount, delegation, hostFilters] = await Promise.all([ - getOrganizationSsoProvider({ organizationId }), - hasScimConnection({ organizationId }), - db.query.logDrain.findFirst({ - columns: { id: true }, - where: (row, { eq }) => eq(row.organizationId, organizationId), - }), - db.query.trackingDomain.findFirst({ - columns: { id: true }, - where: (row, { eq }) => eq(row.organizationId, organizationId), - }), - // Live sub-accounts (sandbox ones are free on every plan). - hasLiveSubaccounts(organizationId), - hasCalendarDelegations(organizationId), - hasLiveHostFilters(organizationId), - ]); + const [sso, scim, drain, tracking, subaccount, delegation, hostFilters, oauthApp] = + await Promise.all([ + getOrganizationSsoProvider({ organizationId }), + hasScimConnection({ organizationId }), + db.query.logDrain.findFirst({ + columns: { id: true }, + where: (row, { eq }) => eq(row.organizationId, organizationId), + }), + db.query.trackingDomain.findFirst({ + columns: { id: true }, + where: (row, { eq }) => eq(row.organizationId, organizationId), + }), + // Live sub-accounts (sandbox ones are free on every plan). + hasLiveSubaccounts(organizationId), + hasCalendarDelegations(organizationId), + hasLiveHostFilters(organizationId), + // Any environment's: tokens only that app can refresh would be stranded just the same. + db.query.schedulingOauthApp.findFirst({ + columns: { id: true }, + where: (row, { eq }) => eq(row.organizationId, organizationId), + }), + ]); return { sso: sso ? 1 : 0, scim: scim ? 1 : 0, @@ -39,6 +45,7 @@ async function featureUsage(organizationId: string): Promise = { schedulingHostPortal: "", schedulingRouting: "Remove host filters from your live event types (Scheduling).", schedulingWhiteLabel: "", + schedulingOauthApps: "Delete your Scheduling OAuth apps.", }; /** diff --git a/apps/web/src/lib/navigation.ts b/apps/web/src/lib/navigation.ts index c237615e2..cf14a434c 100644 --- a/apps/web/src/lib/navigation.ts +++ b/apps/web/src/lib/navigation.ts @@ -187,6 +187,7 @@ const SEGMENT_LABELS: Record = { hooks: "Hooks", "event-types": "Event types", "calendar-delegation": "Calendar delegation", + "oauth-apps": "OAuth apps", endpoints: "Endpoints", received: "Received", inboxes: "Inboxes", diff --git a/apps/worker/src/jobs.ts b/apps/worker/src/jobs.ts index f9b1b2251..f317840d7 100644 --- a/apps/worker/src/jobs.ts +++ b/apps/worker/src/jobs.ts @@ -83,6 +83,7 @@ import { runDueReminderSyncs, runDueSchedulingNotifications, type SchedulingWorkerDeps, + warnOfExpiringOauthAppSecrets, } from "@coss/scheduling"; import { contentScreeningCheck } from "@coss/screening"; import { @@ -264,6 +265,7 @@ export const WORKER_LOOP_NAMES = [ "scheduling-notifications", "scheduling-reminder-sync", "scheduling-calendar-sync", + "scheduling-oauth-app-secrets", "scheduling-calendar-writes", "scheduling-calendar-channels", "scheduling-calendar-delegations", @@ -857,6 +859,14 @@ export function startJobs(deps: WorkerDeps): Loop[] { intervalMs: 15_000, run: async () => (await runDueCalendarDelegations(calendars, { limit: 5 })) === 5, }), + start({ + name: "scheduling-oauth-app-secrets", + intervalMs: 60 * 60_000, + run: async () => { + await warnOfExpiringOauthAppSecrets(calendars); + return false; + }, + }), start({ name: "scheduling-calendar-host-edits", intervalMs: 5 * 60_000, diff --git a/packages/billing/src/catalog/entitlements.ts b/packages/billing/src/catalog/entitlements.ts index 0747329e1..a5ede3acd 100644 --- a/packages/billing/src/catalog/entitlements.ts +++ b/packages/billing/src/catalog/entitlements.ts @@ -13,6 +13,7 @@ export const FEATURE_KEYS = [ "schedulingHostPortal", "schedulingRouting", "schedulingWhiteLabel", + "schedulingOauthApps", ] as const; export type FeatureKey = (typeof FEATURE_KEYS)[number]; @@ -72,6 +73,7 @@ export const FEATURE_LABELS: Record = { schedulingHostPortal: "Scheduling host portal", schedulingRouting: "Scheduling attribute routing", schedulingWhiteLabel: "Scheduling pages without coss.com's name", + schedulingOauthApps: "Bring-your-own OAuth apps for Scheduling", }; export const LIMIT_LABELS: Record = { diff --git a/packages/billing/src/catalog/plans.ts b/packages/billing/src/catalog/plans.ts index 91445bba1..15e5c5f5f 100644 --- a/packages/billing/src/catalog/plans.ts +++ b/packages/billing/src/catalog/plans.ts @@ -107,6 +107,7 @@ export const PLANS = { schedulingHostPortal: false, schedulingRouting: false, schedulingWhiteLabel: false, + schedulingOauthApps: false, }, limits: { domains: 1, @@ -164,6 +165,7 @@ export const PLANS = { schedulingHostPortal: false, schedulingRouting: false, schedulingWhiteLabel: false, + schedulingOauthApps: false, }, limits: { domains: 10, @@ -228,6 +230,7 @@ export const PLANS = { schedulingHostPortal: true, schedulingRouting: true, schedulingWhiteLabel: true, + schedulingOauthApps: true, }, limits: { domains: 100, @@ -292,6 +295,7 @@ export const PLANS = { schedulingHostPortal: true, schedulingRouting: true, schedulingWhiteLabel: true, + schedulingOauthApps: true, }, limits: { domains: null, diff --git a/packages/cli/src/commands/open.ts b/packages/cli/src/commands/open.ts index 8b1a6ee16..70159037d 100644 --- a/packages/cli/src/commands/open.ts +++ b/packages/cli/src/commands/open.ts @@ -47,6 +47,7 @@ const ENVIRONMENT_PAGES: Record = { "scheduling-event-types": "/scheduling/event-types", "scheduling-reminders": "/scheduling/reminders", "scheduling-calendar-delegation": "/scheduling/calendar-delegation", + "scheduling-oauth-apps": "/scheduling/oauth-apps", "scheduling-settings": "/scheduling/settings", hooks: "/hooks", "hooks-event-types": "/hooks/event-types", diff --git a/packages/dashboard-routes/src/index.ts b/packages/dashboard-routes/src/index.ts index 5c31dcb10..df3b3dec2 100644 --- a/packages/dashboard-routes/src/index.ts +++ b/packages/dashboard-routes/src/index.ts @@ -163,6 +163,7 @@ export const PRODUCTS: readonly Product[] = [ { label: "Event types", path: "/scheduling/event-types" }, { label: "Reminders", path: "/scheduling/reminders" }, { label: "Calendar delegation", path: "/scheduling/calendar-delegation" }, + { label: "OAuth apps", path: "/scheduling/oauth-apps" }, { label: "Settings", path: "/scheduling/settings" }, ], }, diff --git a/packages/db/migrations/0140_living_forge.sql b/packages/db/migrations/0140_living_forge.sql new file mode 100644 index 000000000..fd4695fbd --- /dev/null +++ b/packages/db/migrations/0140_living_forge.sql @@ -0,0 +1,33 @@ +CREATE TYPE "public"."scheduling_oauth_app_provider" AS ENUM('google', 'microsoft', 'zoom');--> statement-breakpoint +CREATE TYPE "public"."scheduling_oauth_app_status" AS ENUM('active', 'failing');--> statement-breakpoint +CREATE TABLE "scheduling_oauth_app" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "project_id" text NOT NULL, + "environment_id" text NOT NULL, + "provider" "scheduling_oauth_app_provider" NOT NULL, + "client_id" text NOT NULL, + "tenant_id" text, + "encrypted_credentials" text NOT NULL, + "redirect_uri" text, + "completion_url" text, + "secret_expires_at" timestamp with time zone, + "status" "scheduling_oauth_app_status" DEFAULT 'active' NOT NULL, + "last_error_code" text, + "last_error_at" timestamp with time zone, + "verified_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "scheduling_oauth_app_id_organization_unique" UNIQUE("id","organization_id"), + CONSTRAINT "scheduling_oauth_app_provider_unique" UNIQUE("environment_id","provider") +); +--> statement-breakpoint +ALTER TABLE "scheduling_calendar_connection" ADD COLUMN "oauth_app_id" text;--> statement-breakpoint +ALTER TABLE "scheduling_oauth_state" ADD COLUMN "oauth_app_id" text;--> statement-breakpoint +ALTER TABLE "scheduling_oauth_state" ADD COLUMN "redirect_uri" text;--> statement-breakpoint +ALTER TABLE "scheduling_oauth_state" ADD COLUMN "tenant_id" text;--> statement-breakpoint +ALTER TABLE "scheduling_settings" ADD COLUMN "privacy_url" text;--> statement-breakpoint +ALTER TABLE "scheduling_oauth_app" ADD CONSTRAINT "scheduling_oauth_app_environment_fk" FOREIGN KEY ("environment_id","organization_id") REFERENCES "public"."environment"("id","organization_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "scheduling_calendar_connection" ADD CONSTRAINT "scheduling_calendar_connection_oauth_app_fk" FOREIGN KEY ("oauth_app_id","organization_id") REFERENCES "public"."scheduling_oauth_app"("id","organization_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "scheduling_oauth_state" ADD CONSTRAINT "scheduling_oauth_state_oauth_app_fk" FOREIGN KEY ("oauth_app_id","organization_id") REFERENCES "public"."scheduling_oauth_app"("id","organization_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "scheduling_calendar_connection_oauth_app_idx" ON "scheduling_calendar_connection" USING btree ("oauth_app_id") WHERE "scheduling_calendar_connection"."oauth_app_id" IS NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/meta/0140_snapshot.json b/packages/db/migrations/meta/0140_snapshot.json new file mode 100644 index 000000000..43895745b --- /dev/null +++ b/packages/db/migrations/meta/0140_snapshot.json @@ -0,0 +1,33180 @@ +{ + "id": "e51100c8-f180-430b-9ea7-30cd2b2521fb", + "prevId": "3f39bc05-a004-4202-b808-76f92b10d3e5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_configId_idx": { + "name": "apikey_configId_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_referenceId_idx": { + "name": "apikey_referenceId_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_two_factor": { + "name": "require_two_factor", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "require_sso": { + "name": "require_sso", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_connection_binding": { + "name": "scim_connection_binding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_key": { + "name": "connection_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "decommissioned_at": { + "name": "decommissioned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommission_status": { + "name": "decommission_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "decommission_cursor_user_id": { + "name": "decommission_cursor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommission_reconciled_user_count": { + "name": "decommission_reconciled_user_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_batch_count": { + "name": "decommission_batch_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_revision": { + "name": "decommission_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_completed_at": { + "name": "decommission_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommission_lease_id": { + "name": "decommission_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommission_lease_expires_at": { + "name": "decommission_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scimConnectionBinding_connectionId_idx": { + "name": "scimConnectionBinding_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_connection_binding_connection_key_unique": { + "name": "scim_connection_binding_connection_key_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group": { + "name": "scim_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name_key": { + "name": "display_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimGroup_connectionId_idx": { + "name": "scimGroup_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimGroup_provisioningDomainId_idx": { + "name": "scimGroup_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_group_display_name_key_unique": { + "name": "scim_group_display_name_key_unique", + "nullsNotDistinct": false, + "columns": [ + "display_name_key" + ] + }, + "scim_group_external_id_key_unique": { + "name": "scim_group_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + }, + "scim_group_order_key_unique": { + "name": "scim_group_order_key_unique", + "nullsNotDistinct": false, + "columns": [ + "order_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group_member": { + "name": "scim_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_key": { + "name": "membership_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimGroupMember_connectionId_idx": { + "name": "scimGroupMember_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimGroupMember_groupId_idx": { + "name": "scimGroupMember_groupId_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimGroupMember_scimUserId_idx": { + "name": "scimGroupMember_scimUserId_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_member_group_id_scim_group_id_fk": { + "name": "scim_group_member_group_id_scim_group_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_member_scim_user_id_scim_user_id_fk": { + "name": "scim_group_member_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_user", + "columnsFrom": [ + "scim_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_group_member_membership_key_unique": { + "name": "scim_group_member_membership_key_unique", + "nullsNotDistinct": false, + "columns": [ + "membership_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_identity_tombstone": { + "name": "scim_identity_tombstone", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile": { + "name": "profile", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimIdentityTombstone_connectionId_idx": { + "name": "scimIdentityTombstone_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimIdentityTombstone_provisioningDomainId_idx": { + "name": "scimIdentityTombstone_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimIdentityTombstone_userId_idx": { + "name": "scimIdentityTombstone_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_identity_tombstone_user_id_user_id_fk": { + "name": "scim_identity_tombstone_user_id_user_id_fk", + "tableFrom": "scim_identity_tombstone", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_identity_tombstone_external_id_key_unique": { + "name": "scim_identity_tombstone_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_managed_connection": { + "name": "scim_managed_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "creation_request_id": { + "name": "creation_request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decommission_started_at": { + "name": "decommission_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommission_started_by": { + "name": "decommission_started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommissioned_at": { + "name": "decommissioned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommissioned_by": { + "name": "decommissioned_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scimManagedConnection_provisioningDomainId_idx": { + "name": "scimManagedConnection_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_managed_connection_creation_request_id_unique": { + "name": "scim_managed_connection_creation_request_id_unique", + "nullsNotDistinct": false, + "columns": [ + "creation_request_id" + ] + }, + "scim_managed_connection_connection_id_unique": { + "name": "scim_managed_connection_connection_id_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_managed_connection_event": { + "name": "scim_managed_connection_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_record_id": { + "name": "connection_record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimManagedConnectionEvent_connectionRecordId_idx": { + "name": "scimManagedConnectionEvent_connectionRecordId_idx", + "columns": [ + { + "expression": "connection_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_managed_connection_event_connection_record_id_scim_managed_connection_id_fk": { + "name": "scim_managed_connection_event_connection_record_id_scim_managed_connection_id_fk", + "tableFrom": "scim_managed_connection_event", + "tableTo": "scim_managed_connection", + "columnsFrom": [ + "connection_record_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_managed_connection_event_event_key_unique": { + "name": "scim_managed_connection_event_event_key_unique", + "nullsNotDistinct": false, + "columns": [ + "event_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_managed_credential": { + "name": "scim_managed_credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_record_id": { + "name": "connection_record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_digest": { + "name": "token_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash_version": { + "name": "hash_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_slot_key": { + "name": "active_slot_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serialized_scopes": { + "name": "serialized_scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommissioned_at": { + "name": "decommissioned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scimManagedCredential_connectionRecordId_idx": { + "name": "scimManagedCredential_connectionRecordId_idx", + "columns": [ + { + "expression": "connection_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_managed_credential_connection_record_id_scim_managed_connection_id_fk": { + "name": "scim_managed_credential_connection_record_id_scim_managed_connection_id_fk", + "tableFrom": "scim_managed_credential", + "tableTo": "scim_managed_connection", + "columnsFrom": [ + "connection_record_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_managed_credential_credential_id_unique": { + "name": "scim_managed_credential_credential_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_id" + ] + }, + "scim_managed_credential_active_slot_key_unique": { + "name": "scim_managed_credential_active_slot_key_unique", + "nullsNotDistinct": false, + "columns": [ + "active_slot_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_projection_grant": { + "name": "scim_projection_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_value": { + "name": "source_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_key": { + "name": "grant_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimProjectionGrant_connectionId_idx": { + "name": "scimProjectionGrant_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimProjectionGrant_provisioningDomainId_idx": { + "name": "scimProjectionGrant_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimProjectionGrant_scimUserId_idx": { + "name": "scimProjectionGrant_scimUserId_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimProjectionGrant_userId_idx": { + "name": "scimProjectionGrant_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_projection_grant_scim_user_id_scim_user_id_fk": { + "name": "scim_projection_grant_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_user", + "columnsFrom": [ + "scim_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_projection_grant_user_id_user_id_fk": { + "name": "scim_projection_grant_user_id_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_projection_grant_grant_key_unique": { + "name": "scim_projection_grant_grant_key_unique", + "nullsNotDistinct": false, + "columns": [ + "grant_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_subject": { + "name": "scim_subject", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_source_id": { + "name": "profile_source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimSubject_profileSourceId_idx": { + "name": "scimSubject_profileSourceId_idx", + "columns": [ + { + "expression": "profile_source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_subject_user_id_user_id_fk": { + "name": "scim_subject_user_id_user_id_fk", + "tableFrom": "scim_subject", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_subject_user_id_unique": { + "name": "scim_subject_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user": { + "name": "scim_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_user_key": { + "name": "connection_user_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_name_key": { + "name": "user_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_email": { + "name": "primary_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "work_email_value_index": { + "name": "work_email_value_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_value_index": { + "name": "email_value_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "formatted_name": { + "name": "formatted_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serialized_emails": { + "name": "serialized_emails", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serialized_attributes": { + "name": "serialized_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimUser_connectionId_idx": { + "name": "scimUser_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimUser_provisioningDomainId_idx": { + "name": "scimUser_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimUser_userId_idx": { + "name": "scimUser_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_user_id_user_id_fk": { + "name": "scim_user_user_id_user_id_fk", + "tableFrom": "scim_user", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_user_connection_user_key_unique": { + "name": "scim_user_connection_user_key_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_user_key" + ] + }, + "scim_user_user_name_key_unique": { + "name": "scim_user_user_name_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_name_key" + ] + }, + "scim_user_external_id_key_unique": { + "name": "scim_user_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + }, + "scim_user_order_key_unique": { + "name": "scim_user_order_key_unique", + "nullsNotDistinct": false, + "columns": [ + "order_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "canonical_email": { + "name": "canonical_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_organization_limit": { + "name": "owned_organization_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_canonicalEmail_idx": { + "name": "user_canonicalEmail_idx", + "columns": [ + { + "expression": "canonical_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_account": { + "name": "billing_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "billing_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_customer_id": { + "name": "provider_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collection_method": { + "name": "collection_method", + "type": "billing_collection_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'charge_automatically'" + }, + "net_terms_days": { + "name": "net_terms_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "allowed_payment_methods": { + "name": "allowed_payment_methods", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{card}'::text[]" + }, + "required_payment_methods": { + "name": "required_payment_methods", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "tax_mode": { + "name": "tax_mode", + "type": "billing_tax_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "billing_email": { + "name": "billing_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_cc": { + "name": "billing_cc", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "billing_name": { + "name": "billing_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_address": { + "name": "billing_address", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spend_limit_cents": { + "name": "spend_limit_cents", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "credit_limit_cents": { + "name": "credit_limit_cents", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "alert_thresholds": { + "name": "alert_thresholds", + "type": "integer[]", + "primaryKey": false, + "notNull": true, + "default": "'{80,100}'::integer[]" + }, + "grace_days": { + "name": "grace_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 14 + }, + "status": { + "name": "status", + "type": "billing_account_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "past_due_since": { + "name": "past_due_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hold_exempt_until": { + "name": "hold_exempt_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payment_method": { + "name": "payment_method", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_account_organization_id_organization_id_fk": { + "name": "billing_account_organization_id_organization_id_fk", + "tableFrom": "billing_account", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_account_organization_unique": { + "name": "billing_account_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_budget": { + "name": "billing_budget", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "billing_budget_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "alert_thresholds": { + "name": "alert_thresholds", + "type": "integer[]", + "primaryKey": false, + "notNull": true, + "default": "'{80,100}'::integer[]" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_budget_member_default_idx": { + "name": "billing_budget_member_default_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_budget\".\"scope\" = 'member_default'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_budget_user_idx": { + "name": "billing_budget_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_budget\".\"scope\" = 'user'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_budget_project_idx": { + "name": "billing_budget_project_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_budget\".\"scope\" = 'project'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_budget_api_key_idx": { + "name": "billing_budget_api_key_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "api_key_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_budget\".\"scope\" = 'api_key'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_budget_organization_id_organization_id_fk": { + "name": "billing_budget_organization_id_organization_id_fk", + "tableFrom": "billing_budget", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_budget_user_id_user_id_fk": { + "name": "billing_budget_user_id_user_id_fk", + "tableFrom": "billing_budget", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_budget_api_key_id_apikey_id_fk": { + "name": "billing_budget_api_key_id_apikey_id_fk", + "tableFrom": "billing_budget", + "tableTo": "apikey", + "columnsFrom": [ + "api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_budget_created_by_user_id_user_id_fk": { + "name": "billing_budget_created_by_user_id_user_id_fk", + "tableFrom": "billing_budget", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "billing_budget_project_fk": { + "name": "billing_budget_project_fk", + "tableFrom": "billing_budget", + "tableTo": "project", + "columnsFrom": [ + "project_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_budget_target_check": { + "name": "billing_budget_target_check", + "value": "case \"billing_budget\".\"scope\"\n when 'member_default' then \"billing_budget\".\"user_id\" is null and \"billing_budget\".\"project_id\" is null and \"billing_budget\".\"api_key_id\" is null and \"billing_budget\".\"amount_cents\" is not null\n when 'user' then \"billing_budget\".\"user_id\" is not null and \"billing_budget\".\"project_id\" is null and \"billing_budget\".\"api_key_id\" is null\n when 'project' then \"billing_budget\".\"user_id\" is null and \"billing_budget\".\"project_id\" is not null and \"billing_budget\".\"api_key_id\" is null and \"billing_budget\".\"amount_cents\" is not null\n when 'api_key' then \"billing_budget\".\"user_id\" is null and \"billing_budget\".\"project_id\" is null and \"billing_budget\".\"api_key_id\" is not null and \"billing_budget\".\"amount_cents\" is not null\n end" + }, + "billing_budget_amount_check": { + "name": "billing_budget_amount_check", + "value": "\"billing_budget\".\"amount_cents\" is null or \"billing_budget\".\"amount_cents\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.billing_budget_hold": { + "name": "billing_budget_hold", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "billing_budget_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "spent_cents": { + "name": "spent_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "limit_cents": { + "name": "limit_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_budget_hold_organization_id_organization_id_fk": { + "name": "billing_budget_hold_organization_id_organization_id_fk", + "tableFrom": "billing_budget_hold", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "billing_budget_hold_organization_id_scope_target_id_pk": { + "name": "billing_budget_hold_organization_id_scope_target_id_pk", + "columns": [ + "organization_id", + "scope", + "target_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_contract": { + "name": "billing_contract", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "billing_contract_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "billing_contract_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "terms": { + "name": "terms", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "effective_from": { + "name": "effective_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "effective_until": { + "name": "effective_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "term_end": { + "name": "term_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_renew": { + "name": "auto_renew", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "supersedes_id": { + "name": "supersedes_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pending_invoice_id": { + "name": "pending_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pending_expires_at": { + "name": "pending_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_contract_organization_idx": { + "name": "billing_contract_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effective_from", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_contract_pending_idx": { + "name": "billing_contract_pending_idx", + "columns": [ + { + "expression": "pending_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_contract\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_contract_organization_id_organization_id_fk": { + "name": "billing_contract_organization_id_organization_id_fk", + "tableFrom": "billing_contract", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_contract_account_id_billing_account_id_fk": { + "name": "billing_contract_account_id_billing_account_id_fk", + "tableFrom": "billing_contract", + "tableTo": "billing_account", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_contract_created_by_user_id_user_id_fk": { + "name": "billing_contract_created_by_user_id_user_id_fk", + "tableFrom": "billing_contract", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_counter": { + "name": "billing_counter", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_credit_entry": { + "name": "billing_credit_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "billing_credit_entry_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_credit_entry_grant_idx": { + "name": "billing_credit_entry_grant_idx", + "columns": [ + { + "expression": "grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_credit_entry_grant_id_billing_credit_grant_id_fk": { + "name": "billing_credit_entry_grant_id_billing_credit_grant_id_fk", + "tableFrom": "billing_credit_entry", + "tableTo": "billing_credit_grant", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_credit_grant": { + "name": "billing_credit_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "billing_grant_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "meter": { + "name": "meter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dimension": { + "name": "dimension", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "applies_to": { + "name": "applies_to", + "type": "billing_grant_applies_to", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'usage'" + }, + "source": { + "name": "source", + "type": "billing_grant_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "grant_key": { + "name": "grant_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contract_id": { + "name": "contract_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "voided_at": { + "name": "voided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_credit_grant_organization_idx": { + "name": "billing_credit_grant_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effective_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_credit_grant_invoice_idx": { + "name": "billing_credit_grant_invoice_idx", + "columns": [ + { + "expression": "invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_credit_grant\".\"invoice_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_credit_grant_organization_id_organization_id_fk": { + "name": "billing_credit_grant_organization_id_organization_id_fk", + "tableFrom": "billing_credit_grant", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_credit_grant_created_by_user_id_user_id_fk": { + "name": "billing_credit_grant_created_by_user_id_user_id_fk", + "tableFrom": "billing_credit_grant", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_credit_grant_key_unique": { + "name": "billing_credit_grant_key_unique", + "nullsNotDistinct": false, + "columns": [ + "grant_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_credit_note": { + "name": "billing_credit_note", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "refund_cents": { + "name": "refund_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "billing_credit_note_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "provider_credit_note_id": { + "name": "provider_credit_note_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_attempts": { + "name": "sync_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_credit_note_invoice_idx": { + "name": "billing_credit_note_invoice_idx", + "columns": [ + { + "expression": "invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_credit_note_sync_idx": { + "name": "billing_credit_note_sync_idx", + "columns": [ + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_credit_note\".\"next_sync_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_credit_note_invoice_id_billing_invoice_id_fk": { + "name": "billing_credit_note_invoice_id_billing_invoice_id_fk", + "tableFrom": "billing_credit_note", + "tableTo": "billing_invoice", + "columnsFrom": [ + "invoice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_credit_note_number_unique": { + "name": "billing_credit_note_number_unique", + "nullsNotDistinct": false, + "columns": [ + "number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_hold": { + "name": "billing_hold", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "billing_hold_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_hold_organization_id_organization_id_fk": { + "name": "billing_hold_organization_id_organization_id_fk", + "tableFrom": "billing_hold", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "billing_hold_organization_id_reason_pk": { + "name": "billing_hold_organization_id_reason_pk", + "columns": [ + "organization_id", + "reason" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_invoice": { + "name": "billing_invoice", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_slug": { + "name": "organization_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "billing_invoice_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "billing_invoice_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'usd'" + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "subtotal_cents": { + "name": "subtotal_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "discount_cents": { + "name": "discount_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "credit_cents": { + "name": "credit_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "tax_cents": { + "name": "tax_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "total_cents": { + "name": "total_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "amount_paid_cents": { + "name": "amount_paid_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "amount_due_cents": { + "name": "amount_due_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "collection_method": { + "name": "collection_method", + "type": "billing_collection_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "billing_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_customer_id": { + "name": "provider_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_invoice_id": { + "name": "provider_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hosted_url": { + "name": "hosted_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pdf_url": { + "name": "pdf_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_email": { + "name": "billing_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finalized_at": { + "name": "finalized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "voided_at": { + "name": "voided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_action": { + "name": "sync_action", + "type": "billing_sync_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "sync_attempts": { + "name": "sync_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collection_attempts": { + "name": "collection_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_collection_at": { + "name": "next_collection_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payment_state": { + "name": "payment_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_error": { + "name": "payment_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terms_snapshot": { + "name": "terms_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_invoice_organization_idx": { + "name": "billing_invoice_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_period_idx": { + "name": "billing_invoice_period_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_invoice\".\"kind\" = 'period' and \"billing_invoice\".\"status\" <> 'void'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_final_idx": { + "name": "billing_invoice_final_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_invoice\".\"kind\" = 'final' and \"billing_invoice\".\"status\" <> 'void'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_sync_idx": { + "name": "billing_invoice_sync_idx", + "columns": [ + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_invoice\".\"next_sync_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_collection_idx": { + "name": "billing_invoice_collection_idx", + "columns": [ + { + "expression": "next_collection_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_invoice\".\"next_collection_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_created_at_idx": { + "name": "billing_invoice_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_invoice_number_unique": { + "name": "billing_invoice_number_unique", + "nullsNotDistinct": false, + "columns": [ + "number" + ] + }, + "billing_invoice_provider_unique": { + "name": "billing_invoice_provider_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_invoice_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_invoice_line": { + "name": "billing_invoice_line", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "billing_line_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meter": { + "name": "meter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dimension": { + "name": "dimension", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "covered_units": { + "name": "covered_units", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "unit_nanos": { + "name": "unit_nanos", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "contract_id": { + "name": "contract_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_id": { + "name": "grant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "charge_key": { + "name": "charge_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "billing_invoice_line_invoice_idx": { + "name": "billing_invoice_line_invoice_idx", + "columns": [ + { + "expression": "invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_line_charge_key_idx": { + "name": "billing_invoice_line_charge_key_idx", + "columns": [ + { + "expression": "charge_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_invoice_line\".\"charge_key\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_line_contract_idx": { + "name": "billing_invoice_line_contract_idx", + "columns": [ + { + "expression": "contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_invoice_line\".\"contract_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_invoice_line_invoice_id_billing_invoice_id_fk": { + "name": "billing_invoice_line_invoice_id_billing_invoice_id_fk", + "tableFrom": "billing_invoice_line", + "tableTo": "billing_invoice", + "columnsFrom": [ + "invoice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_notification": { + "name": "billing_notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "billing_notification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "recipients": { + "name": "recipients", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_notification_next_attempt_idx": { + "name": "billing_notification_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_notification\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_notification_created_at_idx": { + "name": "billing_notification_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_notification_organization_id_organization_id_fk": { + "name": "billing_notification_organization_id_organization_id_fk", + "tableFrom": "billing_notification", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_notification_user_id_user_id_fk": { + "name": "billing_notification_user_id_user_id_fk", + "tableFrom": "billing_notification", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_notification_dedupe_unique": { + "name": "billing_notification_dedupe_unique", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_payment": { + "name": "billing_payment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_payment_id": { + "name": "provider_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "recorded_by_user_id": { + "name": "recorded_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_payment_invoice_idx": { + "name": "billing_payment_invoice_idx", + "columns": [ + { + "expression": "invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_payment_invoice_id_billing_invoice_id_fk": { + "name": "billing_payment_invoice_id_billing_invoice_id_fk", + "tableFrom": "billing_payment", + "tableTo": "billing_invoice", + "columnsFrom": [ + "invoice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_payment_provider_unique": { + "name": "billing_payment_provider_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_payment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_period": { + "name": "billing_period", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage": { + "name": "usage", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "warnings": { + "name": "warnings", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_period_account_start_idx": { + "name": "billing_period_account_start_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_period_organization_id_organization_id_fk": { + "name": "billing_period_organization_id_organization_id_fk", + "tableFrom": "billing_period", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_period_account_id_billing_account_id_fk": { + "name": "billing_period_account_id_billing_account_id_fk", + "tableFrom": "billing_period", + "tableTo": "billing_account", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_provider_event": { + "name": "billing_provider_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "billing_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_event_id": { + "name": "provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "billing_provider_event_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_provider_event_unique_idx": { + "name": "billing_provider_event_unique_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_provider_event_next_attempt_idx": { + "name": "billing_provider_event_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_provider_event\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_provider_event_created_at_idx": { + "name": "billing_provider_event_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_quota": { + "name": "billing_quota", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meter": { + "name": "meter", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "reserved": { + "name": "reserved", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_quota_organization_id_organization_id_fk": { + "name": "billing_quota_organization_id_organization_id_fk", + "tableFrom": "billing_quota", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "billing_quota_organization_id_meter_period_start_pk": { + "name": "billing_quota_organization_id_meter_period_start_pk", + "columns": [ + "organization_id", + "meter", + "period_start" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_usage": { + "name": "billing_usage", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hour_start": { + "name": "hour_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "meter": { + "name": "meter", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dimension": { + "name": "dimension", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shard": { + "name": "shard", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "quantity": { + "name": "quantity", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "billing_usage_hour_idx": { + "name": "billing_usage_hour_idx", + "columns": [ + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_usage_organization_id_organization_id_fk": { + "name": "billing_usage_organization_id_organization_id_fk", + "tableFrom": "billing_usage", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "billing_usage_organization_id_hour_start_meter_dimension_environment_id_shard_pk": { + "name": "billing_usage_organization_id_hour_start_meter_dimension_environment_id_shard_pk", + "columns": [ + "organization_id", + "hour_start", + "meter", + "dimension", + "environment_id", + "shard" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_usage_attribution": { + "name": "billing_usage_attribution", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hour_start": { + "name": "hour_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "meter": { + "name": "meter", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dimension": { + "name": "dimension", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "shard": { + "name": "shard", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "quantity": { + "name": "quantity", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "billing_usage_attribution_hour_idx": { + "name": "billing_usage_attribution_hour_idx", + "columns": [ + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_usage_attribution_organization_id_organization_id_fk": { + "name": "billing_usage_attribution_organization_id_organization_id_fk", + "tableFrom": "billing_usage_attribution", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "billing_usage_attribution_pk": { + "name": "billing_usage_attribution_pk", + "columns": [ + "organization_id", + "hour_start", + "meter", + "dimension", + "environment_id", + "api_key_id", + "user_id", + "shard" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_audit_event": { + "name": "admin_audit_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "admin_audit_event_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_audit_event_target_idx": { + "name": "admin_audit_event_target_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_audit_event_organization_idx": { + "name": "admin_audit_event_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"admin_audit_event\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_audit_event_created_at_idx": { + "name": "admin_audit_event_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audience": { + "name": "audience", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "audience_environment_fk": { + "name": "audience_environment_fk", + "tableFrom": "audience", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "audience_environment_name_unique": { + "name": "audience_environment_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "name" + ] + }, + "audience_id_organization_unique": { + "name": "audience_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audience_member": { + "name": "audience_member", + "schema": "", + "columns": { + "audience_id": { + "name": "audience_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audience_member_contact_idx": { + "name": "audience_member_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audience_member_audience_fk": { + "name": "audience_member_audience_fk", + "tableFrom": "audience_member", + "tableTo": "audience", + "columnsFrom": [ + "audience_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audience_member_contact_fk": { + "name": "audience_member_contact_fk", + "tableFrom": "audience_member", + "tableTo": "contact", + "columnsFrom": [ + "contact_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audience_member_audience_id_contact_id_pk": { + "name": "audience_member_audience_id_contact_id_pk", + "columns": [ + "audience_id", + "contact_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_email": { + "name": "auth_email", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "auth_email_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "to": { + "name": "to", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "auth_email_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "auth_email_next_attempt_idx": { + "name": "auth_email_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"auth_email\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.broadcast": { + "name": "broadcast", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_owner_id": { + "name": "api_key_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "broadcast_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "audience_id": { + "name": "audience_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "segment_id": { + "name": "segment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from": { + "name": "from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_to": { + "name": "reply_to", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "preview_text": { + "name": "preview_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "html": { + "name": "html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "track_opens": { + "name": "track_opens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "track_clicks": { + "name": "track_clicks", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rate_per_second": { + "name": "rate_per_second", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "contact_upper_bound": { + "name": "contact_upper_bound", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recipients_estimated": { + "name": "recipients_estimated", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "materialized": { + "name": "materialized", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "screening_status": { + "name": "screening_status", + "type": "broadcast_screening_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "screening_held_at": { + "name": "screening_held_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "broadcast_environment_created_at_idx": { + "name": "broadcast_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "broadcast_due_idx": { + "name": "broadcast_due_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"broadcast\".\"status\" in ('scheduled', 'sending')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "broadcast_environment_fk": { + "name": "broadcast_environment_fk", + "tableFrom": "broadcast", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "broadcast_id_organization_unique": { + "name": "broadcast_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "broadcast_environment_idempotency_key_unique": { + "name": "broadcast_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "broadcast_target_check": { + "name": "broadcast_target_check", + "value": "(\"broadcast\".\"audience_id\" is null) <> (\"broadcast\".\"segment_id\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.broadcast_stat": { + "name": "broadcast_stat", + "schema": "", + "columns": { + "broadcast_id": { + "name": "broadcast_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shard": { + "name": "shard", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipients": { + "name": "recipients", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sent": { + "name": "sent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "delivered": { + "name": "delivered", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bounced": { + "name": "bounced", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "complained": { + "name": "complained", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed": { + "name": "failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "opened": { + "name": "opened", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "clicked": { + "name": "clicked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "unsubscribed": { + "name": "unsubscribed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "broadcast_stat_broadcast_fk": { + "name": "broadcast_stat_broadcast_fk", + "tableFrom": "broadcast_stat", + "tableTo": "broadcast", + "columnsFrom": [ + "broadcast_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "broadcast_stat_broadcast_id_shard_pk": { + "name": "broadcast_stat_broadcast_id_shard_pk", + "columns": [ + "broadcast_id", + "shard" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact": { + "name": "contact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "contact_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "contact_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'subscribed'" + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "source": { + "name": "source", + "type": "contact_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "subscribed_at": { + "name": "subscribed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "unsubscribed_at": { + "name": "unsubscribed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pending_since": { + "name": "pending_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmation_sent_at": { + "name": "confirmation_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmation_count": { + "name": "confirmation_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_environment_sequence_idx": { + "name": "contact_environment_sequence_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_environment_id_idx": { + "name": "contact_environment_id_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_properties_idx": { + "name": "contact_properties_idx", + "columns": [ + { + "expression": "properties", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "jsonb_path_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "contact_pending_since_idx": { + "name": "contact_pending_since_idx", + "columns": [ + { + "expression": "pending_since", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"contact\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_environment_fk": { + "name": "contact_environment_fk", + "tableFrom": "contact", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contact_environment_address_unique": { + "name": "contact_environment_address_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "address" + ] + }, + "contact_id_organization_unique": { + "name": "contact_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_counter": { + "name": "contact_counter", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "contact_counter_environment_fk": { + "name": "contact_counter_environment_fk", + "tableFrom": "contact_counter", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "contact_counter_count_check": { + "name": "contact_counter_count_check", + "value": "\"contact_counter\".\"count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.contact_property": { + "name": "contact_property", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "contact_property_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "fallback": { + "name": "fallback", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "contact_property_environment_fk": { + "name": "contact_property_environment_fk", + "tableFrom": "contact_property", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contact_property_environment_key_unique": { + "name": "contact_property_environment_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_topic": { + "name": "contact_topic", + "schema": "", + "columns": { + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "contact_topic_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "contact_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_topic_topic_status_idx": { + "name": "contact_topic_topic_status_idx", + "columns": [ + { + "expression": "topic_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_topic_contact_fk": { + "name": "contact_topic_contact_fk", + "tableFrom": "contact_topic", + "tableTo": "contact", + "columnsFrom": [ + "contact_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_topic_topic_fk": { + "name": "contact_topic_topic_fk", + "tableFrom": "contact_topic", + "tableTo": "topic", + "columnsFrom": [ + "topic_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contact_topic_contact_id_topic_id_pk": { + "name": "contact_topic_contact_id_topic_id_pk", + "columns": [ + "contact_id", + "topic_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_screening": { + "name": "content_screening", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "classifier": { + "name": "classifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verdict": { + "name": "verdict", + "type": "content_screening_verdict", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enforced": { + "name": "enforced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "scores": { + "name": "scores", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "top_category": { + "name": "top_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "top_score": { + "name": "top_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "content_screening_organization_created_at_idx": { + "name": "content_screening_organization_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_screening_created_at_idx": { + "name": "content_screening_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_screening_subject_idx": { + "name": "content_screening_subject_idx", + "columns": [ + { + "expression": "product", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "content_screening_environment_fk": { + "name": "content_screening_environment_fk", + "tableFrom": "content_screening", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_screening_counter": { + "name": "content_screening_counter", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "seen": { + "name": "seen", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "owed": { + "name": "owed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "content_screening_counter_organization_id_organization_id_fk": { + "name": "content_screening_counter_organization_id_organization_id_fk", + "tableFrom": "content_screening_counter", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "content_screening_counter_organization_id_product_group_id_pk": { + "name": "content_screening_counter_organization_id_product_group_id_pk", + "columns": [ + "organization_id", + "product", + "group_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dedicated_ip_delivery_rollup": { + "name": "dedicated_ip_delivery_rollup", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hour": { + "name": "hour", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "sending_ip": { + "name": "sending_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivered": { + "name": "delivered", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "dedicated_ip_delivery_rollup_hour_idx": { + "name": "dedicated_ip_delivery_rollup_hour_idx", + "columns": [ + { + "expression": "hour", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dedicated_ip_delivery_rollup_organization_id_organization_id_fk": { + "name": "dedicated_ip_delivery_rollup_organization_id_organization_id_fk", + "tableFrom": "dedicated_ip_delivery_rollup", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dedicated_ip_delivery_rollup_organization_id_hour_sending_ip_pk": { + "name": "dedicated_ip_delivery_rollup_organization_id_hour_sending_ip_pk", + "columns": [ + "organization_id", + "hour", + "sending_ip" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dedicated_ip_pool": { + "name": "dedicated_ip_pool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "dedicated_ip_pool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "ips": { + "name": "ips", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "dedicated_share": { + "name": "dedicated_share", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "status_message": { + "name": "status_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provisioned_at": { + "name": "provisioned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ready_at": { + "name": "ready_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "release_after": { + "name": "release_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "release_started_at": { + "name": "release_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tls_required_at": { + "name": "tls_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "marketing_configured_at": { + "name": "marketing_configured_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dedicated_ip_pool_next_check_at_idx": { + "name": "dedicated_ip_pool_next_check_at_idx", + "columns": [ + { + "expression": "next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"dedicated_ip_pool\".\"next_check_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dedicated_ip_pool_organization_id_organization_id_fk": { + "name": "dedicated_ip_pool_organization_id_organization_id_fk", + "tableFrom": "dedicated_ip_pool", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dedicated_ip_pool_organization_unique": { + "name": "dedicated_ip_pool_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dmarc_notification": { + "name": "dmarc_notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_id": { + "name": "domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "dmarc_notification_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "policy_changed_at": { + "name": "policy_changed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "dmarc_notification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "recipients": { + "name": "recipients", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dmarc_notification_due_idx": { + "name": "dmarc_notification_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"dmarc_notification\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "dmarc_notification_created_at_idx": { + "name": "dmarc_notification_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dmarc_notification_domain_fk": { + "name": "dmarc_notification_domain_fk", + "tableFrom": "dmarc_notification", + "tableTo": "domain", + "columnsFrom": [ + "domain_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dmarc_notification_dedupe_unique": { + "name": "dmarc_notification_dedupe_unique", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dmarc_report": { + "name": "dmarc_report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_id": { + "name": "domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reporter_org": { + "name": "reporter_org", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reporter_email": { + "name": "reporter_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report_id": { + "name": "report_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_begin": { + "name": "period_begin", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "policy_published": { + "name": "policy_published", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pass_count": { + "name": "pass_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dmarc_report_domain_period_idx": { + "name": "dmarc_report_domain_period_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dmarc_report_domain_id_idx": { + "name": "dmarc_report_domain_id_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dmarc_report_created_at_idx": { + "name": "dmarc_report_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dmarc_report_organization_idx": { + "name": "dmarc_report_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dmarc_report_domain_fk": { + "name": "dmarc_report_domain_fk", + "tableFrom": "dmarc_report", + "tableTo": "domain", + "columnsFrom": [ + "domain_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dmarc_report_reporter_unique": { + "name": "dmarc_report_reporter_unique", + "nullsNotDistinct": false, + "columns": [ + "domain_id", + "reporter_org", + "report_id" + ] + }, + "dmarc_report_id_organization_unique": { + "name": "dmarc_report_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dmarc_report_record": { + "name": "dmarc_report_record", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_id": { + "name": "domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dmarc_report_id": { + "name": "dmarc_report_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_begin": { + "name": "period_begin", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "source_ip": { + "name": "source_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dkim": { + "name": "dkim", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spf": { + "name": "spf", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dmarc_pass": { + "name": "dmarc_pass", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "header_from": { + "name": "header_from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "envelope_from": { + "name": "envelope_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_results": { + "name": "auth_results", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "via_coss": { + "name": "via_coss", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "dmarc_report_record_report_idx": { + "name": "dmarc_report_record_report_idx", + "columns": [ + { + "expression": "dmarc_report_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dmarc_report_record_domain_period_idx": { + "name": "dmarc_report_record_domain_period_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dmarc_report_record_organization_idx": { + "name": "dmarc_report_record_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dmarc_report_record_report_fk": { + "name": "dmarc_report_record_report_fk", + "tableFrom": "dmarc_report_record", + "tableTo": "dmarc_report", + "columnsFrom": [ + "dmarc_report_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "domain_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "can_send": { + "name": "can_send", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dkim_tokens": { + "name": "dkim_tokens", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "dkim_hosted_zone": { + "name": "dkim_hosted_zone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mail_from_domain": { + "name": "mail_from_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mail_from_status": { + "name": "mail_from_status", + "type": "mail_from_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "records": { + "name": "records", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "bimi": { + "name": "bimi", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verification_started_at": { + "name": "verification_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failed_at": { + "name": "failed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dmarc_report_token": { + "name": "dmarc_report_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dmarc_record": { + "name": "dmarc_record", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dmarc_inherited_record": { + "name": "dmarc_inherited_record", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dmarc_policy_changed_at": { + "name": "dmarc_policy_changed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subaccount_id": { + "name": "subaccount_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_with_subaccounts": { + "name": "shared_with_subaccounts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "domain_subaccount_idx": { + "name": "domain_subaccount_idx", + "columns": [ + { + "expression": "subaccount_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"domain\".\"subaccount_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "domain_name_active_unique": { + "name": "domain_name_active_unique", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"domain\".\"status\" <> 'failed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "domain_organization_idx": { + "name": "domain_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "domain_next_check_at_idx": { + "name": "domain_next_check_at_idx", + "columns": [ + { + "expression": "next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"domain\".\"next_check_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "domain_organization_id_organization_id_fk": { + "name": "domain_organization_id_organization_id_fk", + "tableFrom": "domain", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_created_by_user_id_user_id_fk": { + "name": "domain_created_by_user_id_user_id_fk", + "tableFrom": "domain", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "domain_subaccount_fk": { + "name": "domain_subaccount_fk", + "tableFrom": "domain", + "tableTo": "email_subaccount", + "columnsFrom": [ + "subaccount_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "domain_dmarc_report_token_unique": { + "name": "domain_dmarc_report_token_unique", + "nullsNotDistinct": false, + "columns": [ + "dmarc_report_token" + ] + }, + "domain_id_organization_unique": { + "name": "domain_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "domain_shared_not_subaccount": { + "name": "domain_shared_not_subaccount", + "value": "not (\"domain\".\"shared_with_subaccounts\" and \"domain\".\"subaccount_id\" is not null)" + } + }, + "isRLSEnabled": false + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_owner_id": { + "name": "api_key_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "email_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "from": { + "name": "from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to": { + "name": "to", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "cc": { + "name": "cc", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "bcc": { + "name": "bcc", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "reply_to": { + "name": "reply_to", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "html": { + "name": "html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discard_body": { + "name": "discard_body", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "screening_exempt": { + "name": "screening_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "store_content": { + "name": "store_content", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_discarded_at": { + "name": "content_discarded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream": { + "name": "stream", + "type": "email_stream", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'transactional'" + }, + "track_opens": { + "name": "track_opens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "track_clicks": { + "name": "track_clicks", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "unsubscribe": { + "name": "unsubscribe", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tracking_token": { + "name": "tracking_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "open_count": { + "name": "open_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_opened_at": { + "name": "first_opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_opened_at": { + "name": "last_opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "click_count": { + "name": "click_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_clicked_at": { + "name": "first_clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_clicked_at": { + "name": "last_clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "batch_id": { + "name": "batch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "batch_index": { + "name": "batch_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "broadcast_id": { + "name": "broadcast_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "billable_recipients": { + "name": "billable_recipients", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template_version": { + "name": "template_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "template_locale": { + "name": "template_locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dedicated_pool": { + "name": "dedicated_pool", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "require_tls": { + "name": "require_tls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hipaa_origin": { + "name": "hipaa_origin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "subaccount_id": { + "name": "subaccount_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subaccount_tenant": { + "name": "subaccount_tenant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "traceparent": { + "name": "traceparent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "email_batch_idx": { + "name": "email_batch_idx", + "columns": [ + { + "expression": "batch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "batch_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email\".\"batch_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_broadcast_contact_unique": { + "name": "email_broadcast_contact_unique", + "columns": [ + { + "expression": "broadcast_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"email\".\"broadcast_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_broadcast_status_idx": { + "name": "email_broadcast_status_idx", + "columns": [ + { + "expression": "broadcast_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email\".\"broadcast_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_environment_created_at_idx": { + "name": "email_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_organization_created_at_idx": { + "name": "email_organization_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_organization_send_time_idx": { + "name": "email_organization_send_time_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"sent_at\", \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_subaccount_send_time_idx": { + "name": "email_subaccount_send_time_idx", + "columns": [ + { + "expression": "subaccount_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"sent_at\", \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email\".\"subaccount_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_scheduled_due_idx": { + "name": "email_scheduled_due_idx", + "columns": [ + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email\".\"status\" = 'scheduled'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_queued_created_at_idx": { + "name": "email_queued_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_created_at_idx": { + "name": "email_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_environment_fk": { + "name": "email_environment_fk", + "tableFrom": "email", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_batch_fk": { + "name": "email_batch_fk", + "tableFrom": "email", + "tableTo": "email_batch", + "columnsFrom": [ + "batch_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_tracking_token_unique": { + "name": "email_tracking_token_unique", + "nullsNotDistinct": false, + "columns": [ + "tracking_token" + ] + }, + "email_environment_idempotency_key_unique": { + "name": "email_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + }, + "email_id_organization_unique": { + "name": "email_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_attachment": { + "name": "email_attachment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_attachment_email_fk": { + "name": "email_attachment_email_fk", + "tableFrom": "email_attachment", + "tableTo": "email", + "columnsFrom": [ + "email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_attachment_email_position_unique": { + "name": "email_attachment_email_position_unique", + "nullsNotDistinct": false, + "columns": [ + "email_id", + "position" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_batch": { + "name": "email_batch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_batch_environment_fk": { + "name": "email_batch_environment_fk", + "tableFrom": "email_batch", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_batch_environment_idempotency_key_unique": { + "name": "email_batch_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + }, + "email_batch_id_organization_unique": { + "name": "email_batch_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_event": { + "name": "email_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "email_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_event_type": { + "name": "provider_event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient": { + "name": "recipient", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_event_email_occurred_at_idx": { + "name": "email_event_email_occurred_at_idx", + "columns": [ + { + "expression": "email_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_event_email_fk": { + "name": "email_event_email_fk", + "tableFrom": "email_event", + "tableTo": "email", + "columnsFrom": [ + "email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_event_email_dedupe_unique": { + "name": "email_event_email_dedupe_unique", + "nullsNotDistinct": false, + "columns": [ + "email_id", + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_link": { + "name": "email_link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "click_count": { + "name": "click_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_clicked_at": { + "name": "first_clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_clicked_at": { + "name": "last_clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_link_email_fk": { + "name": "email_link_email_fk", + "tableFrom": "email_link", + "tableTo": "email", + "columnsFrom": [ + "email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_link_email_position_unique": { + "name": "email_link_email_position_unique", + "nullsNotDistinct": false, + "columns": [ + "email_id", + "position" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_recipient": { + "name": "email_recipient", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "email_recipient_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "email_recipient_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "last_event_at": { + "name": "last_event_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_recipient_organization_address_idx": { + "name": "email_recipient_organization_address_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_recipient_email_fk": { + "name": "email_recipient_email_fk", + "tableFrom": "email_recipient", + "tableTo": "email", + "columnsFrom": [ + "email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_recipient_email_address_unique": { + "name": "email_recipient_email_address_unique", + "nullsNotDistinct": false, + "columns": [ + "email_id", + "address" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_reputation_rollup": { + "name": "email_reputation_rollup", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket_start": { + "name": "bucket_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "email_stream", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'transactional'" + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "computed_revision": { + "name": "computed_revision", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "recipients": { + "name": "recipients", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bounces": { + "name": "bounces", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "complaints": { + "name": "complaints", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "simulator_recipients": { + "name": "simulator_recipients", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "simulator_bounces": { + "name": "simulator_bounces", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "simulator_complaints": { + "name": "simulator_complaints", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_reputation_rollup_organization_id_organization_id_fk": { + "name": "email_reputation_rollup_organization_id_organization_id_fk", + "tableFrom": "email_reputation_rollup", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "email_reputation_rollup_organization_id_bucket_start_stream_pk": { + "name": "email_reputation_rollup_organization_id_bucket_start_stream_pk", + "columns": [ + "organization_id", + "bucket_start", + "stream" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_sender_domain_block": { + "name": "email_sender_domain_block", + "schema": "", + "columns": { + "domain": { + "name": "domain", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_sender_domain_block_organization_idx": { + "name": "email_sender_domain_block_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_storage_settings": { + "name": "email_storage_settings", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "store_content": { + "name": "store_content", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_storage_settings_environment_fk": { + "name": "email_storage_settings_environment_fk", + "tableFrom": "email_storage_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_subaccount": { + "name": "email_subaccount", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "email_subaccount_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "daily_recipient_limit": { + "name": "daily_recipient_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "per_second_recipient_limit": { + "name": "per_second_recipient_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "per_second_burst": { + "name": "per_second_burst", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reputation_status": { + "name": "reputation_status", + "type": "email_subaccount_reputation", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'good'" + }, + "reputation_trigger": { + "name": "reputation_trigger", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reputation_changed_at": { + "name": "reputation_changed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reinstated_at": { + "name": "reinstated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_evaluation_at": { + "name": "next_evaluation_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "evaluation_lease_until": { + "name": "evaluation_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deletion_lease_until": { + "name": "deletion_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deletion_attempts": { + "name": "deletion_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_subaccount_environment_created_at_idx": { + "name": "email_subaccount_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_subaccount_next_evaluation_idx": { + "name": "email_subaccount_next_evaluation_idx", + "columns": [ + { + "expression": "next_evaluation_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email_subaccount\".\"next_evaluation_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_subaccount_deleting_idx": { + "name": "email_subaccount_deleting_idx", + "columns": [ + { + "expression": "deletion_lease_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email_subaccount\".\"status\" = 'deleting'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_subaccount_environment_fk": { + "name": "email_subaccount_environment_fk", + "tableFrom": "email_subaccount", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_subaccount_environment_uid_unique": { + "name": "email_subaccount_environment_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "uid" + ] + }, + "email_subaccount_id_organization_unique": { + "name": "email_subaccount_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_suppression": { + "name": "email_suppression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "email_suppression_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "email_suppression_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subaccount_id": { + "name": "subaccount_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_suppression_subaccount_sequence_idx": { + "name": "email_suppression_subaccount_sequence_idx", + "columns": [ + { + "expression": "subaccount_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email_suppression\".\"subaccount_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_suppression_environment_sequence_idx": { + "name": "email_suppression_environment_sequence_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_suppression_environment_fk": { + "name": "email_suppression_environment_fk", + "tableFrom": "email_suppression", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_suppression_subaccount_fk": { + "name": "email_suppression_subaccount_fk", + "tableFrom": "email_suppression", + "tableTo": "email_subaccount", + "columnsFrom": [ + "subaccount_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_suppression_environment_subaccount_address_scope_unique": { + "name": "email_suppression_environment_subaccount_address_scope_unique", + "nullsNotDistinct": true, + "columns": [ + "environment_id", + "subaccount_id", + "address", + "scope" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_template": { + "name": "email_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_version": { + "name": "published_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "email_template_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + } + }, + "indexes": { + "email_template_environment_created_at_idx": { + "name": "email_template_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_template_environment_fk": { + "name": "email_template_environment_fk", + "tableFrom": "email_template", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_template_environment_alias_unique": { + "name": "email_template_environment_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "alias" + ] + }, + "email_template_id_organization_unique": { + "name": "email_template_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_template_content": { + "name": "email_template_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "html": { + "name": "html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_template_content_version_fk": { + "name": "email_template_content_version_fk", + "tableFrom": "email_template_content", + "tableTo": "email_template_version", + "columnsFrom": [ + "version_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_template_content_version_locale_unique": { + "name": "email_template_content_version_locale_unique", + "nullsNotDistinct": false, + "columns": [ + "version_id", + "locale" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_template_version": { + "name": "email_template_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "default_locale": { + "name": "default_locale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sample_variables": { + "name": "sample_variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "has_unpublished_changes": { + "name": "has_unpublished_changes", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_template_version_draft_unique": { + "name": "email_template_version_draft_unique", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"email_template_version\".\"version\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_template_version_template_fk": { + "name": "email_template_version_template_fk", + "tableFrom": "email_template_version", + "tableTo": "email_template", + "columnsFrom": [ + "template_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_template_version_template_version_unique": { + "name": "email_template_version_template_version_unique", + "nullsNotDistinct": false, + "columns": [ + "template_id", + "version" + ] + }, + "email_template_version_id_organization_unique": { + "name": "email_template_version_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_tenant": { + "name": "email_tenant", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subaccount_id": { + "name": "subaccount_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream": { + "name": "stream", + "type": "email_stream", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'transactional'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_name": { + "name": "tenant_name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_arn": { + "name": "tenant_arn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sending_status": { + "name": "sending_status", + "type": "tenant_sending_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'enabled'" + }, + "suppression_migrated_at": { + "name": "suppression_migrated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status_cause": { + "name": "status_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_updated_at": { + "name": "status_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_tenant_organization_id_organization_id_fk": { + "name": "email_tenant_organization_id_organization_id_fk", + "tableFrom": "email_tenant", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_tenant_subaccount_fk": { + "name": "email_tenant_subaccount_fk", + "tableFrom": "email_tenant", + "tableTo": "email_subaccount", + "columnsFrom": [ + "subaccount_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_tenant_owner_stream_unique": { + "name": "email_tenant_owner_stream_unique", + "nullsNotDistinct": true, + "columns": [ + "organization_id", + "subaccount_id", + "stream" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_tracking_rule": { + "name": "email_tracking_rule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "opens": { + "name": "opens", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "clicks": { + "name": "clicks", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "unsubscribe": { + "name": "unsubscribe", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_tracking_rule_environment_fk": { + "name": "email_tracking_rule_environment_fk", + "tableFrom": "email_tracking_rule", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_tracking_rule_environment_category_unique": { + "name": "email_tracking_rule_environment_category_unique", + "nullsNotDistinct": true, + "columns": [ + "environment_id", + "category" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "environment_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "personal_keys": { + "name": "personal_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_organization_idx": { + "name": "environment_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_project_fk": { + "name": "environment_project_fk", + "tableFrom": "environment", + "tableTo": "project", + "columnsFrom": [ + "project_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_project_slug_unique": { + "name": "environment_project_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "slug" + ] + }, + "environment_id_organization_unique": { + "name": "environment_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_api_key": { + "name": "environment_api_key", + "schema": "", + "columns": { + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access": { + "name": "access", + "type": "api_key_access", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_api_key_environment_idx": { + "name": "environment_api_key_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_api_key_owner_idx": { + "name": "environment_api_key_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_api_key_api_key_id_apikey_id_fk": { + "name": "environment_api_key_api_key_id_apikey_id_fk", + "tableFrom": "environment_api_key", + "tableTo": "apikey", + "columnsFrom": [ + "api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_api_key_environment_id_environment_id_fk": { + "name": "environment_api_key_environment_id_environment_id_fk", + "tableFrom": "environment_api_key", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_api_key_created_by_user_id_user_id_fk": { + "name": "environment_api_key_created_by_user_id_user_id_fk", + "tableFrom": "environment_api_key", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_api_key_owner_user_id_user_id_fk": { + "name": "environment_api_key_owner_user_id_user_id_fk", + "tableFrom": "environment_api_key", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_inbound_address": { + "name": "environment_inbound_address", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_inbound_address_environment_fk": { + "name": "environment_inbound_address_environment_fk", + "tableFrom": "environment_inbound_address", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_inbound_address_label_unique": { + "name": "environment_inbound_address_label_unique", + "nullsNotDistinct": false, + "columns": [ + "label" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hipaa_session_activity": { + "name": "hipaa_session_activity", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "hipaa_session_activity_session_id_session_id_fk": { + "name": "hipaa_session_activity_session_id_session_id_fk", + "tableFrom": "hipaa_session_activity", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_application": { + "name": "hook_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hook_application_environment_created_at_idx": { + "name": "hook_application_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hook_application_environment_fk": { + "name": "hook_application_environment_fk", + "tableFrom": "hook_application", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hook_application_environment_uid_unique": { + "name": "hook_application_environment_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "uid" + ] + }, + "hook_application_id_organization_unique": { + "name": "hook_application_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_attempt": { + "name": "hook_attempt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_created_at": { + "name": "message_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_excerpt": { + "name": "response_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "hook_attempt_delivery_idx": { + "name": "hook_attempt_delivery_idx", + "columns": [ + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hook_attempt_environment_idx": { + "name": "hook_attempt_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "hook_attempt_pkey": { + "name": "hook_attempt_pkey", + "columns": [ + "message_created_at", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_delivery": { + "name": "hook_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_created_at": { + "name": "message_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "hook_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_status_code": { + "name": "last_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hook_delivery_due_idx": { + "name": "hook_delivery_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"hook_delivery\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "hook_delivery_endpoint_idx": { + "name": "hook_delivery_endpoint_idx", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hook_delivery_environment_idx": { + "name": "hook_delivery_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "hook_delivery_pkey": { + "name": "hook_delivery_pkey", + "columns": [ + "message_created_at", + "id" + ] + } + }, + "uniqueConstraints": { + "hook_delivery_message_endpoint_unique": { + "name": "hook_delivery_message_endpoint_unique", + "nullsNotDistinct": false, + "columns": [ + "message_created_at", + "message_id", + "endpoint_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_delivery_rollup": { + "name": "hook_delivery_rollup", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hour": { + "name": "hour", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "succeeded": { + "name": "succeeded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed": { + "name": "failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "hook_delivery_rollup_hour_idx": { + "name": "hook_delivery_rollup_hour_idx", + "columns": [ + { + "expression": "hour", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hook_delivery_rollup_organization_id_organization_id_fk": { + "name": "hook_delivery_rollup_organization_id_organization_id_fk", + "tableFrom": "hook_delivery_rollup", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "hook_delivery_rollup_pkey": { + "name": "hook_delivery_rollup_pkey", + "columns": [ + "organization_id", + "hour" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_endpoint": { + "name": "hook_endpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filter_types": { + "name": "filter_types", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "secret_ciphertext": { + "name": "secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previous_secret_ciphertext": { + "name": "previous_secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_secret_expires_at": { + "name": "previous_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rate_limit_per_second": { + "name": "rate_limit_per_second", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failing_since": { + "name": "failing_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hook_endpoint_application_idx": { + "name": "hook_endpoint_application_idx", + "columns": [ + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hook_endpoint_application_fk": { + "name": "hook_endpoint_application_fk", + "tableFrom": "hook_endpoint", + "tableTo": "hook_application", + "columnsFrom": [ + "application_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hook_endpoint_id_organization_unique": { + "name": "hook_endpoint_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "hook_endpoint_previous_secret_check": { + "name": "hook_endpoint_previous_secret_check", + "value": "(\"hook_endpoint\".\"previous_secret_ciphertext\" is null) = (\"hook_endpoint\".\"previous_secret_expires_at\" is null)" + }, + "hook_endpoint_disabled_check": { + "name": "hook_endpoint_disabled_check", + "value": "\"hook_endpoint\".\"enabled\" = (\"hook_endpoint\".\"disabled_reason\" is null) and (\"hook_endpoint\".\"disabled_reason\" is null) = (\"hook_endpoint\".\"disabled_at\" is null)" + }, + "hook_endpoint_rate_limit_check": { + "name": "hook_endpoint_rate_limit_check", + "value": "\"hook_endpoint\".\"rate_limit_per_second\" is null or \"hook_endpoint\".\"rate_limit_per_second\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.hook_endpoint_recovery": { + "name": "hook_endpoint_recovery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "since": { + "name": "since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "until": { + "name": "until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "hook_recovery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor_created_at": { + "name": "cursor_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cursor_id": { + "name": "cursor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requeued": { + "name": "requeued", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hook_endpoint_recovery_pending_idx": { + "name": "hook_endpoint_recovery_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"hook_endpoint_recovery\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "hook_endpoint_recovery_endpoint_idx": { + "name": "hook_endpoint_recovery_endpoint_idx", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hook_endpoint_recovery_endpoint_fk": { + "name": "hook_endpoint_recovery_endpoint_fk", + "tableFrom": "hook_endpoint_recovery", + "tableTo": "hook_endpoint", + "columnsFrom": [ + "endpoint_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_event_type": { + "name": "hook_event_type", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "examples": { + "name": "examples", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "hook_event_type_environment_fk": { + "name": "hook_event_type_environment_fk", + "tableFrom": "hook_event_type", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hook_event_type_environment_name_unique": { + "name": "hook_event_type_environment_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_idempotency_key": { + "name": "hook_idempotency_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_created_at": { + "name": "message_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hook_idempotency_key_created_at_idx": { + "name": "hook_idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hook_idempotency_key_environment_fk": { + "name": "hook_idempotency_key_environment_fk", + "tableFrom": "hook_idempotency_key", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hook_idempotency_key_environment_key_unique": { + "name": "hook_idempotency_key_environment_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_message": { + "name": "hook_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "payload_bytes": { + "name": "payload_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "only_endpoint_id": { + "name": "only_endpoint_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "hook_message_application_created_at_idx": { + "name": "hook_message_application_created_at_idx", + "columns": [ + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hook_message_environment_created_at_idx": { + "name": "hook_message_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "hook_message_pkey": { + "name": "hook_message_pkey", + "columns": [ + "created_at", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_portal_session": { + "name": "hook_portal_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_only": { + "name": "read_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hook_portal_session_expires_at_idx": { + "name": "hook_portal_session_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hook_portal_session_organization_idx": { + "name": "hook_portal_session_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hook_portal_session_created_by_user_id_user_id_fk": { + "name": "hook_portal_session_created_by_user_id_user_id_fk", + "tableFrom": "hook_portal_session", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "hook_portal_session_application_fk": { + "name": "hook_portal_session_application_fk", + "tableFrom": "hook_portal_session", + "tableTo": "hook_application", + "columnsFrom": [ + "application_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hook_portal_session_token_hash_unique": { + "name": "hook_portal_session_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_portal_settings": { + "name": "hook_portal_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_origins": { + "name": "allowed_origins", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "hook_portal_settings_environment_fk": { + "name": "hook_portal_settings_environment_fk", + "tableFrom": "hook_portal_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hook_portal_settings_environment_unique": { + "name": "hook_portal_settings_environment_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbound_drop_counter": { + "name": "inbound_drop_counter", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "inbound_drop_counter_organization_id_organization_id_fk": { + "name": "inbound_drop_counter_organization_id_organization_id_fk", + "tableFrom": "inbound_drop_counter", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "inbound_drop_counter_pk": { + "name": "inbound_drop_counter_pk", + "columns": [ + "organization_id", + "day" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbound_fake_object": { + "name": "inbound_fake_object", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbound_fake_object_organization_idx": { + "name": "inbound_fake_object_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbound_fake_object_created_at_idx": { + "name": "inbound_fake_object_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_drain": { + "name": "log_drain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sources": { + "name": "sources", + "type": "log_drain_source[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'{}'::log_drain_source[]" + }, + "secret_ciphertext": { + "name": "secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "log_drain_format", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ndjson'" + }, + "auth_header_name": { + "name": "auth_header_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_header_ciphertext": { + "name": "auth_header_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "log_drain_environment_idx": { + "name": "log_drain_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"log_drain\".\"enabled\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "log_drain_created_by_user_id_user_id_fk": { + "name": "log_drain_created_by_user_id_user_id_fk", + "tableFrom": "log_drain", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "log_drain_environment_fk": { + "name": "log_drain_environment_fk", + "tableFrom": "log_drain", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "log_drain_id_organization_unique": { + "name": "log_drain_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "log_drain_auth_header_check": { + "name": "log_drain_auth_header_check", + "value": "(\"log_drain\".\"auth_header_name\" is null) = (\"log_drain\".\"auth_header_ciphertext\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.log_drain_attempt": { + "name": "log_drain_attempt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "batch_id": { + "name": "batch_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_excerpt": { + "name": "response_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "log_drain_attempt_batch_idx": { + "name": "log_drain_attempt_batch_idx", + "columns": [ + { + "expression": "batch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "log_drain_attempt_batch_fk": { + "name": "log_drain_attempt_batch_fk", + "tableFrom": "log_drain_attempt", + "tableTo": "log_drain_batch", + "columnsFrom": [ + "batch_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_drain_batch": { + "name": "log_drain_batch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "log_drain_batch_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "record_count": { + "name": "record_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_status_code": { + "name": "last_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "log_drain_batch_pending_unique": { + "name": "log_drain_batch_pending_unique", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"log_drain_batch\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_drain_batch_due_idx": { + "name": "log_drain_batch_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"log_drain_batch\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_drain_batch_drain_created_at_idx": { + "name": "log_drain_batch_drain_created_at_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_drain_batch_updated_at_idx": { + "name": "log_drain_batch_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"log_drain_batch\".\"status\" <> 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "log_drain_batch_drain_fk": { + "name": "log_drain_batch_drain_fk", + "tableFrom": "log_drain_batch", + "tableTo": "log_drain", + "columnsFrom": [ + "drain_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "log_drain_batch_id_organization_unique": { + "name": "log_drain_batch_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_drain_record": { + "name": "log_drain_record", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "batch_id": { + "name": "batch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "log_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "log_drain_record_queued_idx": { + "name": "log_drain_record_queued_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"log_drain_record\".\"batch_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_drain_record_batch_idx": { + "name": "log_drain_record_batch_idx", + "columns": [ + { + "expression": "batch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "log_drain_record_drain_fk": { + "name": "log_drain_record_drain_fk", + "tableFrom": "log_drain_record", + "tableTo": "log_drain", + "columnsFrom": [ + "drain_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "log_drain_record_batch_fk": { + "name": "log_drain_record_batch_fk", + "tableFrom": "log_drain_record", + "tableTo": "log_drain_batch", + "columnsFrom": [ + "batch_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "log_drain_record_drain_source_unique": { + "name": "log_drain_record_drain_source_unique", + "nullsNotDistinct": false, + "columns": [ + "drain_id", + "source_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_entry": { + "name": "log_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "log_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_ip": { + "name": "client_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recipient": { + "name": "recipient", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "search_text": { + "name": "search_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": { + "log_entry_environment_occurred_at_idx": { + "name": "log_entry_environment_occurred_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_entry_environment_request_idx": { + "name": "log_entry_environment_request_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"log_entry\".\"request_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_entry_environment_resource_idx": { + "name": "log_entry_environment_resource_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"log_entry\".\"resource_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_entry_search_text_idx": { + "name": "log_entry_search_text_idx", + "columns": [ + { + "expression": "search_text", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "log_entry_environment_fk": { + "name": "log_entry_environment_fk", + "tableFrom": "log_entry", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "log_entry_pkey": { + "name": "log_entry_pkey", + "columns": [ + "occurred_at", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketing_settings": { + "name": "marketing_settings", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mailing_address": { + "name": "mailing_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmation_from": { + "name": "confirmation_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmation_subject": { + "name": "confirmation_subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmation_html": { + "name": "confirmation_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmation_text": { + "name": "confirmation_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmed_redirect_url": { + "name": "confirmed_redirect_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "double_opt_in_default": { + "name": "double_opt_in_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "marketing_settings_environment_fk": { + "name": "marketing_settings_environment_fk", + "tableFrom": "marketing_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration": { + "name": "migration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "migration_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "migration_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'inspecting'" + }, + "credentials_ciphertext": { + "name": "credentials_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_hint": { + "name": "credential_hint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "inventory": { + "name": "inventory", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "selection": { + "name": "selection", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor": { + "name": "cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "progress": { + "name": "progress", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "migration_environment_created_at_idx": { + "name": "migration_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "migration_due_idx": { + "name": "migration_due_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"migration\".\"status\" in ('inspecting', 'queued', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "migration_created_by_user_id_user_id_fk": { + "name": "migration_created_by_user_id_user_id_fk", + "tableFrom": "migration", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "migration_environment_fk": { + "name": "migration_environment_fk", + "tableFrom": "migration", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "migration_id_organization_unique": { + "name": "migration_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_item": { + "name": "migration_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "migration_id": { + "name": "migration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "migration_item_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_name": { + "name": "source_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_name": { + "name": "target_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "migration_item_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "needs_review": { + "name": "needs_review", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "warnings": { + "name": "warnings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "migration_item_migration_fk": { + "name": "migration_item_migration_fk", + "tableFrom": "migration_item", + "tableTo": "migration", + "columnsFrom": [ + "migration_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "migration_item_source_unique": { + "name": "migration_item_source_unique", + "nullsNotDistinct": false, + "columns": [ + "migration_id", + "kind", + "source_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_audit_event": { + "name": "org_audit_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "org_audit_event_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "org_audit_actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_label": { + "name": "actor_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_label": { + "name": "target_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "client_ip": { + "name": "client_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_audit_event_organization_idx": { + "name": "org_audit_event_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_audit_event_organization_action_idx": { + "name": "org_audit_event_organization_action_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_audit_event_organization_id_organization_id_fk": { + "name": "org_audit_event_organization_id_organization_id_fk", + "tableFrom": "org_audit_event", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_api_version": { + "name": "organization_api_version", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_api_version_organization_id_organization_id_fk": { + "name": "organization_api_version_organization_id_organization_id_fk", + "tableFrom": "organization_api_version", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_api_version_updated_by_user_id_user_id_fk": { + "name": "organization_api_version_updated_by_user_id_user_id_fk", + "tableFrom": "organization_api_version", + "tableTo": "user", + "columnsFrom": [ + "updated_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_deletion": { + "name": "organization_deletion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_slug": { + "name": "organization_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_email": { + "name": "requested_by_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "by_platform_admin": { + "name": "by_platform_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "organization_deletion_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_deletion_next_attempt_idx": { + "name": "organization_deletion_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"organization_deletion\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_deletion_org_unique": { + "name": "organization_deletion_org_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_hipaa_mode": { + "name": "organization_hipaa_mode", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "baa_signed_at": { + "name": "baa_signed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "baa_reference": { + "name": "baa_reference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled_by_user_id": { + "name": "enabled_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_hipaa_mode_organization_id_organization_id_fk": { + "name": "organization_hipaa_mode_organization_id_organization_id_fk", + "tableFrom": "organization_hipaa_mode", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_hipaa_mode_enabled_by_user_id_user_id_fk": { + "name": "organization_hipaa_mode_enabled_by_user_id_user_id_fk", + "tableFrom": "organization_hipaa_mode", + "tableTo": "user", + "columnsFrom": [ + "enabled_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_logo": { + "name": "organization_logo", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "organization_logo_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "failures": { + "name": "failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_logo_organization_id_organization_id_fk": { + "name": "organization_logo_organization_id_organization_id_fk", + "tableFrom": "organization_logo", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_suspension": { + "name": "organization_suspension", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_by_user_id": { + "name": "suspended_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_suspension_organization_id_organization_id_fk": { + "name": "organization_suspension_organization_id_organization_id_fk", + "tableFrom": "organization_suspension", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_suspension_suspended_by_user_id_user_id_fk": { + "name": "organization_suspension_suspended_by_user_id_user_id_fk", + "tableFrom": "organization_suspension", + "tableTo": "user", + "columnsFrom": [ + "suspended_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.product_preference": { + "name": "product_preference", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "product_preference_organization_id_organization_id_fk": { + "name": "product_preference_organization_id_organization_id_fk", + "tableFrom": "product_preference", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "product_preference_user_id_user_id_fk": { + "name": "product_preference_user_id_user_id_fk", + "tableFrom": "product_preference", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "product_preference_member_unique": { + "name": "product_preference_member_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "user_id", + "product" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_organization_id_organization_id_fk": { + "name": "project_organization_id_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_organization_slug_unique": { + "name": "project_organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + }, + "project_id_organization_unique": { + "name": "project_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_override": { + "name": "rate_limit_override", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "limit": { + "name": "limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "period_ms": { + "name": "period_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "burst": { + "name": "burst", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "rate_limit_override_organization_id_organization_id_fk": { + "name": "rate_limit_override_organization_id_organization_id_fk", + "tableFrom": "rate_limit_override", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "rate_limit_override_organization_bucket_unique": { + "name": "rate_limit_override_organization_bucket_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "bucket" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.received_email": { + "name": "received_email", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "received_email_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mail_from": { + "name": "mail_from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipients": { + "name": "recipients", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "from": { + "name": "from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to": { + "name": "to", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "cc": { + "name": "cc", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "reply_to": { + "name": "reply_to", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "references": { + "name": "references", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "html": { + "name": "html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "spam_verdict": { + "name": "spam_verdict", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "virus_verdict": { + "name": "virus_verdict", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spf_verdict": { + "name": "spf_verdict", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dkim_verdict": { + "name": "dkim_verdict", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dmarc_verdict": { + "name": "dmarc_verdict", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dmarc_policy": { + "name": "dmarc_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "raw_key": { + "name": "raw_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "received_email_environment_id_idx": { + "name": "received_email_environment_id_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "received_email_created_at_idx": { + "name": "received_email_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "received_email_organization_idx": { + "name": "received_email_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "received_email_environment_fk": { + "name": "received_email_environment_fk", + "tableFrom": "received_email", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "received_email_provider_message_unique": { + "name": "received_email_provider_message_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "provider", + "provider_message_id" + ] + }, + "received_email_id_organization_unique": { + "name": "received_email_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.received_email_attachment": { + "name": "received_email_attachment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "received_email_id": { + "name": "received_email_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_key": { + "name": "content_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "received_email_attachment_email_fk": { + "name": "received_email_attachment_email_fk", + "tableFrom": "received_email_attachment", + "tableTo": "received_email", + "columnsFrom": [ + "received_email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "received_email_attachment_position_unique": { + "name": "received_email_attachment_position_unique", + "nullsNotDistinct": false, + "columns": [ + "received_email_id", + "position" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.receiving_domain": { + "name": "receiving_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_id": { + "name": "domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record": { + "name": "record", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "receiving_domain_environment_idx": { + "name": "receiving_domain_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "receiving_domain_next_check_at_idx": { + "name": "receiving_domain_next_check_at_idx", + "columns": [ + { + "expression": "next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"receiving_domain\".\"next_check_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "receiving_domain_domain_fk": { + "name": "receiving_domain_domain_fk", + "tableFrom": "receiving_domain", + "tableTo": "domain", + "columnsFrom": [ + "domain_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "receiving_domain_environment_fk": { + "name": "receiving_domain_environment_fk", + "tableFrom": "receiving_domain", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "receiving_domain_domain_unique": { + "name": "receiving_domain_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain_id" + ] + }, + "receiving_domain_hostname_unique": { + "name": "receiving_domain_hostname_unique", + "nullsNotDistinct": false, + "columns": [ + "hostname" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reputation_event": { + "name": "reputation_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "reputation_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor": { + "name": "actor", + "type": "reputation_actor", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reputation_event_organization_created_at_idx": { + "name": "reputation_event_organization_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reputation_event_organization_id_organization_id_fk": { + "name": "reputation_event_organization_id_organization_id_fk", + "tableFrom": "reputation_event", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reputation_event_actor_user_id_user_id_fk": { + "name": "reputation_event_actor_user_id_user_id_fk", + "tableFrom": "reputation_event", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "reputation_event_id_organization_unique": { + "name": "reputation_event_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reputation_notification": { + "name": "reputation_notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reputation_event_id": { + "name": "reputation_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "reputation_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "reputation_notification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "recipients": { + "name": "recipients", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reputation_notification_event_idx": { + "name": "reputation_notification_event_idx", + "columns": [ + { + "expression": "reputation_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reputation_notification_next_attempt_idx": { + "name": "reputation_notification_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"reputation_notification\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reputation_notification_organization_id_organization_id_fk": { + "name": "reputation_notification_organization_id_organization_id_fk", + "tableFrom": "reputation_notification", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reputation_notification_event_fk": { + "name": "reputation_notification_event_fk", + "tableFrom": "reputation_notification", + "tableTo": "reputation_event", + "columnsFrom": [ + "reputation_event_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reputation_standing": { + "name": "reputation_standing", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "reputation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'good'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reinstated_at": { + "name": "reinstated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "marketing_paused_at": { + "name": "marketing_paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "marketing_reason": { + "name": "marketing_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "marketing_reinstated_at": { + "name": "marketing_reinstated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "policy_overrides": { + "name": "policy_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "evaluation_lease_until": { + "name": "evaluation_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_evaluation_at": { + "name": "next_evaluation_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reputation_standing_next_evaluation_idx": { + "name": "reputation_standing_next_evaluation_idx", + "columns": [ + { + "expression": "next_evaluation_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"reputation_standing\".\"next_evaluation_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reputation_standing_organization_id_organization_id_fk": { + "name": "reputation_standing_organization_id_organization_id_fk", + "tableFrom": "reputation_standing", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_assessment": { + "name": "risk_assessment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "risk_assessment_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "risk_event", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "event_name": { + "name": "event_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_hash": { + "name": "email_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_hash": { + "name": "ip_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subnet_hash": { + "name": "subnet_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "card_hash": { + "name": "card_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device_hash": { + "name": "device_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "risk_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "signals": { + "name": "signals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "degraded": { + "name": "degraded", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "outcome": { + "name": "outcome", + "type": "risk_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "outcome_at": { + "name": "outcome_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_status": { + "name": "review_status", + "type": "risk_review_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "personal_data_purged_at": { + "name": "personal_data_purged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_assessment_environment_sequence_idx": { + "name": "risk_assessment_environment_sequence_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_assessment_environment_user_idx": { + "name": "risk_assessment_environment_user_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"risk_assessment\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_assessment_open_review_idx": { + "name": "risk_assessment_open_review_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"risk_assessment\".\"review_status\" = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_assessment_created_at_idx": { + "name": "risk_assessment_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_assessment_personal_data_idx": { + "name": "risk_assessment_personal_data_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"risk_assessment\".\"personal_data_purged_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_assessment_environment_fk": { + "name": "risk_assessment_environment_fk", + "tableFrom": "risk_assessment", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "risk_assessment_id_organization_unique": { + "name": "risk_assessment_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "risk_assessment_environment_idempotency_key_unique": { + "name": "risk_assessment_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_assessment_preparation": { + "name": "risk_assessment_preparation", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "risk_assessment_preparation_created_idx": { + "name": "risk_assessment_preparation_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_assessment_preparation_environment_id_environment_id_fk": { + "name": "risk_assessment_preparation_environment_id_environment_id_fk", + "tableFrom": "risk_assessment_preparation", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "risk_assessment_preparation_environment_id_idempotency_key_pk": { + "name": "risk_assessment_preparation_environment_id_idempotency_key_pk", + "columns": [ + "environment_id", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_dataset": { + "name": "risk_dataset", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_domain_age": { + "name": "risk_domain_age", + "schema": "", + "columns": { + "domain": { + "name": "domain", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_feedback": { + "name": "risk_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assessment_id": { + "name": "assessment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "risk_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "risk_feedback_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_feedback_assessment_idx": { + "name": "risk_feedback_assessment_idx", + "columns": [ + { + "expression": "assessment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_feedback_assessment_fk": { + "name": "risk_feedback_assessment_fk", + "tableFrom": "risk_feedback", + "tableTo": "risk_assessment", + "columnsFrom": [ + "assessment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_identity": { + "name": "risk_identity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "risk_identity_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "value_hash": { + "name": "value_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assessment_id": { + "name": "assessment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_identity_environment_user_idx": { + "name": "risk_identity_environment_user_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"risk_identity\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_identity_last_seen_idx": { + "name": "risk_identity_last_seen_idx", + "columns": [ + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_identity_environment_fk": { + "name": "risk_identity_environment_fk", + "tableFrom": "risk_identity", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "risk_identity_value_user_unique": { + "name": "risk_identity_value_user_unique", + "nullsNotDistinct": true, + "columns": [ + "environment_id", + "kind", + "value_hash", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_limit_bucket": { + "name": "risk_limit_bucket", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "limit": { + "name": "limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "period_ms": { + "name": "period_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "burst": { + "name": "burst", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "on_failure": { + "name": "on_failure", + "type": "risk_limit_failure_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'allow'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "risk_limit_bucket_environment_fk": { + "name": "risk_limit_bucket_environment_fk", + "tableFrom": "risk_limit_bucket", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "risk_limit_bucket_name_unique": { + "name": "risk_limit_bucket_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": { + "risk_limit_bucket_values_check": { + "name": "risk_limit_bucket_values_check", + "value": "\"risk_limit_bucket\".\"limit\" > 0 and \"risk_limit_bucket\".\"period_ms\" > 0 and \"risk_limit_bucket\".\"burst\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.risk_list_entry": { + "name": "risk_list_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "risk_list_entry_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "list": { + "name": "list", + "type": "risk_list", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "risk_list_entry_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "network": { + "name": "network", + "type": "cidr", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_list_entry_environment_sequence_idx": { + "name": "risk_list_entry_environment_sequence_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_list_entry_network_idx": { + "name": "risk_list_entry_network_idx", + "columns": [ + { + "expression": "network", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "inet_ops" + } + ], + "isUnique": false, + "where": "\"risk_list_entry\".\"network\" is not null", + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": { + "risk_list_entry_environment_fk": { + "name": "risk_list_entry_environment_fk", + "tableFrom": "risk_list_entry", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "risk_list_entry_value_unique": { + "name": "risk_list_entry_value_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "list", + "type", + "value" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_report": { + "name": "risk_report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "risk_report_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "value_hash": { + "name": "value_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assessment_id": { + "name": "assessment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_at": { + "name": "reported_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_report_assessment_idx": { + "name": "risk_report_assessment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assessment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_report_reported_at_idx": { + "name": "risk_report_reported_at_idx", + "columns": [ + { + "expression": "reported_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_report_environment_fk": { + "name": "risk_report_environment_fk", + "tableFrom": "risk_report", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "risk_report_value_unique": { + "name": "risk_report_value_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "kind", + "value_hash", + "assessment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_rule": { + "name": "risk_rule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "events": { + "name": "events", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "match": { + "name": "match", + "type": "risk_rule_match", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "risk_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_rule_environment_position_idx": { + "name": "risk_rule_environment_position_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_rule_environment_fk": { + "name": "risk_rule_environment_fk", + "tableFrom": "risk_rule", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_settings": { + "name": "risk_settings", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "review_threshold": { + "name": "review_threshold", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "block_threshold": { + "name": "block_threshold", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "disabled_reasons": { + "name": "disabled_reasons", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "captcha_provider": { + "name": "captcha_provider", + "type": "risk_captcha_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "captcha_secret_ciphertext": { + "name": "captcha_secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "captcha_required": { + "name": "captcha_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "content_thresholds": { + "name": "content_thresholds", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "reason_weights": { + "name": "reason_weights", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "risk_settings_environment_fk": { + "name": "risk_settings_environment_fk", + "tableFrom": "risk_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "risk_settings_thresholds_check": { + "name": "risk_settings_thresholds_check", + "value": "\"risk_settings\".\"review_threshold\" > 0 and \"risk_settings\".\"review_threshold\" <= \"risk_settings\".\"block_threshold\" and \"risk_settings\".\"block_threshold\" <= 1" + }, + "risk_settings_captcha_check": { + "name": "risk_settings_captcha_check", + "value": "(\"risk_settings\".\"captcha_provider\" is null) = (\"risk_settings\".\"captcha_secret_ciphertext\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.risk_user_network": { + "name": "risk_user_network", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_hash": { + "name": "user_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "risk_user_network_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_user_network_last_seen_idx": { + "name": "risk_user_network_last_seen_idx", + "columns": [ + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_user_network_environment_fk": { + "name": "risk_user_network_environment_fk", + "tableFrom": "risk_user_network", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "risk_user_network_value_unique": { + "name": "risk_user_network_value_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "user_hash", + "kind", + "value" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group_role": { + "name": "scim_group_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scim_group_role_organization_id_organization_id_fk": { + "name": "scim_group_role_organization_id_organization_id_fk", + "tableFrom": "scim_group_role", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_group_role_organization_group_unique": { + "name": "scim_group_role_organization_group_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "group_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.segment": { + "name": "segment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filter": { + "name": "filter", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "segment_environment_fk": { + "name": "segment_environment_fk", + "tableFrom": "segment", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "segment_environment_name_unique": { + "name": "segment_environment_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.topic": { + "name": "topic", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_subscription": { + "name": "default_subscription", + "type": "topic_default_subscription", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "topic_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "topic_environment_fk": { + "name": "topic_environment_fk", + "tableFrom": "topic", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "topic_environment_key_unique": { + "name": "topic_environment_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "key" + ] + }, + "topic_id_organization_unique": { + "name": "topic_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tracking_domain": { + "name": "tracking_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_id": { + "name": "domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "tracking_domain_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_hostname_id": { + "name": "provider_hostname_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "record": { + "name": "record", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_message": { + "name": "provider_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verification_started_at": { + "name": "verification_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failed_at": { + "name": "failed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tracking_domain_hostname_active_unique": { + "name": "tracking_domain_hostname_active_unique", + "columns": [ + { + "expression": "hostname", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"tracking_domain\".\"status\" <> 'failed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "tracking_domain_next_check_at_idx": { + "name": "tracking_domain_next_check_at_idx", + "columns": [ + { + "expression": "next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"tracking_domain\".\"next_check_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tracking_domain_domain_fk": { + "name": "tracking_domain_domain_fk", + "tableFrom": "tracking_domain", + "tableTo": "domain", + "columnsFrom": [ + "domain_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tracking_domain_domain_unique": { + "name": "tracking_domain_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_attempt": { + "name": "webhook_attempt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_excerpt": { + "name": "response_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webhook_attempt_delivery_idx": { + "name": "webhook_attempt_delivery_idx", + "columns": [ + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_attempt_delivery_fk": { + "name": "webhook_attempt_delivery_fk", + "tableFrom": "webhook_attempt", + "tableTo": "webhook_delivery", + "columnsFrom": [ + "delivery_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_delivery": { + "name": "webhook_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "webhook_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_status_code": { + "name": "last_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_delivery_due_idx": { + "name": "webhook_delivery_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook_delivery\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_delivery_endpoint_created_at_idx": { + "name": "webhook_delivery_endpoint_created_at_idx", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_delivery_message_fk": { + "name": "webhook_delivery_message_fk", + "tableFrom": "webhook_delivery", + "tableTo": "webhook_message", + "columnsFrom": [ + "message_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_delivery_endpoint_fk": { + "name": "webhook_delivery_endpoint_fk", + "tableFrom": "webhook_delivery", + "tableTo": "webhook_endpoint", + "columnsFrom": [ + "endpoint_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "webhook_delivery_message_endpoint_unique": { + "name": "webhook_delivery_message_endpoint_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id", + "endpoint_id" + ] + }, + "webhook_delivery_id_organization_unique": { + "name": "webhook_delivery_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_endpoint": { + "name": "webhook_endpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_version": { + "name": "api_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "secret_ciphertext": { + "name": "secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previous_secret_ciphertext": { + "name": "previous_secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_secret_expires_at": { + "name": "previous_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failing_since": { + "name": "failing_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_endpoint_environment_idx": { + "name": "webhook_endpoint_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_endpoint_created_by_user_id_user_id_fk": { + "name": "webhook_endpoint_created_by_user_id_user_id_fk", + "tableFrom": "webhook_endpoint", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "webhook_endpoint_environment_fk": { + "name": "webhook_endpoint_environment_fk", + "tableFrom": "webhook_endpoint", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "webhook_endpoint_id_organization_unique": { + "name": "webhook_endpoint_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "webhook_endpoint_previous_secret_check": { + "name": "webhook_endpoint_previous_secret_check", + "value": "(\"webhook_endpoint\".\"previous_secret_ciphertext\" is null) = (\"webhook_endpoint\".\"previous_secret_expires_at\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.webhook_listener": { + "name": "webhook_listener", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_api_key_id": { + "name": "created_by_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_listener_environment_expires_idx": { + "name": "webhook_listener_environment_expires_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_listener_expires_at_idx": { + "name": "webhook_listener_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_listener_environment_fk": { + "name": "webhook_listener_environment_fk", + "tableFrom": "webhook_listener", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "webhook_listener_id_organization_unique": { + "name": "webhook_listener_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_listener_delivery": { + "name": "webhook_listener_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "listener_id": { + "name": "listener_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "webhook_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "handed_out_count": { + "name": "handed_out_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forwarded_at": { + "name": "forwarded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_listener_delivery_pending_idx": { + "name": "webhook_listener_delivery_pending_idx", + "columns": [ + { + "expression": "listener_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook_listener_delivery\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_listener_delivery_listener_idx": { + "name": "webhook_listener_delivery_listener_idx", + "columns": [ + { + "expression": "listener_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_listener_delivery_listener_fk": { + "name": "webhook_listener_delivery_listener_fk", + "tableFrom": "webhook_listener_delivery", + "tableTo": "webhook_listener", + "columnsFrom": [ + "listener_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_listener_delivery_message_fk": { + "name": "webhook_listener_delivery_message_fk", + "tableFrom": "webhook_listener_delivery", + "tableTo": "webhook_message", + "columnsFrom": [ + "message_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "webhook_listener_delivery_message_listener_unique": { + "name": "webhook_listener_delivery_message_listener_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id", + "listener_id" + ] + }, + "webhook_listener_delivery_id_organization_unique": { + "name": "webhook_listener_delivery_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_message": { + "name": "webhook_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_message_environment_idx": { + "name": "webhook_message_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_message_created_at_idx": { + "name": "webhook_message_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_message_environment_fk": { + "name": "webhook_message_environment_fk", + "tableFrom": "webhook_message", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "webhook_message_id_organization_unique": { + "name": "webhook_message_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form": { + "name": "form", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "strict_fields": { + "name": "strict_fields", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allowed_origins": { + "name": "allowed_origins", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "honeypot_field": { + "name": "honeypot_field", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'_gotcha'" + }, + "captcha": { + "name": "captcha", + "type": "form_captcha", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'off'" + }, + "captcha_site_key": { + "name": "captcha_site_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "on_review": { + "name": "on_review", + "type": "form_on_review", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'hold'" + }, + "content_threshold": { + "name": "content_threshold", + "type": "double precision", + "primaryKey": false, + "notNull": false, + "default": 0.9 + }, + "email_field": { + "name": "email_field", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'email'" + }, + "verify_submitter": { + "name": "verify_submitter", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notify_recipients": { + "name": "notify_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "notification_from": { + "name": "notification_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmation_from": { + "name": "confirmation_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hosted": { + "name": "hosted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hosted_published_at": { + "name": "hosted_published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "disabled_by_admin_at": { + "name": "disabled_by_admin_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_environment_created_at_idx": { + "name": "form_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_environment_fk": { + "name": "form_environment_fk", + "tableFrom": "form", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "form_id_organization_unique": { + "name": "form_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_delivery": { + "name": "form_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "submission_id": { + "name": "submission_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "form_delivery_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "form_delivery_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_delivery_due_idx": { + "name": "form_delivery_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"form_delivery\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_delivery_submission_fk": { + "name": "form_delivery_submission_fk", + "tableFrom": "form_delivery", + "tableTo": "form_submission", + "columnsFrom": [ + "submission_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "form_delivery_submission_kind_unique": { + "name": "form_delivery_submission_kind_unique", + "nullsNotDistinct": false, + "columns": [ + "submission_id", + "kind" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_fake_object": { + "name": "form_fake_object", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_fake_object_organization_idx": { + "name": "form_fake_object_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_store_cleanup": { + "name": "form_store_cleanup", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_store_cleanup_next_attempt_at_idx": { + "name": "form_store_cleanup_next_attempt_at_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_store_cleanup_organization_idx": { + "name": "form_store_cleanup_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_submission": { + "name": "form_submission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "form_id": { + "name": "form_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "form_submission_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "submitter_email": { + "name": "submitter_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "referrer": { + "name": "referrer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_assessment_id": { + "name": "risk_assessment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_decision": { + "name": "risk_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_score": { + "name": "risk_score", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "spam_reason": { + "name": "spam_reason", + "type": "form_spam_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "confirmation_token_hash": { + "name": "confirmation_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmation_expires_at": { + "name": "confirmation_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_by": { + "name": "released_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_submission_form_created_at_idx": { + "name": "form_submission_form_created_at_idx", + "columns": [ + { + "expression": "form_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_submission_created_at_idx": { + "name": "form_submission_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_submission_environment_idx": { + "name": "form_submission_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_submission_form_fk": { + "name": "form_submission_form_fk", + "tableFrom": "form_submission", + "tableTo": "form", + "columnsFrom": [ + "form_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "form_submission_confirmation_token_unique": { + "name": "form_submission_confirmation_token_unique", + "nullsNotDistinct": false, + "columns": [ + "confirmation_token_hash" + ] + }, + "form_submission_id_organization_unique": { + "name": "form_submission_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_submission_file": { + "name": "form_submission_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "submission_id": { + "name": "submission_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field": { + "name": "field", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "store_key": { + "name": "store_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_submission_file_submission_idx": { + "name": "form_submission_file_submission_idx", + "columns": [ + { + "expression": "submission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_submission_file_submission_fk": { + "name": "form_submission_file_submission_fk", + "tableFrom": "form_submission_file", + "tableTo": "form_submission", + "columnsFrom": [ + "submission_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox": { + "name": "inbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "inbox_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fee_at": { + "name": "next_fee_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_environment_id_idx": { + "name": "inbox_environment_id_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_organization_idx": { + "name": "inbox_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_next_fee_at_idx": { + "name": "inbox_next_fee_at_idx", + "columns": [ + { + "expression": "next_fee_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"inbox\".\"next_fee_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_environment_fk": { + "name": "inbox_environment_fk", + "tableFrom": "inbox", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "inbox_environment_address_unique": { + "name": "inbox_environment_address_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "address" + ] + }, + "inbox_environment_client_id_unique": { + "name": "inbox_environment_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "client_id" + ] + }, + "inbox_id_organization_unique": { + "name": "inbox_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox_message": { + "name": "inbox_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inbox_id": { + "name": "inbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "inbox_message_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "received_email_id": { + "name": "received_email_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rfc_message_id": { + "name": "rfc_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_message_thread_idx": { + "name": "inbox_message_thread_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_message_inbox_id_idx": { + "name": "inbox_message_inbox_id_idx", + "columns": [ + { + "expression": "inbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_message_inbox_rfc_message_id_idx": { + "name": "inbox_message_inbox_rfc_message_id_idx", + "columns": [ + { + "expression": "inbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rfc_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"inbox_message\".\"rfc_message_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_message_received_email_idx": { + "name": "inbox_message_received_email_idx", + "columns": [ + { + "expression": "received_email_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_message_organization_idx": { + "name": "inbox_message_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_message_thread_fk": { + "name": "inbox_message_thread_fk", + "tableFrom": "inbox_message", + "tableTo": "inbox_thread", + "columnsFrom": [ + "thread_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inbox_message_received_email_fk": { + "name": "inbox_message_received_email_fk", + "tableFrom": "inbox_message", + "tableTo": "received_email", + "columnsFrom": [ + "received_email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inbox_message_email_fk": { + "name": "inbox_message_email_fk", + "tableFrom": "inbox_message", + "tableTo": "email", + "columnsFrom": [ + "email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "inbox_message_inbox_received_email_unique": { + "name": "inbox_message_inbox_received_email_unique", + "nullsNotDistinct": false, + "columns": [ + "inbox_id", + "received_email_id" + ] + }, + "inbox_message_email_unique": { + "name": "inbox_message_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "inbox_message_source_check": { + "name": "inbox_message_source_check", + "value": "(\"inbox_message\".\"direction\" = 'inbound' and \"inbox_message\".\"received_email_id\" is not null and \"inbox_message\".\"email_id\" is null) or (\"inbox_message\".\"direction\" = 'outbound' and \"inbox_message\".\"email_id\" is not null and \"inbox_message\".\"received_email_id\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.inbox_thread": { + "name": "inbox_thread", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inbox_id": { + "name": "inbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject_key": { + "name": "subject_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "participants": { + "name": "participants", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_thread_inbox_last_message_idx": { + "name": "inbox_thread_inbox_last_message_idx", + "columns": [ + { + "expression": "inbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_message_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_thread_inbox_subject_idx": { + "name": "inbox_thread_inbox_subject_idx", + "columns": [ + { + "expression": "inbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_thread_last_message_at_idx": { + "name": "inbox_thread_last_message_at_idx", + "columns": [ + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_thread_organization_idx": { + "name": "inbox_thread_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_thread_inbox_fk": { + "name": "inbox_thread_inbox_fk", + "tableFrom": "inbox_thread", + "tableTo": "inbox", + "columnsFrom": [ + "inbox_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "inbox_thread_id_organization_unique": { + "name": "inbox_thread_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "notification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "recipients": { + "name": "recipients", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notification_dedupe_unique": { + "name": "notification_dedupe_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_next_attempt_idx": { + "name": "notification_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"notification\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_created_at_idx": { + "name": "notification_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_organization_id_organization_id_fk": { + "name": "notification_organization_id_organization_id_fk", + "tableFrom": "notification", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.push_device": { + "name": "push_device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apns_environment": { + "name": "apns_environment", + "type": "apns_environment", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "categories": { + "name": "categories", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "push_device_session_idx": { + "name": "push_device_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_device_token_idx": { + "name": "push_device_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_device_user_idx": { + "name": "push_device_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_device_user_id_user_id_fk": { + "name": "push_device_user_id_user_id_fk", + "tableFrom": "push_device", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "push_device_session_id_session_id_fk": { + "name": "push_device_session_id_session_id_fk", + "tableFrom": "push_device", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.push_notification": { + "name": "push_notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "push_notification_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "collapse_key": { + "name": "collapse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "push_notification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivered_device_ids": { + "name": "delivered_device_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "push_notification_pending_collapse_idx": { + "name": "push_notification_pending_collapse_idx", + "columns": [ + { + "expression": "collapse_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"push_notification\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_notification_collapse_sent_idx": { + "name": "push_notification_collapse_sent_idx", + "columns": [ + { + "expression": "collapse_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sent_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"push_notification\".\"collapse_key\" IS NOT NULL AND \"push_notification\".\"sent_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_notification_next_attempt_idx": { + "name": "push_notification_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"push_notification\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_notification_created_at_idx": { + "name": "push_notification_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_notification_organization_idx": { + "name": "push_notification_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_notification_organization_id_organization_id_fk": { + "name": "push_notification_organization_id_organization_id_fk", + "tableFrom": "push_notification", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "push_notification_environment_fk": { + "name": "push_notification_environment_fk", + "tableFrom": "push_notification", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_attendee": { + "name": "scheduling_attendee", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "scheduling_attendee_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'attendee'" + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_contact": { + "name": "encrypted_contact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_hmac": { + "name": "email_hmac", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_answers": { + "name": "encrypted_answers", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time_zone": { + "name": "time_zone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redacted_at": { + "name": "redacted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_attendee_booking_idx": { + "name": "scheduling_attendee_booking_idx", + "columns": [ + { + "expression": "booking_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_attendee_environment_uid_idx": { + "name": "scheduling_attendee_environment_uid_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_attendee_environment_email_idx": { + "name": "scheduling_attendee_environment_email_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_hmac", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_attendee_booking_fk": { + "name": "scheduling_attendee_booking_fk", + "tableFrom": "scheduling_attendee", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_booking": { + "name": "scheduling_booking", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "scheduling_booking_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "buffer_before_minutes": { + "name": "buffer_before_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "buffer_after_minutes": { + "name": "buffer_after_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hold_expires_at": { + "name": "hold_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "approval_expires_at": { + "name": "approval_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "series_id": { + "name": "series_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "series_index": { + "name": "series_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "group_session_id": { + "name": "group_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "encrypted_attributes": { + "name": "encrypted_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "calendar_unverified": { + "name": "calendar_unverified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "external_conflict_at": { + "name": "external_conflict_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "no_show_at": { + "name": "no_show_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_by": { + "name": "cancelled_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_cancellation_reason": { + "name": "encrypted_cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redacted_at": { + "name": "redacted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_booking_environment_start_idx": { + "name": "scheduling_booking_environment_start_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_event_type_start_idx": { + "name": "scheduling_booking_event_type_start_idx", + "columns": [ + { + "expression": "event_type_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_series_idx": { + "name": "scheduling_booking_series_idx", + "columns": [ + { + "expression": "series_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "series_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_group_session_idx": { + "name": "scheduling_booking_group_session_idx", + "columns": [ + { + "expression": "group_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_end_idx": { + "name": "scheduling_booking_end_idx", + "columns": [ + { + "expression": "end_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_unredacted_end_idx": { + "name": "scheduling_booking_unredacted_end_idx", + "columns": [ + { + "expression": "end_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_booking\".\"redacted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_hold_expiry_idx": { + "name": "scheduling_booking_hold_expiry_idx", + "columns": [ + { + "expression": "hold_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_booking\".\"status\" = 'held'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_approval_expiry_idx": { + "name": "scheduling_booking_approval_expiry_idx", + "columns": [ + { + "expression": "approval_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_booking\".\"status\" = 'pending_approval'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_booking_environment_fk": { + "name": "scheduling_booking_environment_fk", + "tableFrom": "scheduling_booking", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_booking_event_type_fk": { + "name": "scheduling_booking_event_type_fk", + "tableFrom": "scheduling_booking", + "tableTo": "scheduling_event_type", + "columnsFrom": [ + "event_type_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_booking_series_fk": { + "name": "scheduling_booking_series_fk", + "tableFrom": "scheduling_booking", + "tableTo": "scheduling_series", + "columnsFrom": [ + "series_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "scheduling_booking_group_session_fk": { + "name": "scheduling_booking_group_session_fk", + "tableFrom": "scheduling_booking", + "tableTo": "scheduling_group_session", + "columnsFrom": [ + "group_session_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_booking_id_organization_unique": { + "name": "scheduling_booking_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "scheduling_booking_environment_idempotency_unique": { + "name": "scheduling_booking_environment_idempotency_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scheduling_booking_time_order": { + "name": "scheduling_booking_time_order", + "value": "\"scheduling_booking\".\"start_at\" < \"scheduling_booking\".\"end_at\"" + }, + "scheduling_booking_hold_has_expiry": { + "name": "scheduling_booking_hold_has_expiry", + "value": "\"scheduling_booking\".\"status\" <> 'held' OR \"scheduling_booking\".\"hold_expires_at\" IS NOT NULL" + }, + "scheduling_booking_approval_has_expiry": { + "name": "scheduling_booking_approval_has_expiry", + "value": "\"scheduling_booking\".\"status\" <> 'pending_approval' OR \"scheduling_booking\".\"approval_expires_at\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.scheduling_booking_event": { + "name": "scheduling_booking_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor": { + "name": "actor", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "encrypted_note": { + "name": "encrypted_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scheduling_booking_event_booking_fk": { + "name": "scheduling_booking_event_booking_fk", + "tableFrom": "scheduling_booking_event", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_booking_event_sequence_unique": { + "name": "scheduling_booking_event_sequence_unique", + "nullsNotDistinct": false, + "columns": [ + "booking_id", + "sequence" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_booking_host": { + "name": "scheduling_booking_host", + "schema": "", + "columns": { + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "scheduling_host_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "fair_share": { + "name": "fair_share", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scheduling_booking_host_host_idx": { + "name": "scheduling_booking_host_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_booking_host_booking_fk": { + "name": "scheduling_booking_host_booking_fk", + "tableFrom": "scheduling_booking_host", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_booking_host_host_fk": { + "name": "scheduling_booking_host_host_fk", + "tableFrom": "scheduling_booking_host", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scheduling_booking_host_booking_id_host_id_pk": { + "name": "scheduling_booking_host_booking_id_host_id_pk", + "columns": [ + "booking_id", + "host_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_busy_interval": { + "name": "scheduling_busy_interval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "calendar_id": { + "name": "calendar_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scheduling_busy_interval_host_idx": { + "name": "scheduling_busy_interval_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "end_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_busy_interval_calendar_idx": { + "name": "scheduling_busy_interval_calendar_idx", + "columns": [ + { + "expression": "calendar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_busy_interval_calendar_fk": { + "name": "scheduling_busy_interval_calendar_fk", + "tableFrom": "scheduling_busy_interval", + "tableTo": "scheduling_calendar", + "columnsFrom": [ + "calendar_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_calendar": { + "name": "scheduling_calendar", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_busy": { + "name": "read_busy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "write_target": { + "name": "write_target", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "covered_until": { + "name": "covered_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dirty_at": { + "name": "dirty_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fetch_started_at": { + "name": "last_fetch_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_failures": { + "name": "sync_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel_resource_id": { + "name": "channel_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel_token_hash": { + "name": "channel_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel_expires_at": { + "name": "channel_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_calendar_host_write_target_unique": { + "name": "scheduling_calendar_host_write_target_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"scheduling_calendar\".\"write_target\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_calendar_host_idx": { + "name": "scheduling_calendar_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_calendar_channel_idx": { + "name": "scheduling_calendar_channel_idx", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_calendar_next_sync_idx": { + "name": "scheduling_calendar_next_sync_idx", + "columns": [ + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_calendar_connection_fk": { + "name": "scheduling_calendar_connection_fk", + "tableFrom": "scheduling_calendar", + "tableTo": "scheduling_calendar_connection", + "columnsFrom": [ + "connection_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_calendar_id_organization_unique": { + "name": "scheduling_calendar_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "scheduling_calendar_connection_external_unique": { + "name": "scheduling_calendar_connection_external_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_id", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_calendar_connection": { + "name": "scheduling_calendar_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "scheduling_calendar_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_credentials": { + "name": "encrypted_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delegation_id": { + "name": "delegation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_app_id": { + "name": "oauth_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reached_at": { + "name": "reached_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "scheduling_connection_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_calendar_connection_oauth_app_idx": { + "name": "scheduling_calendar_connection_oauth_app_idx", + "columns": [ + { + "expression": "oauth_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_calendar_connection\".\"oauth_app_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_calendar_connection_delegation_idx": { + "name": "scheduling_calendar_connection_delegation_idx", + "columns": [ + { + "expression": "delegation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_calendar_connection\".\"delegation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_calendar_connection_host_fk": { + "name": "scheduling_calendar_connection_host_fk", + "tableFrom": "scheduling_calendar_connection", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_calendar_connection_delegation_fk": { + "name": "scheduling_calendar_connection_delegation_fk", + "tableFrom": "scheduling_calendar_connection", + "tableTo": "scheduling_calendar_delegation", + "columnsFrom": [ + "delegation_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_calendar_connection_oauth_app_fk": { + "name": "scheduling_calendar_connection_oauth_app_fk", + "tableFrom": "scheduling_calendar_connection", + "tableTo": "scheduling_oauth_app", + "columnsFrom": [ + "oauth_app_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_calendar_connection_id_organization_unique": { + "name": "scheduling_calendar_connection_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "scheduling_calendar_connection_account_unique": { + "name": "scheduling_calendar_connection_account_unique", + "nullsNotDistinct": false, + "columns": [ + "host_id", + "provider", + "account_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_calendar_delegation": { + "name": "scheduling_calendar_delegation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "scheduling_delegation_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_host_email": { + "name": "match_host_email", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "scheduling_delegation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "encrypted_credentials": { + "name": "encrypted_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verify_account": { + "name": "verify_account", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failing_since": { + "name": "failing_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "secret_expires_at": { + "name": "secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_reconcile_at": { + "name": "next_reconcile_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reconcile_cursor": { + "name": "reconcile_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_calendar_delegation_due_idx": { + "name": "scheduling_calendar_delegation_due_idx", + "columns": [ + { + "expression": "next_reconcile_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_calendar_delegation_environment_fk": { + "name": "scheduling_calendar_delegation_environment_fk", + "tableFrom": "scheduling_calendar_delegation", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_calendar_delegation_id_organization_unique": { + "name": "scheduling_calendar_delegation_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "scheduling_calendar_delegation_domain_unique": { + "name": "scheduling_calendar_delegation_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_calendar_event": { + "name": "scheduling_calendar_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_session_id": { + "name": "group_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "out_of_office_id": { + "name": "out_of_office_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "calendar_id": { + "name": "calendar_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "desired_start": { + "name": "desired_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "desired_end": { + "name": "desired_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "desired_state": { + "name": "desired_state", + "type": "scheduling_calendar_event_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'present'" + }, + "desired_version": { + "name": "desired_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applied_version": { + "name": "applied_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "provider_event_id": { + "name": "provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_generation": { + "name": "key_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "external_state": { + "name": "external_state", + "type": "scheduling_external_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'in_sync'" + }, + "details_written": { + "name": "details_written", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "scrub_pending": { + "name": "scrub_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_calendar_event_host_idx": { + "name": "scheduling_calendar_event_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "desired_end", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "desired_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_calendar_event_due_idx": { + "name": "scheduling_calendar_event_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_calendar_event\".\"applied_version\" < \"scheduling_calendar_event\".\"desired_version\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_calendar_event_booking_fk": { + "name": "scheduling_calendar_event_booking_fk", + "tableFrom": "scheduling_calendar_event", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_calendar_event_group_session_fk": { + "name": "scheduling_calendar_event_group_session_fk", + "tableFrom": "scheduling_calendar_event", + "tableTo": "scheduling_group_session", + "columnsFrom": [ + "group_session_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_calendar_event_out_of_office_fk": { + "name": "scheduling_calendar_event_out_of_office_fk", + "tableFrom": "scheduling_calendar_event", + "tableTo": "scheduling_out_of_office", + "columnsFrom": [ + "out_of_office_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_calendar_event_calendar_fk": { + "name": "scheduling_calendar_event_calendar_fk", + "tableFrom": "scheduling_calendar_event", + "tableTo": "scheduling_calendar", + "columnsFrom": [ + "calendar_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_calendar_event_target_unique": { + "name": "scheduling_calendar_event_target_unique", + "nullsNotDistinct": false, + "columns": [ + "booking_id", + "host_id", + "calendar_id" + ] + }, + "scheduling_calendar_event_session_target_unique": { + "name": "scheduling_calendar_event_session_target_unique", + "nullsNotDistinct": false, + "columns": [ + "group_session_id", + "host_id", + "calendar_id" + ] + }, + "scheduling_calendar_event_out_of_office_target_unique": { + "name": "scheduling_calendar_event_out_of_office_target_unique", + "nullsNotDistinct": false, + "columns": [ + "out_of_office_id", + "host_id", + "calendar_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scheduling_calendar_event_one_owner": { + "name": "scheduling_calendar_event_one_owner", + "value": "num_nonnulls(\"scheduling_calendar_event\".\"booking_id\", \"scheduling_calendar_event\".\"group_session_id\", \"scheduling_calendar_event\".\"out_of_office_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.scheduling_claim": { + "name": "scheduling_claim", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_session_id": { + "name": "group_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occupied_start": { + "name": "occupied_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "occupied_end": { + "name": "occupied_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "live_from": { + "name": "live_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "live_until": { + "name": "live_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_day": { + "name": "limit_day", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "limit_week": { + "name": "limit_week", + "type": "date", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scheduling_claim_booking_idx": { + "name": "scheduling_claim_booking_idx", + "columns": [ + { + "expression": "booking_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_claim_group_session_idx": { + "name": "scheduling_claim_group_session_idx", + "columns": [ + { + "expression": "group_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_claim_host_day_idx": { + "name": "scheduling_claim_host_day_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "limit_day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_claim_host_week_idx": { + "name": "scheduling_claim_host_week_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "limit_week", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_claim_occupied_end_idx": { + "name": "scheduling_claim_occupied_end_idx", + "columns": [ + { + "expression": "occupied_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_claim_live_until_idx": { + "name": "scheduling_claim_live_until_idx", + "columns": [ + { + "expression": "live_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_claim\".\"live_until\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_claim_host_fk": { + "name": "scheduling_claim_host_fk", + "tableFrom": "scheduling_claim", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_claim_booking_fk": { + "name": "scheduling_claim_booking_fk", + "tableFrom": "scheduling_claim", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_claim_group_session_fk": { + "name": "scheduling_claim_group_session_fk", + "tableFrom": "scheduling_claim", + "tableTo": "scheduling_group_session", + "columnsFrom": [ + "group_session_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "scheduling_claim_occupied_order": { + "name": "scheduling_claim_occupied_order", + "value": "\"scheduling_claim\".\"occupied_start\" < \"scheduling_claim\".\"occupied_end\"" + }, + "scheduling_claim_live_order": { + "name": "scheduling_claim_live_order", + "value": "\"scheduling_claim\".\"live_until\" IS NULL OR \"scheduling_claim\".\"live_from\" < \"scheduling_claim\".\"live_until\"" + }, + "scheduling_claim_one_owner": { + "name": "scheduling_claim_one_owner", + "value": "(\"scheduling_claim\".\"booking_id\" IS NULL) <> (\"scheduling_claim\".\"group_session_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.scheduling_event_type": { + "name": "scheduling_event_type", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "scheduling_event_type_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'one_on_one'" + }, + "status": { + "name": "status", + "type": "scheduling_event_type_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_options": { + "name": "duration_options", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "step_minutes": { + "name": "step_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grid_time_zone": { + "name": "grid_time_zone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buffer_before_minutes": { + "name": "buffer_before_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "buffer_after_minutes": { + "name": "buffer_after_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "min_notice_minutes": { + "name": "min_notice_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "booking_window": { + "name": "booking_window", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "limits": { + "name": "limits", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "requires_approval": { + "name": "requires_approval", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reassignment_enabled": { + "name": "reassignment_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "approval_ttl_minutes": { + "name": "approval_ttl_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "hold_ttl_seconds": { + "name": "hold_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 600 + }, + "cancel_notice_minutes": { + "name": "cancel_notice_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reschedule_notice_minutes": { + "name": "reschedule_notice_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_guests": { + "name": "max_guests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_active_bookings_per_attendee": { + "name": "max_active_bookings_per_attendee", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "host_calendar_details": { + "name": "host_calendar_details", + "type": "scheduling_calendar_details", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'neutral'" + }, + "questions": { + "name": "questions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "locations": { + "name": "locations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "host_filter": { + "name": "host_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notifications": { + "name": "notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reminders": { + "name": "reminders", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "screened_at": { + "name": "screened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_event_type_environment_created_at_idx": { + "name": "scheduling_event_type_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_event_type_environment_fk": { + "name": "scheduling_event_type_environment_fk", + "tableFrom": "scheduling_event_type", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_event_type_environment_uid_unique": { + "name": "scheduling_event_type_environment_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "uid" + ] + }, + "scheduling_event_type_id_organization_unique": { + "name": "scheduling_event_type_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scheduling_event_type_duration_positive": { + "name": "scheduling_event_type_duration_positive", + "value": "\"scheduling_event_type\".\"duration_minutes\" > 0" + }, + "scheduling_event_type_step_positive": { + "name": "scheduling_event_type_step_positive", + "value": "\"scheduling_event_type\".\"step_minutes\" > 0" + }, + "scheduling_event_type_capacity": { + "name": "scheduling_event_type_capacity", + "value": "\"scheduling_event_type\".\"capacity\" IS NULL OR (\"scheduling_event_type\".\"kind\" = 'group' AND \"scheduling_event_type\".\"capacity\" > 0)" + } + }, + "isRLSEnabled": false + }, + "public.scheduling_event_type_host": { + "name": "scheduling_event_type_host", + "schema": "", + "columns": { + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "scheduling_host_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "assigned": { + "name": "assigned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "fair_units": { + "name": "fair_units", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_assigned_at": { + "name": "last_assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_event_type_host_host_idx": { + "name": "scheduling_event_type_host_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_event_type_host_event_type_fk": { + "name": "scheduling_event_type_host_event_type_fk", + "tableFrom": "scheduling_event_type_host", + "tableTo": "scheduling_event_type", + "columnsFrom": [ + "event_type_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_event_type_host_host_fk": { + "name": "scheduling_event_type_host_host_fk", + "tableFrom": "scheduling_event_type_host", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scheduling_event_type_host_event_type_id_host_id_pk": { + "name": "scheduling_event_type_host_event_type_id_host_id_pk", + "columns": [ + "event_type_id", + "host_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "scheduling_event_type_host_weight_positive": { + "name": "scheduling_event_type_host_weight_positive", + "value": "\"scheduling_event_type_host\".\"weight\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.scheduling_group_session": { + "name": "scheduling_group_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "meeting_url": { + "name": "meeting_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_group_session_end_at_idx": { + "name": "scheduling_group_session_end_at_idx", + "columns": [ + { + "expression": "end_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_group_session_event_type_fk": { + "name": "scheduling_group_session_event_type_fk", + "tableFrom": "scheduling_group_session", + "tableTo": "scheduling_event_type", + "columnsFrom": [ + "event_type_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_group_session_host_fk": { + "name": "scheduling_group_session_host_fk", + "tableFrom": "scheduling_group_session", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_group_session_id_organization_unique": { + "name": "scheduling_group_session_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "scheduling_group_session_slot_unique": { + "name": "scheduling_group_session_slot_unique", + "nullsNotDistinct": false, + "columns": [ + "event_type_id", + "host_id", + "start_at" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_holiday_calendar": { + "name": "scheduling_holiday_calendar", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_ics_url": { + "name": "encrypted_ics_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "holidays": { + "name": "holidays", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "regions": { + "name": "regions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "host_ids": { + "name": "host_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "host_filter": { + "name": "host_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "delegate_ids": { + "name": "delegate_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "existing_bookings": { + "name": "existing_bookings", + "type": "scheduling_out_of_office_existing", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'leave'" + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settings_revision": { + "name": "settings_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_holiday_calendar_environment_idx": { + "name": "scheduling_holiday_calendar_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_holiday_calendar_due_idx": { + "name": "scheduling_holiday_calendar_due_idx", + "columns": [ + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_holiday_calendar_environment_fk": { + "name": "scheduling_holiday_calendar_environment_fk", + "tableFrom": "scheduling_holiday_calendar", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_host": { + "name": "scheduling_host", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "calendar_account": { + "name": "calendar_account", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time_zone": { + "name": "time_zone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "locations": { + "name": "locations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "status": { + "name": "status", + "type": "scheduling_host_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "screened_at": { + "name": "screened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_fee_at": { + "name": "next_fee_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_host_environment_created_at_idx": { + "name": "scheduling_host_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_host_attributes_idx": { + "name": "scheduling_host_attributes_idx", + "columns": [ + { + "expression": "attributes", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "jsonb_path_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "scheduling_host_next_fee_at_idx": { + "name": "scheduling_host_next_fee_at_idx", + "columns": [ + { + "expression": "next_fee_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_host\".\"next_fee_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_host_live_organization_idx": { + "name": "scheduling_host_live_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_host\".\"next_fee_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_host_environment_fk": { + "name": "scheduling_host_environment_fk", + "tableFrom": "scheduling_host", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_host_environment_uid_unique": { + "name": "scheduling_host_environment_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "uid" + ] + }, + "scheduling_host_id_organization_unique": { + "name": "scheduling_host_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_limit_bucket": { + "name": "scheduling_limit_bucket", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_limit_bucket_organization_idx": { + "name": "scheduling_limit_bucket_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_limit_bucket_created_at_idx": { + "name": "scheduling_limit_bucket_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "scheduling_limit_bucket_scope_scope_id_period_pk": { + "name": "scheduling_limit_bucket_scope_scope_id_period_pk", + "columns": [ + "scope", + "scope_id", + "period" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_logo": { + "name": "scheduling_logo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scheduling_logo_environment_fk": { + "name": "scheduling_logo_environment_fk", + "tableFrom": "scheduling_logo", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_logo_environment_unique": { + "name": "scheduling_logo_environment_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_notification": { + "name": "scheduling_notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reassignment_id": { + "name": "reassignment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "scheduling_notification_channel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "recipient": { + "name": "recipient", + "type": "scheduling_notification_recipient", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "booking_version": { + "name": "booking_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reminder_key": { + "name": "reminder_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reminder_anchor": { + "name": "reminder_anchor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reminder_offset_minutes": { + "name": "reminder_offset_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "anchor_at": { + "name": "anchor_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_notification_due_idx": { + "name": "scheduling_notification_due_idx", + "columns": [ + { + "expression": "due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_notification\".\"finished_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_notification_booking_idx": { + "name": "scheduling_notification_booking_idx", + "columns": [ + { + "expression": "booking_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_notification_environment_fk": { + "name": "scheduling_notification_environment_fk", + "tableFrom": "scheduling_notification", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_notification_booking_fk": { + "name": "scheduling_notification_booking_fk", + "tableFrom": "scheduling_notification", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_notification_dedupe_unique": { + "name": "scheduling_notification_dedupe_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_oauth_app": { + "name": "scheduling_oauth_app", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "scheduling_oauth_app_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_credentials": { + "name": "encrypted_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_url": { + "name": "completion_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_expires_at": { + "name": "secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "scheduling_oauth_app_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scheduling_oauth_app_environment_fk": { + "name": "scheduling_oauth_app_environment_fk", + "tableFrom": "scheduling_oauth_app", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_oauth_app_id_organization_unique": { + "name": "scheduling_oauth_app_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "scheduling_oauth_app_provider_unique": { + "name": "scheduling_oauth_app_provider_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "provider" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_oauth_state": { + "name": "scheduling_oauth_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "scheduling_calendar_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_code_verifier": { + "name": "encrypted_code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "return_url": { + "name": "return_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_app_id": { + "name": "oauth_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_oauth_state_expires_at_idx": { + "name": "scheduling_oauth_state_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_oauth_state_host_fk": { + "name": "scheduling_oauth_state_host_fk", + "tableFrom": "scheduling_oauth_state", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_oauth_state_oauth_app_fk": { + "name": "scheduling_oauth_state_oauth_app_fk", + "tableFrom": "scheduling_oauth_state", + "tableTo": "scheduling_oauth_app", + "columnsFrom": [ + "oauth_app_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_oauth_state_hash_unique": { + "name": "scheduling_oauth_state_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "state_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_out_of_office": { + "name": "scheduling_out_of_office", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "all_day_zone": { + "name": "all_day_zone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unspecified'" + }, + "encrypted_notes": { + "name": "encrypted_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_message": { + "name": "public_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "show_on_calendar": { + "name": "show_on_calendar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "existing_bookings": { + "name": "existing_bookings", + "type": "scheduling_out_of_office_existing", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'leave'" + }, + "reassign_strategy": { + "name": "reassign_strategy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "scheduling_out_of_office_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_revision": { + "name": "source_revision", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "announced_start_at": { + "name": "announced_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "announced_end_at": { + "name": "announced_end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fairness_leveled_at": { + "name": "fairness_leveled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "actor": { + "name": "actor", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_out_of_office_source_key_idx": { + "name": "scheduling_out_of_office_source_key_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"scheduling_out_of_office\".\"source_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_out_of_office_external_id_idx": { + "name": "scheduling_out_of_office_external_id_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"scheduling_out_of_office\".\"external_id\" IS NOT NULL AND \"scheduling_out_of_office\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_out_of_office_host_idx": { + "name": "scheduling_out_of_office_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "end_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_out_of_office\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_out_of_office_environment_idx": { + "name": "scheduling_out_of_office_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_out_of_office_unannounced_start_idx": { + "name": "scheduling_out_of_office_unannounced_start_idx", + "columns": [ + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_out_of_office\".\"announced_start_at\" IS NULL AND \"scheduling_out_of_office\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_out_of_office_unannounced_end_idx": { + "name": "scheduling_out_of_office_unannounced_end_idx", + "columns": [ + { + "expression": "end_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_out_of_office\".\"announced_end_at\" IS NULL AND \"scheduling_out_of_office\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_out_of_office_host_fk": { + "name": "scheduling_out_of_office_host_fk", + "tableFrom": "scheduling_out_of_office", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_out_of_office_environment_fk": { + "name": "scheduling_out_of_office_environment_fk", + "tableFrom": "scheduling_out_of_office", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_out_of_office_id_organization_unique": { + "name": "scheduling_out_of_office_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scheduling_out_of_office_span": { + "name": "scheduling_out_of_office_span", + "value": "\"scheduling_out_of_office\".\"start_at\" < \"scheduling_out_of_office\".\"end_at\"" + } + }, + "isRLSEnabled": false + }, + "public.scheduling_out_of_office_delegate": { + "name": "scheduling_out_of_office_delegate", + "schema": "", + "columns": { + "out_of_office_id": { + "name": "out_of_office_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scheduling_out_of_office_delegate_host_idx": { + "name": "scheduling_out_of_office_delegate_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_out_of_office_delegate_entry_fk": { + "name": "scheduling_out_of_office_delegate_entry_fk", + "tableFrom": "scheduling_out_of_office_delegate", + "tableTo": "scheduling_out_of_office", + "columnsFrom": [ + "out_of_office_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_out_of_office_delegate_host_fk": { + "name": "scheduling_out_of_office_delegate_host_fk", + "tableFrom": "scheduling_out_of_office_delegate", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scheduling_out_of_office_delegate_pk": { + "name": "scheduling_out_of_office_delegate_pk", + "columns": [ + "out_of_office_id", + "host_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_public_holiday": { + "name": "scheduling_public_holiday", + "schema": "", + "columns": { + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "scheduling_public_holiday_pk": { + "name": "scheduling_public_holiday_pk", + "columns": [ + "country_code", + "date" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_public_holiday_source": { + "name": "scheduling_public_holiday_source", + "schema": "", + "columns": { + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_read_at": { + "name": "next_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_reassignment": { + "name": "scheduling_reassignment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "scheduling_reassignment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "selection": { + "name": "selection", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "strategy": { + "name": "strategy", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_note": { + "name": "encrypted_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "overrides": { + "name": "overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "notify_attendees": { + "name": "notify_attendees", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_hosts": { + "name": "notify_hosts", + "type": "scheduling_reassignment_host_notice", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'digest'" + }, + "out_of_office_id": { + "name": "out_of_office_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "undo_of_id": { + "name": "undo_of_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor": { + "name": "actor", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expanded_at": { + "name": "expanded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_reassignment_environment_idx": { + "name": "scheduling_reassignment_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_reassignment_due_idx": { + "name": "scheduling_reassignment_due_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_reassignment\".\"status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_reassignment_created_at_idx": { + "name": "scheduling_reassignment_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_reassignment_environment_fk": { + "name": "scheduling_reassignment_environment_fk", + "tableFrom": "scheduling_reassignment", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_reassignment_id_organization_unique": { + "name": "scheduling_reassignment_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "scheduling_reassignment_idempotency_unique": { + "name": "scheduling_reassignment_idempotency_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_reassignment_item": { + "name": "scheduling_reassignment_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reassignment_id": { + "name": "reassignment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_host_id": { + "name": "from_host_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to_host_id": { + "name": "to_host_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "series_id": { + "name": "series_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "scheduling_reassignment_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_reassignment_item_pending_idx": { + "name": "scheduling_reassignment_item_pending_idx", + "columns": [ + { + "expression": "reassignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_reassignment_item\".\"outcome\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_reassignment_item_booking_idx": { + "name": "scheduling_reassignment_item_booking_idx", + "columns": [ + { + "expression": "booking_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_reassignment_item_reassignment_fk": { + "name": "scheduling_reassignment_item_reassignment_fk", + "tableFrom": "scheduling_reassignment_item", + "tableTo": "scheduling_reassignment", + "columnsFrom": [ + "reassignment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_reassignment_item_booking_fk": { + "name": "scheduling_reassignment_item_booking_fk", + "tableFrom": "scheduling_reassignment_item", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_reassignment_item_booking_unique": { + "name": "scheduling_reassignment_item_booking_unique", + "nullsNotDistinct": false, + "columns": [ + "reassignment_id", + "booking_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_reminder_sync": { + "name": "scheduling_reminder_sync", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_start_at": { + "name": "cursor_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cursor_booking_id": { + "name": "cursor_booking_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_reminder_sync_environment_idx": { + "name": "scheduling_reminder_sync_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_reminder_sync_environment_fk": { + "name": "scheduling_reminder_sync_environment_fk", + "tableFrom": "scheduling_reminder_sync", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_reminder_sync_event_type_fk": { + "name": "scheduling_reminder_sync_event_type_fk", + "tableFrom": "scheduling_reminder_sync", + "tableTo": "scheduling_event_type", + "columnsFrom": [ + "event_type_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_schedule": { + "name": "scheduling_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_zone": { + "name": "time_zone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rules": { + "name": "rules", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_schedule_host_default_unique": { + "name": "scheduling_schedule_host_default_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"scheduling_schedule\".\"is_default\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_schedule_host_idx": { + "name": "scheduling_schedule_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_schedule_host_fk": { + "name": "scheduling_schedule_host_fk", + "tableFrom": "scheduling_schedule", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_schedule_id_organization_unique": { + "name": "scheduling_schedule_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_schedule_override": { + "name": "scheduling_schedule_override", + "schema": "", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "ranges": { + "name": "ranges", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "scheduling_schedule_override_schedule_fk": { + "name": "scheduling_schedule_override_schedule_fk", + "tableFrom": "scheduling_schedule_override", + "tableTo": "scheduling_schedule", + "columnsFrom": [ + "schedule_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scheduling_schedule_override_schedule_id_date_pk": { + "name": "scheduling_schedule_override_schedule_id_date_pk", + "columns": [ + "schedule_id", + "date" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_series": { + "name": "scheduling_series", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule": { + "name": "rule", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "anchor_time_zone": { + "name": "anchor_time_zone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_series_environment_idx": { + "name": "scheduling_series_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_series_created_at_idx": { + "name": "scheduling_series_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_series_event_type_fk": { + "name": "scheduling_series_event_type_fk", + "tableFrom": "scheduling_series", + "tableTo": "scheduling_event_type", + "columnsFrom": [ + "event_type_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_series_id_organization_unique": { + "name": "scheduling_series_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "scheduling_series_environment_idempotency_unique": { + "name": "scheduling_series_environment_idempotency_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_session": { + "name": "scheduling_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "scheduling_session_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_attendee": { + "name": "encrypted_attendee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_attributes": { + "name": "encrypted_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attendee_uid": { + "name": "attendee_uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manager_host_ids": { + "name": "manager_host_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "manager_host_filter": { + "name": "manager_host_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attendee_email_hmac": { + "name": "attendee_email_hmac", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "read_only": { + "name": "read_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_session_expires_at_idx": { + "name": "scheduling_session_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_session_booking_idx": { + "name": "scheduling_session_booking_idx", + "columns": [ + { + "expression": "booking_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_session_attendee_email_idx": { + "name": "scheduling_session_attendee_email_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attendee_email_hmac", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "attendee_email_hmac is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_session_attendee_uid_idx": { + "name": "scheduling_session_attendee_uid_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attendee_uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_session\".\"attendee_uid\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_session_environment_fk": { + "name": "scheduling_session_environment_fk", + "tableFrom": "scheduling_session", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_session_host_fk": { + "name": "scheduling_session_host_fk", + "tableFrom": "scheduling_session", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_session_event_type_fk": { + "name": "scheduling_session_event_type_fk", + "tableFrom": "scheduling_session", + "tableTo": "scheduling_event_type", + "columnsFrom": [ + "event_type_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_session_booking_fk": { + "name": "scheduling_session_booking_fk", + "tableFrom": "scheduling_session", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_session_token_hash_unique": { + "name": "scheduling_session_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scheduling_session_scope": { + "name": "scheduling_session_scope", + "value": "(\"scheduling_session\".\"kind\" = 'host' AND \"scheduling_session\".\"host_id\" IS NOT NULL)\n OR (\"scheduling_session\".\"kind\" = 'booker' AND (\"scheduling_session\".\"event_type_id\" IS NOT NULL OR \"scheduling_session\".\"host_id\" IS NOT NULL))\n OR (\"scheduling_session\".\"kind\" = 'manage' AND \"scheduling_session\".\"booking_id\" IS NOT NULL)\n OR (\"scheduling_session\".\"kind\"::text = 'manager' AND (\"scheduling_session\".\"manager_host_ids\" IS NOT NULL OR \"scheduling_session\".\"manager_host_filter\" IS NOT NULL))" + } + }, + "isRLSEnabled": false + }, + "public.scheduling_settings": { + "name": "scheduling_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_origins": { + "name": "allowed_origins", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "brand_name": { + "name": "brand_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "privacy_url": { + "name": "privacy_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_from": { + "name": "email_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_reply_to": { + "name": "email_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sms_from": { + "name": "sms_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notifications": { + "name": "notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "out_of_office_reasons": { + "name": "out_of_office_reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "name_host_in_attendee_emails": { + "name": "name_host_in_attendee_emails", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host_reassignment": { + "name": "host_reassignment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'off'" + }, + "reminders": { + "name": "reminders", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "calendar_failure_mode": { + "name": "calendar_failure_mode", + "type": "scheduling_calendar_failure_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'hide'" + }, + "screened_at": { + "name": "screened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scheduling_settings_environment_fk": { + "name": "scheduling_settings_environment_fk", + "tableFrom": "scheduling_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_settings_environment_unique": { + "name": "scheduling_settings_environment_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_builder_session": { + "name": "sign_builder_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_builder_session_expires_at_idx": { + "name": "sign_builder_session_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_builder_session_template_fk": { + "name": "sign_builder_session_template_fk", + "tableFrom": "sign_builder_session", + "tableTo": "sign_template", + "columnsFrom": [ + "template_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_builder_session_token_hash_unique": { + "name": "sign_builder_session_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_document": { + "name": "sign_document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "store_key": { + "name": "store_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pages": { + "name": "pages", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "anchors": { + "name": "anchors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_document_environment_created_at_idx": { + "name": "sign_document_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_document_created_at_idx": { + "name": "sign_document_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_document_environment_fk": { + "name": "sign_document_environment_fk", + "tableFrom": "sign_document", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_document_id_organization_unique": { + "name": "sign_document_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_event": { + "name": "sign_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sign_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "signer_id": { + "name": "signer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor": { + "name": "actor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "previous_hash": { + "name": "previous_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sign_event_request_fk": { + "name": "sign_event_request_fk", + "tableFrom": "sign_event", + "tableTo": "sign_request", + "columnsFrom": [ + "request_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_event_request_sequence_unique": { + "name": "sign_event_request_sequence_unique", + "nullsNotDistinct": false, + "columns": [ + "request_id", + "sequence" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_fake_object": { + "name": "sign_fake_object", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_fake_object_organization_idx": { + "name": "sign_fake_object_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_field": { + "name": "sign_field", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signer_id": { + "name": "signer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sign_field_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "page": { + "name": "page", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "x": { + "name": "x", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "y": { + "name": "y", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "anchor": { + "name": "anchor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefilled": { + "name": "prefilled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_field_request_idx": { + "name": "sign_field_request_idx", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_field_signer_idx": { + "name": "sign_field_signer_idx", + "columns": [ + { + "expression": "signer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_field_signer_fk": { + "name": "sign_field_signer_fk", + "tableFrom": "sign_field", + "tableTo": "sign_signer", + "columnsFrom": [ + "signer_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sign_field_request_fk": { + "name": "sign_field_request_fk", + "tableFrom": "sign_field", + "tableTo": "sign_request", + "columnsFrom": [ + "request_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_request": { + "name": "sign_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sign_request_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signing_order": { + "name": "signing_order", + "type": "sign_signing_order", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'parallel'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "declined_at": { + "name": "declined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "signed_document_key": { + "name": "signed_document_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signed_document_sha256": { + "name": "signed_document_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sealed_at": { + "name": "sealed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "seal_lease_until": { + "name": "seal_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "seal_attempts": { + "name": "seal_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seal_next_attempt_at": { + "name": "seal_next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "seal_error": { + "name": "seal_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reminder_interval_days": { + "name": "reminder_interval_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expired_at": { + "name": "expired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_request_environment_created_at_idx": { + "name": "sign_request_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_request_expires_at_idx": { + "name": "sign_request_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sign_request\".\"status\" = 'sent'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_request_created_at_idx": { + "name": "sign_request_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_request_unsealed_idx": { + "name": "sign_request_unsealed_idx", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sign_request\".\"status\" = 'completed' and \"sign_request\".\"sealed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_request_environment_fk": { + "name": "sign_request_environment_fk", + "tableFrom": "sign_request", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_request_environment_idempotency_key_unique": { + "name": "sign_request_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + }, + "sign_request_id_organization_unique": { + "name": "sign_request_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_request_document": { + "name": "sign_request_document", + "schema": "", + "columns": { + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sign_request_document_document_idx": { + "name": "sign_request_document_document_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_request_document_request_fk": { + "name": "sign_request_document_request_fk", + "tableFrom": "sign_request_document", + "tableTo": "sign_request", + "columnsFrom": [ + "request_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sign_request_document_document_fk": { + "name": "sign_request_document_document_fk", + "tableFrom": "sign_request_document", + "tableTo": "sign_document", + "columnsFrom": [ + "document_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sign_request_document_request_id_position_pk": { + "name": "sign_request_document_request_id_position_pk", + "columns": [ + "request_id", + "position" + ] + } + }, + "uniqueConstraints": { + "sign_request_document_unique": { + "name": "sign_request_document_unique", + "nullsNotDistinct": false, + "columns": [ + "request_id", + "document_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_session": { + "name": "sign_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signer_id": { + "name": "signer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_session_expires_at_idx": { + "name": "sign_session_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_session_signer_idx": { + "name": "sign_session_signer_idx", + "columns": [ + { + "expression": "signer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_session_signer_fk": { + "name": "sign_session_signer_fk", + "tableFrom": "sign_session", + "tableTo": "sign_signer", + "columnsFrom": [ + "signer_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_session_token_hash_unique": { + "name": "sign_session_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_settings": { + "name": "sign_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_origins": { + "name": "allowed_origins", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "disclosure": { + "name": "disclosure", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_from": { + "name": "email_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reminder_interval_days": { + "name": "reminder_interval_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sign_settings_environment_fk": { + "name": "sign_settings_environment_fk", + "tableFrom": "sign_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_settings_environment_unique": { + "name": "sign_settings_environment_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_signer": { + "name": "sign_signer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "delivery": { + "name": "delivery", + "type": "sign_delivery", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "authentication": { + "name": "authentication", + "type": "sign_authentication", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sign_signer_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "client_ref": { + "name": "client_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "viewed_at": { + "name": "viewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consented_at": { + "name": "consented_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "signed_at": { + "name": "signed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "declined_at": { + "name": "declined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "decline_reason": { + "name": "decline_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature_key": { + "name": "signature_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature_sha256": { + "name": "signature_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initials_key": { + "name": "initials_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initials_sha256": { + "name": "initials_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notified_at": { + "name": "notified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reminder_count": { + "name": "reminder_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_reminded_at": { + "name": "last_reminded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_reminder_at": { + "name": "next_reminder_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remind_requested_at": { + "name": "remind_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "copy_sent_at": { + "name": "copy_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email_failures": { + "name": "email_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "email_retry_at": { + "name": "email_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_signer_request_idx": { + "name": "sign_signer_request_idx", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_signer_email_due_idx": { + "name": "sign_signer_email_due_idx", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sign_signer\".\"delivery\" = 'email' and (\"sign_signer\".\"notified_at\" is null or \"sign_signer\".\"next_reminder_at\" is not null or \"sign_signer\".\"remind_requested_at\" is not null or (\"sign_signer\".\"status\" = 'signed' and \"sign_signer\".\"copy_sent_at\" is null))", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_signer_request_fk": { + "name": "sign_signer_request_fk", + "tableFrom": "sign_signer", + "tableTo": "sign_request", + "columnsFrom": [ + "request_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_signer_id_organization_unique": { + "name": "sign_signer_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "sign_signer_request_email_unique": { + "name": "sign_signer_request_email_unique", + "nullsNotDistinct": false, + "columns": [ + "request_id", + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_store_cleanup": { + "name": "sign_store_cleanup", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_store_cleanup_next_attempt_at_idx": { + "name": "sign_store_cleanup_next_attempt_at_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_store_cleanup_organization_idx": { + "name": "sign_store_cleanup_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_template": { + "name": "sign_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signing_order": { + "name": "signing_order", + "type": "sign_signing_order", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'parallel'" + }, + "roles": { + "name": "roles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_template_environment_created_at_idx": { + "name": "sign_template_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_template_environment_fk": { + "name": "sign_template_environment_fk", + "tableFrom": "sign_template", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_template_id_organization_unique": { + "name": "sign_template_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_template_document": { + "name": "sign_template_document", + "schema": "", + "columns": { + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sign_template_document_document_idx": { + "name": "sign_template_document_document_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_template_document_template_fk": { + "name": "sign_template_document_template_fk", + "tableFrom": "sign_template_document", + "tableTo": "sign_template", + "columnsFrom": [ + "template_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sign_template_document_document_fk": { + "name": "sign_template_document_document_fk", + "tableFrom": "sign_template_document", + "tableTo": "sign_document", + "columnsFrom": [ + "document_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sign_template_document_template_id_position_pk": { + "name": "sign_template_document_template_id_position_pk", + "columns": [ + "template_id", + "position" + ] + } + }, + "uniqueConstraints": { + "sign_template_document_unique": { + "name": "sign_template_document_unique", + "nullsNotDistinct": false, + "columns": [ + "template_id", + "document_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_template_field": { + "name": "sign_template_field", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document": { + "name": "document", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sign_field_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "page": { + "name": "page", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "x": { + "name": "x", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "y": { + "name": "y", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merge_key": { + "name": "merge_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_template_field_template_idx": { + "name": "sign_template_field_template_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_template_field_template_fk": { + "name": "sign_template_field_template_fk", + "tableFrom": "sign_template_field", + "tableTo": "sign_template", + "columnsFrom": [ + "template_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_account": { + "name": "sms_account", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subaccount_sid": { + "name": "subaccount_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_token": { + "name": "auth_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sms_account_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "allowed_countries": { + "name": "allowed_countries", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{US,CA}'::text[]" + }, + "unlocked_countries": { + "name": "unlocked_countries", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sms_account_organization_id_organization_id_fk": { + "name": "sms_account_organization_id_organization_id_fk", + "tableFrom": "sms_account", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_account_subaccount_sid_unique": { + "name": "sms_account_subaccount_sid_unique", + "nullsNotDistinct": false, + "columns": [ + "subaccount_sid" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_event": { + "name": "sms_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sms_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_status": { + "name": "provider_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_event_message_occurred_at_idx": { + "name": "sms_event_message_occurred_at_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_event_message_fk": { + "name": "sms_event_message_fk", + "tableFrom": "sms_event", + "tableTo": "sms_message", + "columnsFrom": [ + "message_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_event_message_dedupe_unique": { + "name": "sms_event_message_dedupe_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id", + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_inbound_receipt": { + "name": "sms_inbound_receipt", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_number": { + "name": "from_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_inbound_receipt_created_at_idx": { + "name": "sms_inbound_receipt_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_inbound_receipt_organization_created_at_idx": { + "name": "sms_inbound_receipt_organization_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_inbound_receipt_organization_id_organization_id_fk": { + "name": "sms_inbound_receipt_organization_id_organization_id_fk", + "tableFrom": "sms_inbound_receipt", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sms_inbound_receipt_provider_provider_message_id_pk": { + "name": "sms_inbound_receipt_provider_provider_message_id_pk", + "columns": [ + "provider", + "provider_message_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_message": { + "name": "sms_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_owner_id": { + "name": "api_key_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sms_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "from": { + "name": "from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to": { + "name": "to", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_country": { + "name": "to_country", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_id": { + "name": "number_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_body": { + "name": "discard_body", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "encoding": { + "name": "encoding", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "segments": { + "name": "segments", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "traceparent": { + "name": "traceparent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_message_environment_created_at_idx": { + "name": "sms_message_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_message_organization_send_time_idx": { + "name": "sms_message_organization_send_time_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"sent_at\", \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_message_scheduled_due_idx": { + "name": "sms_message_scheduled_due_idx", + "columns": [ + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sms_message\".\"status\" = 'scheduled'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_message_queued_created_at_idx": { + "name": "sms_message_queued_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sms_message\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_message_created_at_idx": { + "name": "sms_message_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_message_environment_fk": { + "name": "sms_message_environment_fk", + "tableFrom": "sms_message", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_message_environment_idempotency_key_unique": { + "name": "sms_message_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + }, + "sms_message_id_organization_unique": { + "name": "sms_message_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_number": { + "name": "sms_number", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "sms_number_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_sid": { + "name": "provider_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sms_number_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "receiving_environment_id": { + "name": "receiving_environment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fee_at": { + "name": "next_fee_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_number_organization_idx": { + "name": "sms_number_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_number_next_fee_idx": { + "name": "sms_number_next_fee_idx", + "columns": [ + { + "expression": "next_fee_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sms_number\".\"next_fee_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_number_organization_id_organization_id_fk": { + "name": "sms_number_organization_id_organization_id_fk", + "tableFrom": "sms_number", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_number_phone_number_unique": { + "name": "sms_number_phone_number_unique", + "nullsNotDistinct": false, + "columns": [ + "phone_number" + ] + }, + "sms_number_id_organization_unique": { + "name": "sms_number_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_received": { + "name": "sms_received", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_id": { + "name": "number_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from": { + "name": "from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to": { + "name": "to", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "segments": { + "name": "segments", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_received_environment_created_at_idx": { + "name": "sms_received_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_received_created_at_idx": { + "name": "sms_received_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_received_environment_fk": { + "name": "sms_received_environment_fk", + "tableFrom": "sms_received", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_received_provider_message_unique": { + "name": "sms_received_provider_message_unique", + "nullsNotDistinct": false, + "columns": [ + "provider", + "provider_message_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_registration": { + "name": "sms_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "sms_registration_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sms_registration_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "step": { + "name": "step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_type": { + "name": "brand_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile": { + "name": "profile", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "campaign": { + "name": "campaign", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "customer_profile_sid": { + "name": "customer_profile_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trust_product_sid": { + "name": "trust_product_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_sid": { + "name": "brand_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messaging_service_sid": { + "name": "messaging_service_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "campaign_sid": { + "name": "campaign_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "toll_free_verification_sid": { + "name": "toll_free_verification_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_id": { + "name": "number_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_fee_at": { + "name": "next_fee_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_registration_organization_idx": { + "name": "sms_registration_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_registration_next_check_idx": { + "name": "sms_registration_next_check_idx", + "columns": [ + { + "expression": "next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sms_registration\".\"next_check_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_registration_organization_id_organization_id_fk": { + "name": "sms_registration_organization_id_organization_id_fk", + "tableFrom": "sms_registration", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_registration_id_organization_unique": { + "name": "sms_registration_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_suppression": { + "name": "sms_suppression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "sms_suppression_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "sms_suppression_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_suppression_environment_sequence_idx": { + "name": "sms_suppression_environment_sequence_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_suppression_environment_fk": { + "name": "sms_suppression_environment_fk", + "tableFrom": "sms_suppression", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_suppression_environment_phone_unique": { + "name": "sms_suppression_environment_phone_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "phone_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_attachment": { + "name": "support_attachment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by_user_id": { + "name": "uploaded_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "support_attachment_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "store_key": { + "name": "store_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_attachment_message_idx": { + "name": "support_attachment_message_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_attachment_ticket_idx": { + "name": "support_attachment_ticket_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_attachment_copy_idx": { + "name": "support_attachment_copy_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_attachment\".\"state\" = 'pending_copy'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_attachment_unclaimed_idx": { + "name": "support_attachment_unclaimed_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_attachment\".\"message_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_attachment_ticket_id_support_ticket_id_fk": { + "name": "support_attachment_ticket_id_support_ticket_id_fk", + "tableFrom": "support_attachment", + "tableTo": "support_ticket", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "support_attachment_message_id_support_message_id_fk": { + "name": "support_attachment_message_id_support_message_id_fk", + "tableFrom": "support_attachment", + "tableTo": "support_message", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "support_attachment_uploaded_by_user_id_user_id_fk": { + "name": "support_attachment_uploaded_by_user_id_user_id_fk", + "tableFrom": "support_attachment", + "tableTo": "user", + "columnsFrom": [ + "uploaded_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_fake_object": { + "name": "support_fake_object", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_intake": { + "name": "support_intake", + "schema": "", + "columns": { + "received_email_id": { + "name": "received_email_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_intake_created_idx": { + "name": "support_intake_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_message": { + "name": "support_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "support_message_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_data": { + "name": "event_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_email": { + "name": "author_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "visible_text": { + "name": "visible_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sender_auth": { + "name": "sender_auth", + "type": "support_sender_auth", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "customer_visible": { + "name": "customer_visible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "low_signal": { + "name": "low_signal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rfc_message_id": { + "name": "rfc_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_email_id": { + "name": "received_email_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_status": { + "name": "delivery_status", + "type": "support_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "delivery_attempts": { + "name": "delivery_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_delivery_at": { + "name": "next_delivery_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivery_error": { + "name": "delivery_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rep_alert_pending": { + "name": "rep_alert_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_message_ticket_idx": { + "name": "support_message_ticket_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_message_rfc_unique": { + "name": "support_message_rfc_unique", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rfc_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"support_message\".\"rfc_message_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_message_rfc_idx": { + "name": "support_message_rfc_idx", + "columns": [ + { + "expression": "rfc_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_message\".\"rfc_message_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_message_delivery_idx": { + "name": "support_message_delivery_idx", + "columns": [ + { + "expression": "next_delivery_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_message\".\"delivery_status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_message_accepted_idx": { + "name": "support_message_accepted_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_message\".\"delivery_status\" = 'accepted'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_message_alert_idx": { + "name": "support_message_alert_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_message\".\"rep_alert_pending\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_message_ticket_id_support_ticket_id_fk": { + "name": "support_message_ticket_id_support_ticket_id_fk", + "tableFrom": "support_message", + "tableTo": "support_ticket", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "support_message_author_user_id_user_id_fk": { + "name": "support_message_author_user_id_user_id_fk", + "tableFrom": "support_message", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_saved_reply": { + "name": "support_saved_reply", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_saved_reply_title_unique": { + "name": "support_saved_reply_title_unique", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_saved_reply_created_by_user_id_user_id_fk": { + "name": "support_saved_reply_created_by_user_id_user_id_fk", + "tableFrom": "support_saved_reply", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_ticket": { + "name": "support_ticket", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "support_ticket_number_seq", + "schema": "public", + "increment": "1", + "startWith": "1001", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "reply_token": { + "name": "reply_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "severity": { + "name": "severity", + "type": "support_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "status": { + "name": "status", + "type": "support_ticket_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "source": { + "name": "source", + "type": "support_ticket_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "filter_reason": { + "name": "filter_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_match": { + "name": "org_match", + "type": "support_org_match", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "suggested_organization_id": { + "name": "suggested_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requester_user_id": { + "name": "requester_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requester_email": { + "name": "requester_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_name": { + "name": "requester_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_auth": { + "name": "sender_auth", + "type": "support_sender_auth", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cc": { + "name": "cc", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "suggested_cc": { + "name": "suggested_cc", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "all_hours": { + "name": "all_hours", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "awaiting_since": { + "name": "awaiting_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "response_due_at": { + "name": "response_due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "first_responded_at": { + "name": "first_responded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "breached_at": { + "name": "breached_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "risk_alerted_due_at": { + "name": "risk_alerted_due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "breach_alerted_due_at": { + "name": "breach_alerted_due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_customer_message_at": { + "name": "last_customer_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_agent_message_at": { + "name": "last_agent_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_ticket_number_unique": { + "name": "support_ticket_number_unique", + "columns": [ + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_reply_token_unique": { + "name": "support_ticket_reply_token_unique", + "columns": [ + { + "expression": "reply_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_queue_idx": { + "name": "support_ticket_queue_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "response_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_organization_idx": { + "name": "support_ticket_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_suggested_idx": { + "name": "support_ticket_suggested_idx", + "columns": [ + { + "expression": "suggested_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_requester_idx": { + "name": "support_ticket_requester_idx", + "columns": [ + { + "expression": "requester_email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_requester_user_idx": { + "name": "support_ticket_requester_user_idx", + "columns": [ + { + "expression": "requester_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_assignee_idx": { + "name": "support_ticket_assignee_idx", + "columns": [ + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_snoozed_idx": { + "name": "support_ticket_snoozed_idx", + "columns": [ + { + "expression": "snoozed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_ticket\".\"snoozed_until\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_ticket_organization_id_organization_id_fk": { + "name": "support_ticket_organization_id_organization_id_fk", + "tableFrom": "support_ticket", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "support_ticket_suggested_organization_id_organization_id_fk": { + "name": "support_ticket_suggested_organization_id_organization_id_fk", + "tableFrom": "support_ticket", + "tableTo": "organization", + "columnsFrom": [ + "suggested_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "support_ticket_requester_user_id_user_id_fk": { + "name": "support_ticket_requester_user_id_user_id_fk", + "tableFrom": "support_ticket", + "tableTo": "user", + "columnsFrom": [ + "requester_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "support_ticket_assignee_user_id_user_id_fk": { + "name": "support_ticket_assignee_user_id_user_id_fk", + "tableFrom": "support_ticket", + "tableTo": "user", + "columnsFrom": [ + "assignee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_ticket_read": { + "name": "support_ticket_read", + "schema": "", + "columns": { + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_ticket_read_unique": { + "name": "support_ticket_read_unique", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_ticket_read_ticket_id_support_ticket_id_fk": { + "name": "support_ticket_read_ticket_id_support_ticket_id_fk", + "tableFrom": "support_ticket_read", + "tableTo": "support_ticket", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "support_ticket_read_user_id_user_id_fk": { + "name": "support_ticket_read_user_id_user_id_fk", + "tableFrom": "support_ticket_read", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verify_check": { + "name": "verify_check", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid": { + "name": "valid", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "verify_check_request_fk": { + "name": "verify_check_request_fk", + "tableFrom": "verify_check", + "tableTo": "verify_request", + "columnsFrom": [ + "request_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "verify_check_request_idempotency_key_unique": { + "name": "verify_check_request_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "request_id", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verify_request": { + "name": "verify_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_owner_id": { + "name": "api_key_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel": { + "name": "channel", + "type": "verify_channel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "to": { + "name": "to", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_country": { + "name": "to_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_reasons": { + "name": "risk_reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_length": { + "name": "code_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sends": { + "name": "sends", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verify_request_environment_created_at_idx": { + "name": "verify_request_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verify_request_pending_to_idx": { + "name": "verify_request_pending_to_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"verify_request\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "verify_request_created_at_idx": { + "name": "verify_request_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "verify_request_environment_fk": { + "name": "verify_request_environment_fk", + "tableFrom": "verify_request", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "verify_request_environment_idempotency_key_unique": { + "name": "verify_request_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + }, + "verify_request_id_organization_unique": { + "name": "verify_request_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verify_send": { + "name": "verify_send", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "verify_channel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "verify_send_route", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sms_id": { + "name": "sms_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_environment_id": { + "name": "message_environment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_owner_id": { + "name": "api_key_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verify_send_unmetered_idx": { + "name": "verify_send_unmetered_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"verify_send\".\"route\" = 'shared_number' and \"verify_send\".\"usage_recorded_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "verify_send_request_fk": { + "name": "verify_send_request_fk", + "tableFrom": "verify_send", + "tableTo": "verify_request", + "columnsFrom": [ + "request_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "verify_send_request_sequence_unique": { + "name": "verify_send_request_sequence_unique", + "nullsNotDistinct": false, + "columns": [ + "request_id", + "sequence" + ] + }, + "verify_send_environment_idempotency_key_unique": { + "name": "verify_send_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verify_settings": { + "name": "verify_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_name": { + "name": "app_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_length": { + "name": "code_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 6 + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 600 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "sms_from": { + "name": "sms_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_from": { + "name": "email_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_countries": { + "name": "allowed_countries", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{US,CA}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "verify_settings_environment_fk": { + "name": "verify_settings_environment_fk", + "tableFrom": "verify_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "verify_settings_environment_unique": { + "name": "verify_settings_environment_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.billing_account_status": { + "name": "billing_account_status", + "schema": "public", + "values": [ + "active", + "past_due", + "delinquent" + ] + }, + "public.billing_budget_scope": { + "name": "billing_budget_scope", + "schema": "public", + "values": [ + "member_default", + "user", + "project", + "api_key" + ] + }, + "public.billing_collection_method": { + "name": "billing_collection_method", + "schema": "public", + "values": [ + "charge_automatically", + "send_invoice" + ] + }, + "public.billing_contract_kind": { + "name": "billing_contract_kind", + "schema": "public", + "values": [ + "self_serve", + "sales", + "internal" + ] + }, + "public.billing_contract_status": { + "name": "billing_contract_status", + "schema": "public", + "values": [ + "pending", + "confirmed", + "canceled" + ] + }, + "public.billing_credit_entry_kind": { + "name": "billing_credit_entry_kind", + "schema": "public", + "values": [ + "applied", + "reversed", + "voided", + "rolled_over" + ] + }, + "public.billing_credit_note_status": { + "name": "billing_credit_note_status", + "schema": "public", + "values": [ + "pending", + "issued", + "failed" + ] + }, + "public.billing_grant_applies_to": { + "name": "billing_grant_applies_to", + "schema": "public", + "values": [ + "usage", + "any" + ] + }, + "public.billing_grant_kind": { + "name": "billing_grant_kind", + "schema": "public", + "values": [ + "amount", + "units" + ] + }, + "public.billing_grant_source": { + "name": "billing_grant_source", + "schema": "public", + "values": [ + "fee", + "commitment", + "support", + "promotion", + "refund", + "sla", + "threshold" + ] + }, + "public.billing_hold_reason": { + "name": "billing_hold_reason", + "schema": "public", + "values": [ + "delinquent", + "spend_limit", + "credit_limit", + "admin" + ] + }, + "public.billing_invoice_kind": { + "name": "billing_invoice_kind", + "schema": "public", + "values": [ + "period", + "plan_change", + "one_off", + "final", + "threshold" + ] + }, + "public.billing_invoice_status": { + "name": "billing_invoice_status", + "schema": "public", + "values": [ + "draft", + "open", + "paid", + "void", + "uncollectible" + ] + }, + "public.billing_line_kind": { + "name": "billing_line_kind", + "schema": "public", + "values": [ + "plan_fee", + "proration", + "usage", + "credit", + "discount", + "commitment_fee", + "commitment_shortfall", + "add_on", + "adjustment", + "write_off" + ] + }, + "public.billing_notification_status": { + "name": "billing_notification_status", + "schema": "public", + "values": [ + "pending", + "sent", + "skipped", + "failed" + ] + }, + "public.billing_provider": { + "name": "billing_provider", + "schema": "public", + "values": [ + "fake", + "stripe", + "manual" + ] + }, + "public.billing_provider_event_status": { + "name": "billing_provider_event_status", + "schema": "public", + "values": [ + "pending", + "processed", + "ignored", + "failed" + ] + }, + "public.billing_sync_action": { + "name": "billing_sync_action", + "schema": "public", + "values": [ + "push", + "collect", + "void", + "mark_uncollectible" + ] + }, + "public.billing_tax_mode": { + "name": "billing_tax_mode", + "schema": "public", + "values": [ + "none", + "stripe_automatic" + ] + }, + "public.api_key_access": { + "name": "api_key_access", + "schema": "public", + "values": [ + "full", + "restricted" + ] + }, + "public.auth_email_kind": { + "name": "auth_email_kind", + "schema": "public", + "values": [ + "verify_email", + "reset_password", + "invitation" + ] + }, + "public.auth_email_status": { + "name": "auth_email_status", + "schema": "public", + "values": [ + "pending", + "sent", + "skipped", + "failed" + ] + }, + "public.broadcast_screening_status": { + "name": "broadcast_screening_status", + "schema": "public", + "values": [ + "pending", + "passed", + "held", + "blocked" + ] + }, + "public.broadcast_status": { + "name": "broadcast_status", + "schema": "public", + "values": [ + "draft", + "scheduled", + "sending", + "paused", + "sent", + "canceled", + "failed" + ] + }, + "public.contact_property_type": { + "name": "contact_property_type", + "schema": "public", + "values": [ + "string", + "number", + "boolean", + "date" + ] + }, + "public.contact_source": { + "name": "contact_source", + "schema": "public", + "values": [ + "api", + "dashboard", + "import", + "preference_center", + "form" + ] + }, + "public.contact_status": { + "name": "contact_status", + "schema": "public", + "values": [ + "subscribed", + "unsubscribed", + "pending" + ] + }, + "public.contact_topic_status": { + "name": "contact_topic_status", + "schema": "public", + "values": [ + "opted_in", + "opted_out" + ] + }, + "public.content_screening_verdict": { + "name": "content_screening_verdict", + "schema": "public", + "values": [ + "clean", + "flagged", + "blocked", + "error" + ] + }, + "public.dedicated_ip_pool_status": { + "name": "dedicated_ip_pool_status", + "schema": "public", + "values": [ + "waiting", + "provisioning", + "warming", + "ready", + "releasing" + ] + }, + "public.dmarc_notification_kind": { + "name": "dmarc_notification_kind", + "schema": "public", + "values": [ + "ready_quarantine", + "ready_reject", + "pass_rate_dropped" + ] + }, + "public.dmarc_notification_status": { + "name": "dmarc_notification_status", + "schema": "public", + "values": [ + "pending", + "sent", + "skipped", + "failed" + ] + }, + "public.domain_status": { + "name": "domain_status", + "schema": "public", + "values": [ + "pending", + "verified", + "temporary_failure", + "failed", + "deleting" + ] + }, + "public.email_event_type": { + "name": "email_event_type", + "schema": "public", + "values": [ + "email.sent", + "email.delivered", + "email.delivery_delayed", + "email.bounced", + "email.complained", + "email.failed", + "email.suppressed", + "email.scheduled", + "email.canceled", + "email.opened", + "email.clicked", + "email.rescheduled", + "email.unsubscribed" + ] + }, + "public.email_recipient_kind": { + "name": "email_recipient_kind", + "schema": "public", + "values": [ + "to", + "cc", + "bcc" + ] + }, + "public.email_recipient_status": { + "name": "email_recipient_status", + "schema": "public", + "values": [ + "queued", + "sent", + "delivery_delayed", + "delivered", + "bounced", + "suppressed", + "complained", + "failed", + "canceled" + ] + }, + "public.email_status": { + "name": "email_status", + "schema": "public", + "values": [ + "queued", + "sent", + "failed", + "scheduled", + "canceled" + ] + }, + "public.email_stream": { + "name": "email_stream", + "schema": "public", + "values": [ + "transactional", + "marketing" + ] + }, + "public.email_subaccount_reputation": { + "name": "email_subaccount_reputation", + "schema": "public", + "values": [ + "good", + "warning", + "paused" + ] + }, + "public.email_subaccount_status": { + "name": "email_subaccount_status", + "schema": "public", + "values": [ + "active", + "disabled", + "deleting" + ] + }, + "public.email_suppression_reason": { + "name": "email_suppression_reason", + "schema": "public", + "values": [ + "bounce", + "complaint", + "manual", + "import", + "unsubscribe" + ] + }, + "public.environment_mode": { + "name": "environment_mode", + "schema": "public", + "values": [ + "live", + "sandbox" + ] + }, + "public.hook_delivery_status": { + "name": "hook_delivery_status", + "schema": "public", + "values": [ + "pending", + "succeeded", + "failed" + ] + }, + "public.hook_recovery_status": { + "name": "hook_recovery_status", + "schema": "public", + "values": [ + "pending", + "completed" + ] + }, + "public.log_drain_batch_status": { + "name": "log_drain_batch_status", + "schema": "public", + "values": [ + "pending", + "succeeded", + "failed" + ] + }, + "public.log_drain_format": { + "name": "log_drain_format", + "schema": "public", + "values": [ + "ndjson", + "loki", + "datadog", + "otlp" + ] + }, + "public.log_drain_source": { + "name": "log_drain_source", + "schema": "public", + "values": [ + "api", + "email", + "webhook", + "risk", + "audit", + "smtp", + "sms", + "sign", + "verify", + "forms", + "scheduling" + ] + }, + "public.log_level": { + "name": "log_level", + "schema": "public", + "values": [ + "info", + "warning", + "error" + ] + }, + "public.log_source": { + "name": "log_source", + "schema": "public", + "values": [ + "api", + "email", + "webhook", + "risk", + "smtp", + "sms", + "sign", + "verify", + "forms", + "scheduling" + ] + }, + "public.mail_from_status": { + "name": "mail_from_status", + "schema": "public", + "values": [ + "pending", + "success", + "temporary_failure", + "failed" + ] + }, + "public.migration_item_kind": { + "name": "migration_item_kind", + "schema": "public", + "values": [ + "domain", + "property", + "topic", + "audience", + "template", + "webhook_endpoint" + ] + }, + "public.migration_item_outcome": { + "name": "migration_item_outcome", + "schema": "public", + "values": [ + "pending", + "created", + "existing", + "skipped", + "failed" + ] + }, + "public.migration_provider": { + "name": "migration_provider", + "schema": "public", + "values": [ + "resend", + "sendgrid", + "postmark" + ] + }, + "public.migration_status": { + "name": "migration_status", + "schema": "public", + "values": [ + "inspecting", + "ready", + "queued", + "running", + "completed", + "failed", + "canceled" + ] + }, + "public.org_audit_actor_type": { + "name": "org_audit_actor_type", + "schema": "public", + "values": [ + "user", + "api_key", + "system" + ] + }, + "public.organization_deletion_status": { + "name": "organization_deletion_status", + "schema": "public", + "values": [ + "pending", + "completed" + ] + }, + "public.organization_logo_status": { + "name": "organization_logo_status", + "schema": "public", + "values": [ + "found", + "not_found", + "hidden" + ] + }, + "public.received_email_status": { + "name": "received_email_status", + "schema": "public", + "values": [ + "received", + "blocked" + ] + }, + "public.reputation_actor": { + "name": "reputation_actor", + "schema": "public", + "values": [ + "system", + "admin" + ] + }, + "public.reputation_event_type": { + "name": "reputation_event_type", + "schema": "public", + "values": [ + "warning_raised", + "warning_cleared", + "suspended", + "reinstated", + "policy_updated", + "marketing_paused", + "marketing_reinstated" + ] + }, + "public.reputation_notification_status": { + "name": "reputation_notification_status", + "schema": "public", + "values": [ + "pending", + "sent", + "skipped", + "failed" + ] + }, + "public.reputation_status": { + "name": "reputation_status", + "schema": "public", + "values": [ + "good", + "warning", + "suspended" + ] + }, + "public.risk_captcha_provider": { + "name": "risk_captcha_provider", + "schema": "public", + "values": [ + "turnstile", + "hcaptcha" + ] + }, + "public.risk_decision": { + "name": "risk_decision", + "schema": "public", + "values": [ + "allow", + "review", + "block" + ] + }, + "public.risk_event": { + "name": "risk_event", + "schema": "public", + "values": [ + "signup", + "login", + "message", + "checkout", + "custom" + ] + }, + "public.risk_feedback_source": { + "name": "risk_feedback_source", + "schema": "public", + "values": [ + "api", + "dashboard" + ] + }, + "public.risk_identity_kind": { + "name": "risk_identity_kind", + "schema": "public", + "values": [ + "mailbox", + "device" + ] + }, + "public.risk_limit_failure_mode": { + "name": "risk_limit_failure_mode", + "schema": "public", + "values": [ + "allow", + "deny" + ] + }, + "public.risk_list": { + "name": "risk_list", + "schema": "public", + "values": [ + "allow", + "block" + ] + }, + "public.risk_list_entry_type": { + "name": "risk_list_entry_type", + "schema": "public", + "values": [ + "email", + "domain", + "ip", + "asn", + "country", + "user" + ] + }, + "public.risk_outcome": { + "name": "risk_outcome", + "schema": "public", + "values": [ + "legitimate", + "fraud", + "abuse" + ] + }, + "public.risk_report_kind": { + "name": "risk_report_kind", + "schema": "public", + "values": [ + "mailbox", + "ip", + "subnet", + "user", + "card", + "device" + ] + }, + "public.risk_review_status": { + "name": "risk_review_status", + "schema": "public", + "values": [ + "open", + "resolved" + ] + }, + "public.risk_rule_match": { + "name": "risk_rule_match", + "schema": "public", + "values": [ + "all", + "any" + ] + }, + "public.risk_user_network_kind": { + "name": "risk_user_network_kind", + "schema": "public", + "values": [ + "country", + "asn" + ] + }, + "public.tenant_sending_status": { + "name": "tenant_sending_status", + "schema": "public", + "values": [ + "enabled", + "disabled", + "reinstated" + ] + }, + "public.topic_default_subscription": { + "name": "topic_default_subscription", + "schema": "public", + "values": [ + "opt_in", + "opt_out" + ] + }, + "public.topic_visibility": { + "name": "topic_visibility", + "schema": "public", + "values": [ + "public", + "private" + ] + }, + "public.tracking_domain_status": { + "name": "tracking_domain_status", + "schema": "public", + "values": [ + "pending", + "active", + "temporary_failure", + "failed", + "deleting" + ] + }, + "public.webhook_delivery_status": { + "name": "webhook_delivery_status", + "schema": "public", + "values": [ + "pending", + "succeeded", + "failed" + ] + }, + "public.form_captcha": { + "name": "form_captcha", + "schema": "public", + "values": [ + "off", + "required" + ] + }, + "public.form_delivery_kind": { + "name": "form_delivery_kind", + "schema": "public", + "values": [ + "notification", + "contact", + "verification" + ] + }, + "public.form_delivery_state": { + "name": "form_delivery_state", + "schema": "public", + "values": [ + "pending", + "delivered", + "failed", + "skipped" + ] + }, + "public.form_on_review": { + "name": "form_on_review", + "schema": "public", + "values": [ + "deliver", + "hold" + ] + }, + "public.form_spam_reason": { + "name": "form_spam_reason", + "schema": "public", + "values": [ + "honeypot", + "risk_block", + "risk_review", + "content" + ] + }, + "public.form_submission_status": { + "name": "form_submission_status", + "schema": "public", + "values": [ + "pending_confirmation", + "accepted", + "held", + "spam" + ] + }, + "public.inbox_kind": { + "name": "inbox_kind", + "schema": "public", + "values": [ + "managed", + "domain" + ] + }, + "public.inbox_message_direction": { + "name": "inbox_message_direction", + "schema": "public", + "values": [ + "inbound", + "outbound" + ] + }, + "public.notification_status": { + "name": "notification_status", + "schema": "public", + "values": [ + "pending", + "sent", + "skipped", + "failed" + ] + }, + "public.apns_environment": { + "name": "apns_environment", + "schema": "public", + "values": [ + "production", + "sandbox" + ] + }, + "public.push_notification_kind": { + "name": "push_notification_kind", + "schema": "public", + "values": [ + "reputation_warning", + "reputation_suspended", + "billing_payment_failed", + "billing_hold_placed", + "billing_budget_paused", + "webhook_endpoint_disabled", + "hook_endpoint_disabled", + "log_drain_disabled", + "risk_review" + ] + }, + "public.push_notification_status": { + "name": "push_notification_status", + "schema": "public", + "values": [ + "pending", + "sent", + "skipped", + "failed" + ] + }, + "public.scheduling_attendee_role": { + "name": "scheduling_attendee_role", + "schema": "public", + "values": [ + "attendee", + "guest" + ] + }, + "public.scheduling_booking_status": { + "name": "scheduling_booking_status", + "schema": "public", + "values": [ + "held", + "pending_approval", + "confirmed", + "cancelled", + "declined" + ] + }, + "public.scheduling_calendar_details": { + "name": "scheduling_calendar_details", + "schema": "public", + "values": [ + "neutral", + "full" + ] + }, + "public.scheduling_calendar_event_state": { + "name": "scheduling_calendar_event_state", + "schema": "public", + "values": [ + "present", + "absent" + ] + }, + "public.scheduling_calendar_failure_mode": { + "name": "scheduling_calendar_failure_mode", + "schema": "public", + "values": [ + "hide", + "last_known" + ] + }, + "public.scheduling_calendar_provider": { + "name": "scheduling_calendar_provider", + "schema": "public", + "values": [ + "google", + "microsoft", + "caldav", + "simulated", + "zoom" + ] + }, + "public.scheduling_connection_status": { + "name": "scheduling_connection_status", + "schema": "public", + "values": [ + "active", + "needs_reauth", + "delegation_failing" + ] + }, + "public.scheduling_delegation_provider": { + "name": "scheduling_delegation_provider", + "schema": "public", + "values": [ + "google", + "microsoft" + ] + }, + "public.scheduling_delegation_status": { + "name": "scheduling_delegation_status", + "schema": "public", + "values": [ + "pending", + "active", + "failing", + "deleting" + ] + }, + "public.scheduling_event_type_kind": { + "name": "scheduling_event_type_kind", + "schema": "public", + "values": [ + "one_on_one", + "pool", + "collective", + "group" + ] + }, + "public.scheduling_event_type_status": { + "name": "scheduling_event_type_status", + "schema": "public", + "values": [ + "active", + "archived" + ] + }, + "public.scheduling_external_state": { + "name": "scheduling_external_state", + "schema": "public", + "values": [ + "in_sync", + "moved", + "deleted" + ] + }, + "public.scheduling_host_role": { + "name": "scheduling_host_role", + "schema": "public", + "values": [ + "required", + "member" + ] + }, + "public.scheduling_host_status": { + "name": "scheduling_host_status", + "schema": "public", + "values": [ + "active", + "disabled" + ] + }, + "public.scheduling_notification_channel": { + "name": "scheduling_notification_channel", + "schema": "public", + "values": [ + "email", + "sms", + "webhook" + ] + }, + "public.scheduling_notification_recipient": { + "name": "scheduling_notification_recipient", + "schema": "public", + "values": [ + "attendee", + "host", + "endpoint", + "address" + ] + }, + "public.scheduling_oauth_app_provider": { + "name": "scheduling_oauth_app_provider", + "schema": "public", + "values": [ + "google", + "microsoft", + "zoom" + ] + }, + "public.scheduling_oauth_app_status": { + "name": "scheduling_oauth_app_status", + "schema": "public", + "values": [ + "active", + "failing" + ] + }, + "public.scheduling_out_of_office_existing": { + "name": "scheduling_out_of_office_existing", + "schema": "public", + "values": [ + "leave", + "reassign" + ] + }, + "public.scheduling_out_of_office_source": { + "name": "scheduling_out_of_office_source", + "schema": "public", + "values": [ + "api", + "host", + "manager", + "dashboard", + "holiday" + ] + }, + "public.scheduling_reassignment_host_notice": { + "name": "scheduling_reassignment_host_notice", + "schema": "public", + "values": [ + "digest", + "each", + "none" + ] + }, + "public.scheduling_reassignment_outcome": { + "name": "scheduling_reassignment_outcome", + "schema": "public", + "values": [ + "pending", + "reassigned", + "skipped_changed", + "no_candidate", + "failed", + "cancelled" + ] + }, + "public.scheduling_reassignment_status": { + "name": "scheduling_reassignment_status", + "schema": "public", + "values": [ + "pending", + "running", + "completed", + "cancelled" + ] + }, + "public.scheduling_session_kind": { + "name": "scheduling_session_kind", + "schema": "public", + "values": [ + "host", + "booker", + "manage", + "manager" + ] + }, + "public.sign_authentication": { + "name": "sign_authentication", + "schema": "public", + "values": [ + "email_link", + "customer_asserted" + ] + }, + "public.sign_delivery": { + "name": "sign_delivery", + "schema": "public", + "values": [ + "embedded", + "email" + ] + }, + "public.sign_event_type": { + "name": "sign_event_type", + "schema": "public", + "values": [ + "created", + "sent", + "notified", + "viewed", + "consented", + "signed", + "declined", + "reminded", + "canceled", + "expired", + "completed", + "sealed" + ] + }, + "public.sign_field_type": { + "name": "sign_field_type", + "schema": "public", + "values": [ + "signature", + "initials", + "date_signed", + "name", + "email", + "text", + "checkbox" + ] + }, + "public.sign_request_status": { + "name": "sign_request_status", + "schema": "public", + "values": [ + "draft", + "sent", + "completed", + "declined", + "canceled", + "expired" + ] + }, + "public.sign_signer_status": { + "name": "sign_signer_status", + "schema": "public", + "values": [ + "pending", + "notified", + "viewed", + "signed", + "declined" + ] + }, + "public.sign_signing_order": { + "name": "sign_signing_order", + "schema": "public", + "values": [ + "parallel", + "sequential" + ] + }, + "public.sms_account_status": { + "name": "sms_account_status", + "schema": "public", + "values": [ + "active", + "closing", + "closed" + ] + }, + "public.sms_event_type": { + "name": "sms_event_type", + "schema": "public", + "values": [ + "sms.scheduled", + "sms.sent", + "sms.delivered", + "sms.undelivered", + "sms.failed", + "sms.suppressed", + "sms.canceled" + ] + }, + "public.sms_number_kind": { + "name": "sms_number_kind", + "schema": "public", + "values": [ + "local", + "toll_free" + ] + }, + "public.sms_number_status": { + "name": "sms_number_status", + "schema": "public", + "values": [ + "pending", + "active", + "releasing" + ] + }, + "public.sms_registration_kind": { + "name": "sms_registration_kind", + "schema": "public", + "values": [ + "us_10dlc", + "toll_free" + ] + }, + "public.sms_registration_status": { + "name": "sms_registration_status", + "schema": "public", + "values": [ + "draft", + "submitted", + "in_review", + "approved", + "rejected", + "failed" + ] + }, + "public.sms_status": { + "name": "sms_status", + "schema": "public", + "values": [ + "queued", + "scheduled", + "sent", + "delivered", + "undelivered", + "failed", + "canceled" + ] + }, + "public.sms_suppression_reason": { + "name": "sms_suppression_reason", + "schema": "public", + "values": [ + "opt_out", + "manual", + "invalid_number" + ] + }, + "public.support_attachment_state": { + "name": "support_attachment_state", + "schema": "public", + "values": [ + "uploaded", + "pending_copy", + "stored", + "failed" + ] + }, + "public.support_delivery_status": { + "name": "support_delivery_status", + "schema": "public", + "values": [ + "pending", + "accepted", + "delivered", + "bounced", + "failed" + ] + }, + "public.support_message_kind": { + "name": "support_message_kind", + "schema": "public", + "values": [ + "customer", + "agent", + "note", + "event" + ] + }, + "public.support_org_match": { + "name": "support_org_match", + "schema": "public", + "values": [ + "session", + "email", + "manual" + ] + }, + "public.support_sender_auth": { + "name": "support_sender_auth", + "schema": "public", + "values": [ + "session", + "dmarc_pass", + "unverified" + ] + }, + "public.support_severity": { + "name": "support_severity", + "schema": "public", + "values": [ + "normal", + "urgent" + ] + }, + "public.support_ticket_source": { + "name": "support_ticket_source", + "schema": "public", + "values": [ + "email", + "dashboard" + ] + }, + "public.support_ticket_status": { + "name": "support_ticket_status", + "schema": "public", + "values": [ + "open", + "pending", + "snoozed", + "resolved", + "spam" + ] + }, + "public.verify_channel": { + "name": "verify_channel", + "schema": "public", + "values": [ + "sms", + "email" + ] + }, + "public.verify_send_route": { + "name": "verify_send_route", + "schema": "public", + "values": [ + "own_number", + "shared_number", + "email", + "coss_email" + ] + }, + "public.verify_status": { + "name": "verify_status", + "schema": "public", + "values": [ + "pending", + "approved", + "expired", + "canceled", + "failed", + "blocked" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index c8ccadb6a..12c9bd246 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -981,6 +981,13 @@ "when": 1793837456474, "tag": "0139_unusual_goblin_queen", "breakpoints": true + }, + { + "idx": 140, + "version": "7", + "when": 1793888126066, + "tag": "0140_living_forge", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/id.ts b/packages/db/src/id.ts index 7d945f6d0..6e610e65c 100644 --- a/packages/db/src/id.ts +++ b/packages/db/src/id.ts @@ -157,6 +157,7 @@ export const ID_PREFIXES = { schedulingEventType: "evtype", schedulingCalendarConnection: "calconn", schedulingCalendarDelegation: "caldlg", + schedulingOauthApp: "calapp", schedulingCalendar: "cal", schedulingClaim: "claim", schedulingBooking: "book", diff --git a/packages/db/src/schema/scheduling.ts b/packages/db/src/schema/scheduling.ts index e00a1a269..9fbf9ed27 100644 --- a/packages/db/src/schema/scheduling.ts +++ b/packages/db/src/schema/scheduling.ts @@ -164,6 +164,16 @@ export const schedulingDelegationProvider = pgEnum("scheduling_delegation_provid "google", "microsoft", ]); +export const schedulingOauthAppProvider = pgEnum("scheduling_oauth_app_provider", [ + "google", + "microsoft", + "zoom", +]); +export const schedulingOauthAppStatus = pgEnum("scheduling_oauth_app_status", [ + "active", + // The provider refused the app's own credentials. Hosts aren't touched: their calendars go stale. + "failing", +]); export const schedulingDelegationStatus = pgEnum("scheduling_delegation_status", [ "pending", "active", @@ -603,6 +613,49 @@ export const schedulingCalendarDelegation = pgTable( ], ); +/** + * A customer's own OAuth app for one provider, so consent screens show their brand. New consents in + * the environment use it; a connection stays on the app that issued its tokens (`oauth_app_id`). + */ +export const schedulingOauthApp = pgTable( + "scheduling_oauth_app", + { + id: text("id") + .primaryKey() + .$defaultFn(() => newId(ID_PREFIXES.schedulingOauthApp)), + organizationId: text("organization_id").notNull(), + projectId: text("project_id").notNull(), + environmentId: text("environment_id").notNull(), + provider: schedulingOauthAppProvider("provider").notNull(), + clientId: text("client_id").notNull(), + // Microsoft only: a single-tenant app can't use `common`. It goes into a URL, so it's validated. + tenantId: text("tenant_id"), + // `encryptSecret`, and listed in `reencryptWebhookSecrets`. Never returned. + encryptedCredentials: text("encrypted_credentials").notNull(), + // Null: coss.com's callback. Set: the customer's own page, which forwards there. + redirectUri: text("redirect_uri"), + // Where the browser lands afterwards when the consent didn't name a `return_url`. + completionUrl: text("completion_url"), + secretExpiresAt: instant("secret_expires_at"), + status: schedulingOauthAppStatus("status").notNull().default("active"), + lastErrorCode: text("last_error_code"), + lastErrorAt: instant("last_error_at"), + // A consent has completed through it, which is the first proof its secret works. + verifiedAt: instant("verified_at"), + createdAt: createdAt(), + updatedAt: updatedAt(), + }, + (t) => [ + foreignKey({ + name: "scheduling_oauth_app_environment_fk", + columns: [t.environmentId, t.organizationId], + foreignColumns: [environment.id, environment.organizationId], + }).onDelete("cascade"), + unique("scheduling_oauth_app_id_organization_unique").on(t.id, t.organizationId), + unique("scheduling_oauth_app_provider_unique").on(t.environmentId, t.provider), + ], +); + export const schedulingCalendarConnection = pgTable( "scheduling_calendar_connection", { @@ -620,6 +673,8 @@ export const schedulingCalendarConnection = pgTable( encryptedCredentials: text("encrypted_credentials"), // Set: the delegation's credentials reach this mailbox, and it isn't the host's to end. delegationId: text("delegation_id"), + // The customer's app that issued its tokens, which only that app can refresh. Null: coss.com's. + oauthAppId: text("oauth_app_id"), // When a delegation first reached this mailbox. Null with one: it never has, so a host's calendars // here are only a stand-in, and there are no events. reachedAt: instant("reached_at"), @@ -643,7 +698,16 @@ export const schedulingCalendarConnection = pgTable( schedulingCalendarDelegation.organizationId, ], }).onDelete("cascade"), + // No cascade: deleting an app with connections is refused, as their tokens would be useless. + foreignKey({ + name: "scheduling_calendar_connection_oauth_app_fk", + columns: [t.oauthAppId, t.organizationId], + foreignColumns: [schedulingOauthApp.id, schedulingOauthApp.organizationId], + }), unique("scheduling_calendar_connection_id_organization_unique").on(t.id, t.organizationId), + index("scheduling_calendar_connection_oauth_app_idx") + .on(t.oauthAppId) + .where(sql`${t.oauthAppId} IS NOT NULL`), index("scheduling_calendar_connection_delegation_idx") .on(t.delegationId) .where(sql`${t.delegationId} IS NOT NULL`), @@ -1458,6 +1522,10 @@ export const schedulingOauthState = pgTable( stateHash: text("state_hash").notNull(), encryptedCodeVerifier: text("encrypted_code_verifier").notNull(), returnUrl: text("return_url"), + // Fixed when the consent starts: a code can only be exchanged with what it was issued for. + oauthAppId: text("oauth_app_id"), + redirectUri: text("redirect_uri"), + tenantId: text("tenant_id"), expiresAt: instant("expires_at").notNull(), createdAt: createdAt(), }, @@ -1467,6 +1535,11 @@ export const schedulingOauthState = pgTable( columns: [t.hostId, t.organizationId], foreignColumns: [schedulingHost.id, schedulingHost.organizationId], }).onDelete("cascade"), + foreignKey({ + name: "scheduling_oauth_state_oauth_app_fk", + columns: [t.oauthAppId, t.organizationId], + foreignColumns: [schedulingOauthApp.id, schedulingOauthApp.organizationId], + }).onDelete("cascade"), unique("scheduling_oauth_state_hash_unique").on(t.stateHash), index("scheduling_oauth_state_expires_at_idx").on(t.expiresAt), ], @@ -1484,6 +1557,8 @@ export const schedulingSettings = pgTable( allowedOrigins: jsonb("allowed_origins").$type().notNull().default([]), brandName: text("brand_name"), accentColor: text("accent_color"), + // What the host's page links to before a consent, with `brand_name` and the customer's own app. + privacyUrl: text("privacy_url"), emailFrom: text("email_from"), emailReplyTo: text("email_reply_to"), smsFrom: text("sms_from"), diff --git a/packages/email/src/test-event-schemas.ts b/packages/email/src/test-event-schemas.ts index 1f02ed6db..32c9b03c8 100644 --- a/packages/email/src/test-event-schemas.ts +++ b/packages/email/src/test-event-schemas.ts @@ -12,6 +12,7 @@ import { SchedulingBookingWebhookEvent, SchedulingCalendarDelegationWebhookEvent, SchedulingCalendarWebhookEvent, + SchedulingOauthAppWebhookEvent, SchedulingOutOfOfficeWebhookEvent, SchedulingReassignmentWebhookEvent, } from "@coss/scheduling/schemas"; @@ -82,6 +83,7 @@ export function webhookEventSchema(type: TestWebhookEventType): z.ZodType { } if (type.startsWith("scheduling.out_of_office.")) return SchedulingOutOfOfficeWebhookEvent; if (type.startsWith("scheduling.reassignment.")) return SchedulingReassignmentWebhookEvent; + if (type.startsWith("scheduling.oauth_app.")) return SchedulingOauthAppWebhookEvent; if (type.startsWith("scheduling.")) return SchedulingCalendarWebhookEvent; if (type.startsWith("email.")) return EmailWebhookEvent; if (type.startsWith("hook_endpoint.")) return HookEndpointWebhookEvent; @@ -168,6 +170,8 @@ export const WebhookTestEventObject = z SchedulingCalendarDelegationWebhookEvent, SchedulingOutOfOfficeWebhookEvent, SchedulingReassignmentWebhookEvent, + SchedulingCalendarDelegationWebhookEvent, + SchedulingOauthAppWebhookEvent, ]) .meta({ description: diff --git a/packages/email/src/test-events.ts b/packages/email/src/test-events.ts index 8b2f6b51c..6ce2ef0b8 100644 --- a/packages/email/src/test-events.ts +++ b/packages/email/src/test-events.ts @@ -299,6 +299,14 @@ export function sampleWebhookEvent(type: TestWebhookEventType, now: Date): Store code: type.endsWith(".failing") ? "unauthorized_client" : null, occurred_at: at, }; + } else if (type.startsWith("scheduling.oauth_app.")) { + data = { + ...base, + oauth_app_id: newId(ID_PREFIXES.schedulingOauthApp), + provider: "google", + code: type.endsWith(".failing") ? "app_refused" : null, + occurred_at: at, + }; } else if (type.startsWith("scheduling.calendar.")) { data = { ...base, diff --git a/packages/scheduling/AGENTS.md b/packages/scheduling/AGENTS.md index 5c9818b11..fcf73cfee 100644 --- a/packages/scheduling/AGENTS.md +++ b/packages/scheduling/AGENTS.md @@ -306,6 +306,48 @@ once, and every host on it gets their calendar with no consent screen. domain-wide delegation, Graph ids matching between `/me` and `/users/{id}`, app-only subscriptions and Teams links) needs checking against a real Workspace and tenant when any of this changes. +### Customers' own OAuth apps + +`calendar/oauth-apps.ts` (managing them, plan-gated on `schedulingOauthApps` when created), `oauth-app-state.ts` (reading +credentials, and refusals; imported by `connections.ts`, so it can't import that). One app per environment and provider, so +consent screens show the customer's brand. + +- **A connection is pinned to the app that issued its tokens** (`oauth_app_id`, null for coss.com's), because only that + app can refresh or revoke them. Never look an app up by environment for an existing connection: adding an app would + knock out everyone connected before it. A re-consent moves the pin, and `client_id` never changes on a row, so a + different app means new consents. +- **A consent is pinned when it starts** (`scheduling_oauth_state.oauth_app_id`, `redirect_uri`, `tenant_id`): a code is exchanged with + what it was issued for, whatever's been edited since. +- **No fallback**: a failing app refuses new consents rather than using coss.com's, which would show the wrong brand and + pin the connection to it. With no app of its own, an environment uses `deps.apps` as before. +- **A bad secret isn't a host's problem.** `invalid_client` and `deleted_client` become the `transient` `app_refused`, never + `auth`, so no host is marked or emailed, and `calendarVerdict` hides them when their data goes stale. One outage is one + compare-and-set on the app (`markOauthAppFailing`, only if it still holds the secret that was refused), so one webhook, + log line and owner email. The same refusal of coss.com's own app goes to Sentry. `unauthorized_client` stays `auth`: + Microsoft answers that for a single tenant that disallows the app, which says nothing about the secret. +- **A new secret is tried first** (`checkOnConnections`): a real token through the refresh path, keeping what comes back, + since Zoom's work once. With no connections, `checkWithoutConnections` tries a token that can't exist: it only refuses on + `app_refused`, so passing doesn't prove the secret where a provider checks the token first. `verified_at`, set by the + first consent, does. Any successful refresh ends an outage too (`oauthAppAccepted`). +- **Zoom's deauthorization events are per app** (`POST /scheduling/zoom/events/:appId`, its own Secret Token), and + `handleZoomDeauthorization` only reaches that app's connections, or with no id, coss.com's. A customer holds their token, + so an unscoped handler would let them delete other organizations' connections. Keep the filter. Zoom doesn't send these + events for unpublished apps. +- **A forwarding page** (`redirect_uri`) is the customer's own, because Google verifies redirect domains as the app owner's, + and nobody else can verify `api.coss.com`. It sends `code` and `state` to the unchanged callback. Only the customer's own + app is ever sent there, so it isn't an open redirect. `completion_url` fills in a consent's `return_url`, and is stored on + the state like one. +- **Deleting** is refused while connections reference the app (a foreign key with no cascade, and `FOR UPDATE` first). It's + NO ACTION rather than RESTRICT, so deleting an environment or organization still takes both in one statement. +- **Plans** gate creating only, like every feature here: a lapsed contract keeps working, and rotating or deleting always + works, since refusing would take hosts' calendars out. `downgradeBlockers` refuses a self-serve downgrade while apps exist. +- Provider behaviour the fakes can't prove needs a real app when this changes: which error each provider gives a bad secret, + whether Microsoft checks the app before the token, Google's verification with a forwarding page, and channels under a + customer's project. + +A Meet or Teams link becomes a `link` location on the booking, stored with `conference` (stripped from the wire), so a +new host gets a meeting of their own (`locationForNewHost`). A browser only chooses among an event type's `locations` +(`locationFromBrowser`): it can't supply its own link or address. ## Team pages `manager.ts`: a `manager` session reaches the hosts it names, or those matching `manager_host_filter` on every request diff --git a/packages/scheduling/src/calendar/connections.ts b/packages/scheduling/src/calendar/connections.ts index 5dd48696e..cfaaadbe8 100644 --- a/packages/scheduling/src/calendar/connections.ts +++ b/packages/scheduling/src/calendar/connections.ts @@ -25,6 +25,7 @@ import { recordCalendarEvent } from "./events.ts"; import { googleCalendarClient } from "./google.ts"; import { microsoftCalendarClient } from "./microsoft.ts"; import { + APP_REFUSED, authorizationUrl, exchangeCode, type OAuthApp, @@ -34,6 +35,14 @@ import { refreshTokens, revokeToken, } from "./oauth.ts"; +import { + environmentOauthApp, + markOauthAppFailing, + type OauthAppRow, + oauthAppOf, + pinnedOauthApp, + restoreOauthApp, +} from "./oauth-app-state.ts"; import { simulatedCalendarClient } from "./simulated.ts"; import { ZOOM_CALENDAR_ID, zoomCalendarClient, zoomUserId } from "./zoom.ts"; @@ -59,7 +68,7 @@ type Env = Scope & { projectId: string; mode: "live" | "sandbox" }; export type CalendarDeps = { secretsKeys: SecretsKeyRing; - /** Coss.com's OAuth apps. A provider without one can't be connected. */ + /** Coss.com's OAuth apps. Without one, only an environment's own app can connect a provider. */ apps: { google: OAuthApp | null; microsoft: OAuthApp | null; @@ -86,7 +95,7 @@ const REQUEST_TIMEOUT_MS = 30_000; // Past a year: a weekly series of 52 has to fit, or its last occurrences couldn't be verified. export const SYNC_HORIZON_DAYS = 400; -export const fetcherOf = (deps: CalendarDeps): HttpFetch => { +export const fetcherOf = (deps: Pick): HttpFetch => { const fetcher = deps.fetcher ?? fetch; const timeoutMs = deps.requestTimeoutMs ?? REQUEST_TIMEOUT_MS; return async (url, init) => { @@ -101,7 +110,8 @@ export const fetcherOf = (deps: CalendarDeps): HttpFetch => { } }; }; -export const nowMs = (deps: CalendarDeps): number => (deps.now?.() ?? new Date()).getTime(); +export const nowMs = (deps: Pick): number => + (deps.now?.() ?? new Date()).getTime(); export const oauthRedirectUri = (deps: Pick, provider: OAuthProvider) => `${withoutTrailingSlashes(deps.publicUrl)}/scheduling/oauth/${provider}/callback`; @@ -120,6 +130,19 @@ const appOf = (deps: CalendarDeps, provider: OAuthProvider): OAuthApp => { return app; }; +/** The app a connection's tokens came from: only that one can refresh or revoke them. */ +async function appOfConnection( + connection: Pick, + provider: OAuthProvider, + deps: CalendarDeps, +): Promise<{ app: OAuthApp; own: OauthAppRow | null }> { + if (!connection.oauthAppId) return { app: appOf(deps, provider), own: null }; + const own = await pinnedOauthApp(connection.oauthAppId, connection.organizationId); + // A foreign key keeps it while the connection exists, so this is a read of a row just deleted. + if (!own) throw new CalendarError("transient", "oauth_app_gone", "This OAuth app is gone."); + return { app: oauthAppOf(own, deps.secretsKeys), own }; +} + const sha256 = (value: string) => createHash("sha256").update(value).digest("hex"); /** Starts a consent: the state is random, and only its hash is kept. It's never a credential. */ @@ -129,7 +152,16 @@ export async function startCalendarAuthorization( input: { provider: OAuthProvider; returnUrl: string | null }, deps: CalendarDeps, ): Promise<{ url: string }> { - const app = appOf(deps, input.provider); + const own = await environmentOauthApp(scope, input.provider); + // Never coss.com's app instead: it would show the wrong brand, and stay pinned to it. + if (own?.status === "failing") { + throw new SchedulingError( + "calendar_unavailable", + `This environment's ${input.provider} OAuth app was refused by the provider. Update its client secret.`, + ); + } + const app = own ? oauthAppOf(own, deps.secretsKeys) : appOf(deps, input.provider); + const redirectUri = own?.redirectUri ?? oauthRedirectUri(deps, input.provider); const host = await resolveHost(db, scope, hostRef); const state = randomBytes(32).toString("base64url"); const { verifier, challenge } = pkcePair(); @@ -140,14 +172,17 @@ export async function startCalendarAuthorization( provider: input.provider, stateHash: sha256(state), encryptedCodeVerifier: encryptSecret(verifier, deps.secretsKeys), - returnUrl: input.returnUrl, + returnUrl: input.returnUrl ?? own?.completionUrl ?? null, + oauthAppId: own?.id ?? null, + redirectUri, + tenantId: own?.tenantId ?? null, expiresAt: new Date(nowMs(deps) + OAUTH_STATE_TTL_MS), }); return { url: authorizationUrl({ provider: input.provider, app, - redirectUri: oauthRedirectUri(deps, input.provider), + redirectUri, state, codeChallenge: challenge, }), @@ -206,6 +241,7 @@ async function saveConnection( accountId: string; encryptedCredentials: string | null; client: CalendarClient; + oauthAppId?: string | null; }, deps: Pick, ): Promise { @@ -246,6 +282,7 @@ async function saveConnection( provider: input.provider, accountId: input.accountId, encryptedCredentials: input.encryptedCredentials, + oauthAppId: input.oauthAppId ?? null, }) .onConflictDoUpdate({ target: [ @@ -255,6 +292,8 @@ async function saveConnection( ], set: { encryptedCredentials: input.encryptedCredentials, + // A new consent can come through a different app, and only that one can refresh these. + oauthAppId: input.oauthAppId ?? null, status: "active", lastErrorCode: null, lastErrorAt: null, @@ -308,16 +347,26 @@ export async function completeCalendarAuthorization( return { returnUrl: state?.returnUrl ?? null, outcome: "invalid" }; } if (input.error || !input.code) return { returnUrl: state.returnUrl, outcome: "denied" }; - const app = appOf(deps, input.provider); - const tokens = await exchangeCode({ - provider: input.provider, - app, - code: input.code, - codeVerifier: decryptSecret(state.encryptedCodeVerifier, deps.secretsKeys), - redirectUri: oauthRedirectUri(deps, input.provider), - fetcher: fetcherOf(deps), - nowMs: nowMs(deps), - }); + // What the consent started with, never what the environment has now. + const { app, own } = await appOfConnection(state, input.provider, deps); + let tokens: OAuthTokens; + try { + tokens = await exchangeCode({ + provider: input.provider, + // The authority that issued this code, whatever the app says now. + app: own ? { ...app, tenantId: state.tenantId } : app, + code: input.code, + codeVerifier: decryptSecret(state.encryptedCodeVerifier, deps.secretsKeys), + redirectUri: state.redirectUri ?? oauthRedirectUri(deps, input.provider), + fetcher: fetcherOf(deps), + nowMs: nowMs(deps), + }); + } catch (error) { + if (own && error instanceof CalendarError && error.code === APP_REFUSED) { + await markOauthAppFailing(own, error.code, new Date(nowMs(deps))); + } + throw error; + } const client = oauthClient(input.provider, tokens.accessToken, deps); const accountId = await oauthAccountId(input.provider, tokens.accessToken, client, deps); await saveConnection( @@ -331,9 +380,11 @@ export async function completeCalendarAuthorization( accountId, encryptedCredentials: encryptCredentials(tokens, deps.secretsKeys), client, + oauthAppId: own?.id ?? null, }, deps, ); + if (own) await oauthAppAccepted(own, deps); return { returnUrl: state.returnUrl, outcome: "connected", hostId: state.hostId }; } @@ -547,12 +598,31 @@ export async function setSimulatedBusy( }); } +/** The provider took this app's secret: the first proof it works, and the end of any outage. */ +async function oauthAppAccepted(own: OauthAppRow, deps: Pick): Promise { + const now = deps.now?.() ?? new Date(); + if (!own.verifiedAt) { + await db + .update(schema.schedulingOauthApp) + .set({ verifiedAt: now }) + .where( + and( + eq(schema.schedulingOauthApp.id, own.id), + eq(schema.schedulingOauthApp.organizationId, own.organizationId), + ), + ); + } + // Whatever it said when this began: another request may have been refused since, and this secret + // has just worked. The compare-and-set makes it a no-op on a healthy app. + await restoreOauthApp(own, now); +} + /** A re-encrypted row still holds the token used; any other value is newer, and stays. */ -async function saveRefreshedTokens( +export async function saveRefreshedTokens( connection: ConnectionRow, usedRefreshToken: string, tokens: OAuthTokens, - deps: CalendarDeps, + deps: Pick, ): Promise { await db.transaction(async (tx) => { const where = and( @@ -601,15 +671,21 @@ export async function clientFor( if (!usedRefreshToken) { throw new CalendarError("auth", "no_refresh_token", "Calendar access has expired."); } + const { app, own } = await appOfConnection(connection, provider, deps); try { tokens = await refreshTokens({ provider, - app: appOf(deps, provider), + app, refreshToken: usedRefreshToken, fetcher: fetcherOf(deps), nowMs: nowMs(deps), }); } catch (error) { + // One outage of the app, not one per host. Coss.com's own is ours to hear about. + if (error instanceof CalendarError && error.code === APP_REFUSED) { + if (own) await markOauthAppFailing(own, error.code, new Date(nowMs(deps))); + else console.error(`[scheduling] ${provider} refused coss.com's OAuth app`, error); + } // Zoom's refresh tokens work once, so another worker may just have used this one. if (error instanceof CalendarError && error.kind === "auth") { const [current] = await db @@ -626,6 +702,7 @@ export async function clientFor( throw error; } await saveRefreshedTokens(connection, usedRefreshToken, tokens, deps); + if (own) await oauthAppAccepted(own, deps); } return oauthClient(provider, tokens.accessToken, deps); } @@ -713,6 +790,8 @@ export type ConnectionObject = { status: "active" | "needs_reauth"; /** The delegation that manages it: the host can't disconnect or reconnect it. */ delegation: string | null; + /** Your own OAuth app its access came through. Null: coss.com's, or there's no consent. */ + oauth_app: string | null; calendars: { id: string; external_id: string; @@ -747,6 +826,7 @@ async function toConnectionObjects( // A customer's code only needs to know the host's out; `delegation` says whose problem it is. status: row.status === "active" ? "active" : "needs_reauth", delegation: row.delegationId, + oauth_app: row.oauthAppId, calendars: calendars .filter( (calendar) => @@ -920,10 +1000,15 @@ export async function disconnectCalendar( return { id: connection.id }; } -/** Someone removed the app in Zoom: every connection of that Zoom user goes, in any organization. */ +/** + * Someone removed an app in Zoom: that Zoom user's connections through that app go, and nobody else's. + * Coss.com's app (`oauthAppId` null) covers any organization that used it. A customer's holds its own + * event token, so it must never reach a connection that came through another app. + */ export async function handleZoomDeauthorization( zoomUser: string, deps: CalendarDeps, + oauthAppId: string | null = null, ): Promise { const connections = await db .select() @@ -932,6 +1017,9 @@ export async function handleZoomDeauthorization( and( eq(schedulingCalendarConnection.provider, "zoom"), eq(schedulingCalendarConnection.accountId, zoomUser), + oauthAppId + ? eq(schedulingCalendarConnection.oauthAppId, oauthAppId) + : isNull(schedulingCalendarConnection.oauthAppId), ), ); for (const connection of connections) { @@ -1082,16 +1170,21 @@ export async function releaseConnection( } // A delegation's tokens aren't this host's to revoke. if (connection.encryptedCredentials && !connection.delegationId) { - const tokens = JSON.parse( - decryptSecret(connection.encryptedCredentials, deps.secretsKeys), - ) as OAuthTokens; - if (tokens.refreshToken) { - await revokeToken( - connection.provider, - tokens.refreshToken, - fetcherOf(deps), - deps.apps[connection.provider], + // As it is now: `clientFor` may just have spent the refresh token this row was read with. + const [current] = await db + .select({ stored: schedulingCalendarConnection.encryptedCredentials }) + .from(schedulingCalendarConnection) + .where( + and( + eq(schedulingCalendarConnection.id, connection.id), + eq(schedulingCalendarConnection.organizationId, connection.organizationId), + ), ); + const stored = current?.stored ?? connection.encryptedCredentials; + const tokens = JSON.parse(decryptSecret(stored, deps.secretsKeys)) as OAuthTokens; + if (tokens.refreshToken) { + const { app } = await appOfConnection(connection, connection.provider, deps); + await revokeToken(connection.provider, tokens.refreshToken, fetcherOf(deps), app); } } } catch (error) { diff --git a/packages/scheduling/src/calendar/delegation-notifications.ts b/packages/scheduling/src/calendar/delegation-notifications.ts index 97776ad46..6879cad9d 100644 --- a/packages/scheduling/src/calendar/delegation-notifications.ts +++ b/packages/scheduling/src/calendar/delegation-notifications.ts @@ -8,7 +8,7 @@ import { db, schema } from "@coss/db"; import type { NotificationContext, NotificationKinds } from "@coss/notifications"; import { and, eq } from "drizzle-orm"; -const { environment, project, schedulingCalendarDelegation } = schema; +const { environment, project, schedulingCalendarDelegation, schedulingOauthApp } = schema; /** How long before a client secret expires its owners are warned. */ export const SECRET_WARNING_MS = 14 * 86_400_000; @@ -34,6 +34,44 @@ async function delegationOf(context: NotificationContext) { return row ?? null; } +async function oauthAppOf(context: NotificationContext) { + const id = context.payload.oauthAppId; + if (typeof id !== "string") return null; + const [row] = await db + .select({ + app: schedulingOauthApp, + projectSlug: project.slug, + environmentSlug: environment.slug, + }) + .from(schedulingOauthApp) + .innerJoin(environment, eq(environment.id, schedulingOauthApp.environmentId)) + .innerJoin(project, eq(project.id, schedulingOauthApp.projectId)) + .where( + and( + eq(schedulingOauthApp.id, id), + eq(schedulingOauthApp.organizationId, context.organizationId), + ), + ); + return row ?? null; +} + +const PROVIDER_NAMES = { google: "Google", microsoft: "Microsoft", zoom: "Zoom" }; + +const oauthAppPage = ( + context: NotificationContext, + row: { projectSlug: string; environmentSlug: string }, +) => + context.url( + environmentHref( + { + organization: context.organization.slug, + project: row.projectSlug, + environment: row.environmentSlug, + }, + "/scheduling/oauth-apps", + ), + ); + const pageOf = ( context: NotificationContext, row: { projectSlug: string; environmentSlug: string }, @@ -98,4 +136,50 @@ export const SCHEDULING_NOTIFICATION_KINDS: NotificationKinds = { }; }, }, + "scheduling.oauth_app_failing": { + async render(context) { + const row = await oauthAppOf(context); + if (!row) return { skip: "the app is gone" }; + if (row.app.status !== "failing") return { skip: "it works again" }; + const provider = PROVIDER_NAMES[row.app.provider]; + return { + subject: `${provider} refused your OAuth app's credentials`, + blocks: [ + { + paragraph: `${provider} refused the client ID and secret of your own OAuth app, so calendars connected through it can't be read. Hosts stop being offered once what's known of their calendars is out of date, and new connections are refused. Existing bookings stay as they are.`, + }, + { + paragraph: + "Update the client secret, and they come back by themselves. Nobody has to reconnect anything.", + }, + { button: { label: "Update the secret", url: oauthAppPage(context, row) } }, + ], + }; + }, + }, + "scheduling.oauth_app_secret_expiring": { + async render(context) { + const row = await oauthAppOf(context); + if (!row) return { skip: "the app is gone" }; + const expires = row.app.secretExpiresAt; + if ( + !expires || + expires < context.now || + expires.getTime() - context.now.getTime() > SECRET_WARNING_MS + ) { + return { skip: "no secret about to expire" }; + } + const provider = PROVIDER_NAMES[row.app.provider]; + return { + subject: `The client secret of your ${provider} OAuth app expires soon`, + blocks: [ + { + paragraph: `The client secret of your own ${provider} OAuth app expires on ${expires.toISOString().slice(0, 10)}. When it does, calendars connected through it can't be read, and hosts stop being offered for new bookings.`, + }, + { paragraph: `Create a new secret at ${provider}, then paste it into coss.com.` }, + { button: { label: "Update the secret", url: oauthAppPage(context, row) } }, + ], + }; + }, + }, }; diff --git a/packages/scheduling/src/calendar/delegation.ts b/packages/scheduling/src/calendar/delegation.ts index 79527e22d..d99118328 100644 --- a/packages/scheduling/src/calendar/delegation.ts +++ b/packages/scheduling/src/calendar/delegation.ts @@ -660,6 +660,8 @@ async function connectHost( const upgraded = host.existing ? null : await connectionToUpgrade(tx, delegation, host); const fields = { delegationId: delegation.id, + // Taken over from a host's own consent, the delegation's credentials are all that matter. + oauthAppId: null, reachedAt: now, encryptedCredentials: encryptCredentials(tokens, deps.secretsKeys), status: "active" as const, diff --git a/packages/scheduling/src/calendar/oauth-app-state.ts b/packages/scheduling/src/calendar/oauth-app-state.ts new file mode 100644 index 000000000..10ac40976 --- /dev/null +++ b/packages/scheduling/src/calendar/oauth-app-state.ts @@ -0,0 +1,197 @@ +/** + * A customer's OAuth app as `connections.ts` needs it: reading its credentials, and the provider + * refusing them. Apart from `oauth-apps.ts` (managing them), which needs `connections.ts`. + * + * A refusal is one outage of one app, so it's one webhook, one log line and one email to the owners. + * Hosts aren't told, since they can't fix it, and they aren't marked: their calendars go stale, and + * `calendarVerdict` decides when that stops them being offered, as for any calendar that can't be read. + */ +import { db, type Executor, schema } from "@coss/db"; +import { ID_PREFIXES, isId, newId } from "@coss/db/id"; +import { appendLogEntries } from "@coss/logs"; +import { enqueueNotification } from "@coss/notifications"; +import { + decryptSecret, + encryptSecret, + enqueueWebhookMessage, + type SecretsKeyRing, +} from "@coss/webhooks"; +import { and, eq } from "drizzle-orm"; +import type { SchedulingOauthAppWebhookEventType } from "../webhook-types.ts"; +import type { OAuthApp, OAuthProvider } from "./oauth.ts"; + +const { schedulingOauthApp } = schema; + +export type OauthAppRow = typeof schedulingOauthApp.$inferSelect; + +export type OauthAppCredentials = { + clientSecret: string; + /** Zoom's Secret Token, which signs the app's deauthorization events. */ + webhookSecret?: string | null; +}; + +export const encryptOauthAppCredentials = ( + credentials: OauthAppCredentials, + keys: SecretsKeyRing, +): string => encryptSecret(JSON.stringify(credentials), keys); + +export const oauthAppCredentials = ( + row: Pick, + keys: SecretsKeyRing, +): OauthAppCredentials => + JSON.parse(decryptSecret(row.encryptedCredentials, keys)) as OauthAppCredentials; + +export const oauthAppOf = (row: OauthAppRow, keys: SecretsKeyRing): OAuthApp => ({ + clientId: row.clientId, + clientSecret: oauthAppCredentials(row, keys).clientSecret, + tenantId: row.tenantId, +}); + +/** The app new consents of this environment use. Null: coss.com's. */ +export async function environmentOauthApp( + scope: { organizationId: string; environmentId: string }, + provider: OAuthProvider, + executor: Executor = db, +): Promise { + const [row] = await executor + .select() + .from(schedulingOauthApp) + .where( + and( + eq(schedulingOauthApp.organizationId, scope.organizationId), + eq(schedulingOauthApp.environmentId, scope.environmentId), + eq(schedulingOauthApp.provider, provider), + ), + ); + return row ?? null; +} + +/** The app a connection or a consent is pinned to. */ +export async function pinnedOauthApp( + id: string, + organizationId: string, +): Promise { + const [row] = await db + .select() + .from(schedulingOauthApp) + .where( + and(eq(schedulingOauthApp.id, id), eq(schedulingOauthApp.organizationId, organizationId)), + ); + return row ?? null; +} + +/** + * The app Zoom's event names in its URL. The id comes from an unauthenticated request, so nothing + * here is trusted until the event's signature has been checked with this app's token. + */ +export async function zoomOauthAppForEvents(id: string): Promise { + if (!isId(ID_PREFIXES.schedulingOauthApp, id)) return null; + const [row] = await db + .select() + .from(schedulingOauthApp) + .where(and(eq(schedulingOauthApp.id, id), eq(schedulingOauthApp.provider, "zoom"))); + return row ?? null; +} + +async function announce( + tx: Parameters[0], + app: OauthAppRow, + type: SchedulingOauthAppWebhookEventType, + code: string | null, + now: Date, +): Promise { + const id = newId(ID_PREFIXES.webhookEvent); + await enqueueWebhookMessage(tx, { + id, + organizationId: app.organizationId, + environmentId: app.environmentId, + type, + payload: { + id, + object: "event", + type, + created_at: now.toISOString(), + data: { oauth_app_id: app.id, provider: app.provider, code, occurred_at: now.toISOString() }, + }, + }); + const failing = type === "scheduling.oauth_app.failing"; + await appendLogEntries(tx, [ + { + organizationId: app.organizationId, + projectId: app.projectId, + environmentId: app.environmentId, + occurredAt: now, + source: "scheduling", + level: failing ? "error" : "info", + type, + message: failing + ? `Your ${app.provider} OAuth app's credentials were refused: calendars connected through it can't be read` + : `Your ${app.provider} OAuth app's credentials work again`, + resourceType: "scheduling_oauth_app", + resourceId: app.id, + attributes: { provider: app.provider, ...(code ? { code } : {}) }, + }, + ]); +} + +/** + * Once per outage (compare-and-set). Only if the row still holds the secret that was refused: a + * request on its way while someone rotates it says nothing about the new one. + */ +export async function markOauthAppFailing( + app: Pick, + code: string, + now: Date, +): Promise { + await db.transaction(async (tx) => { + const [changed] = await tx + .update(schedulingOauthApp) + .set({ status: "failing", lastErrorCode: code, lastErrorAt: now }) + .where( + and( + eq(schedulingOauthApp.id, app.id), + eq(schedulingOauthApp.organizationId, app.organizationId), + eq(schedulingOauthApp.status, "active"), + eq(schedulingOauthApp.encryptedCredentials, app.encryptedCredentials), + ), + ) + .returning(); + if (!changed) return; + await announce(tx, changed, "scheduling.oauth_app.failing", code, now); + await enqueueNotification(tx, { + organizationId: changed.organizationId, + kind: "scheduling.oauth_app_failing", + dedupeKey: `${changed.id}:${now.getTime()}`, + payload: { oauthAppId: changed.id }, + now, + }); + }); +} + +/** + * The provider accepted it again, and calendars recover on their own next read. Only if the row still + * holds the secret that was accepted: it says nothing about one saved since. + */ +export async function restoreOauthApp( + app: Pick, + now: Date, + tx?: Parameters[0], +): Promise { + const restore = async (executor: Parameters[0]) => { + const [changed] = await executor + .update(schedulingOauthApp) + .set({ status: "active", lastErrorCode: null, lastErrorAt: null }) + .where( + and( + eq(schedulingOauthApp.id, app.id), + eq(schedulingOauthApp.organizationId, app.organizationId), + eq(schedulingOauthApp.status, "failing"), + eq(schedulingOauthApp.encryptedCredentials, app.encryptedCredentials), + ), + ) + .returning(); + if (changed) await announce(executor, changed, "scheduling.oauth_app.restored", null, now); + }; + if (tx) await restore(tx); + else await db.transaction(restore); +} diff --git a/packages/scheduling/src/calendar/oauth-apps.ts b/packages/scheduling/src/calendar/oauth-apps.ts new file mode 100644 index 000000000..138551a35 --- /dev/null +++ b/packages/scheduling/src/calendar/oauth-apps.ts @@ -0,0 +1,537 @@ +/** + * A customer's own OAuth apps, so calendar consent screens show their brand. An environment has at + * most one per provider, and new consents use it. A connection keeps the app that issued its tokens, + * so connections made before an app existed stay on coss.com's until their host consents again. + * + * The secret is tried before it's stored, on a real connection if there is one, since a bad paste + * would otherwise take every host's calendar out at their next refresh. + */ +import type { PlanGate } from "@coss/billing/gate"; +import { db, schema } from "@coss/db"; +import { ID_PREFIXES, isId } from "@coss/db/id"; +import { withoutTrailingSlashes } from "@coss/db/url"; +import { enqueueNotification } from "@coss/notifications"; +import { decryptSecret } from "@coss/webhooks"; +import { and, asc, eq, gt, inArray, isNull, lte, sql } from "drizzle-orm"; +import { SchedulingError } from "../errors.ts"; +import type { + CreateOauthAppRequestOutput, + OauthAppObjectOutput, + UpdateOauthAppRequestOutput, +} from "../schemas.ts"; +import type { SchedulingEnvironment } from "../types.ts"; +import { CalendarError } from "./client.ts"; +import { + type CalendarDeps, + fetcherOf, + nowMs, + oauthRedirectUri, + saveRefreshedTokens, +} from "./connections.ts"; +import { SECRET_WARNING_MS } from "./delegation-notifications.ts"; +import { + APP_REFUSED, + GOOGLE_SCOPES, + MICROSOFT_SCOPES, + type OAuthApp, + type OAuthTokens, + refreshTokens, +} from "./oauth.ts"; +import { + encryptOauthAppCredentials, + environmentOauthApp, + type OauthAppRow, + oauthAppCredentials, + restoreOauthApp, +} from "./oauth-app-state.ts"; + +export { oauthAppCredentials, zoomOauthAppForEvents } from "./oauth-app-state.ts"; + +const { schedulingCalendarConnection, schedulingOauthApp } = schema; + +type Scope = Pick; +type Scan = { limit?: number; scope?: { organizationIds: readonly string[] } }; + +export type OauthAppDeps = Pick< + CalendarDeps, + "secretsKeys" | "publicUrl" | "fetcher" | "requestTimeoutMs" | "now" +> & { + /** Only creating an app is gated: rotating or removing one always works. */ + plans?: Pick | undefined; +}; + +// What a Zoom app has to be given in its own settings: Zoom doesn't take scopes in the request. +export const ZOOM_APP_SCOPES = [ + "meeting:read:list_meetings", + "meeting:read:meeting", + "meeting:write:meeting", + "meeting:update:meeting", + "meeting:delete:meeting", + "user:read:user", +]; + +const SCOPES = { google: GOOGLE_SCOPES, microsoft: MICROSOFT_SCOPES, zoom: ZOOM_APP_SCOPES }; +const SECRET_CHECK_CONNECTIONS = 3; +// Never a real token, so all this can learn is whether the provider accepts the app. +const SECRET_CHECK_TOKEN = "coss-secret-check"; + +const invalid = (message: string, param: string) => + new SchedulingError("invalid_oauth_app", message, { param }); + +const refused = () => + new SchedulingError( + "oauth_app_refused", + "The provider refused this client ID and secret. Check them, and that the secret hasn't expired.", + ); + +const unanswered = () => + new SchedulingError("calendar_unavailable", "The provider didn't answer. Try again."); + +const isUniqueViolation = (error: unknown): boolean => { + const code = (error as { code?: string; cause?: { code?: string } }).cause?.code; + return code === "23505" || (error as { code?: string }).code === "23505"; +}; + +/** + * A live environment's addresses are https. A sandbox may use localhost, to try a forwarder from a + * laptop. Only the customer's own app can ever be sent to these, so they're no open redirect. + */ +function checkedUrl( + value: string | null | undefined, + param: string, + mode: SchedulingEnvironment["mode"], +): string | null | undefined { + if (value === null || value === undefined) return value; + let url: URL; + try { + url = new URL(value); + } catch { + throw invalid("Use a full URL.", param); + } + const local = url.hostname === "localhost" || url.hostname === "127.0.0.1"; + if (url.protocol !== "https:" && !(url.protocol === "http:" && local && mode === "sandbox")) { + throw invalid("Use an https URL.", param); + } + if (url.username || url.password || url.hash) { + throw invalid("This URL can't have credentials or a fragment.", param); + } + return value; +} + +async function connectedCounts(rows: readonly OauthAppRow[]): Promise> { + const counts = new Map(); + const first = rows[0]; + if (!first) return counts; + const found = await db + .select({ + oauthAppId: schedulingCalendarConnection.oauthAppId, + connected: sql`count(*)::int`, + }) + .from(schedulingCalendarConnection) + .where( + and( + eq(schedulingCalendarConnection.organizationId, first.organizationId), + inArray( + schedulingCalendarConnection.oauthAppId, + rows.map((row) => row.id), + ), + ), + ) + .groupBy(schedulingCalendarConnection.oauthAppId); + for (const row of found) counts.set(row.oauthAppId as string, row.connected); + return counts; +} + +function toObject( + row: OauthAppRow, + counts: Map, + deps: Pick, +): OauthAppObjectOutput { + return { + object: "scheduling.oauth_app", + id: row.id, + provider: row.provider, + client_id: row.clientId, + tenant_id: row.tenantId, + redirect_uri: row.redirectUri, + coss_callback_url: oauthRedirectUri(deps, row.provider), + completion_url: row.completionUrl, + zoom_event_url: + row.provider === "zoom" + ? `${withoutTrailingSlashes(deps.publicUrl)}/scheduling/zoom/events/${row.id}` + : null, + scopes: [...SCOPES[row.provider]], + status: row.status, + last_error_code: row.lastErrorCode, + last_error_at: row.lastErrorAt?.toISOString() ?? null, + secret_expires_at: row.secretExpiresAt?.toISOString() ?? null, + verified_at: row.verifiedAt?.toISOString() ?? null, + connections: counts.get(row.id) ?? 0, + created_at: row.createdAt.toISOString(), + updated_at: row.updatedAt.toISOString(), + }; +} + +const objectOf = async (row: OauthAppRow, deps: Pick) => + toObject(row, await connectedCounts([row]), deps); + +async function findOauthApp(scope: Scope, id: string): Promise { + const [row] = isId(ID_PREFIXES.schedulingOauthApp, id) + ? await db + .select() + .from(schedulingOauthApp) + .where( + and( + eq(schedulingOauthApp.id, id), + eq(schedulingOauthApp.organizationId, scope.organizationId), + eq(schedulingOauthApp.environmentId, scope.environmentId), + ), + ) + : []; + if (!row) throw new SchedulingError("not_found", `No OAuth app "${id}" here.`); + return row; +} + +export async function listOauthApps( + scope: Scope, + deps: Pick, +): Promise { + const rows = await db + .select() + .from(schedulingOauthApp) + .where( + and( + eq(schedulingOauthApp.organizationId, scope.organizationId), + eq(schedulingOauthApp.environmentId, scope.environmentId), + ), + ) + .orderBy(asc(schedulingOauthApp.provider)); + const counts = await connectedCounts(rows); + return rows.map((row) => toObject(row, counts, deps)); +} + +export async function getOauthApp( + scope: Scope, + id: string, + deps: Pick, +): Promise { + return objectOf(await findOauthApp(scope, id), deps); +} + +/** Whether a host's page can offer Zoom: coss.com's app, or this environment's own. */ +export async function zoomConnectable( + scope: Scope, + deps: Pick, +): Promise { + return Boolean(deps.apps.zoom) || Boolean(await environmentOauthApp(scope, "zoom")); +} + +/** Whether a host's page should speak for the customer: it has an app of its own. */ +export async function hasOauthApp(scope: Scope): Promise { + const [row] = await db + .select({ id: schedulingOauthApp.id }) + .from(schedulingOauthApp) + .where( + and( + eq(schedulingOauthApp.organizationId, scope.organizationId), + eq(schedulingOauthApp.environmentId, scope.environmentId), + ), + ) + .limit(1); + return Boolean(row); +} + +/** + * With no host's token to try, a token that can't exist: a provider that knows the app says that + * token is no good, and one that doesn't says so about the app. Not every provider checks the app + * first, so passing here doesn't prove the secret. The first consent does (`verified_at`). + */ +async function checkWithoutConnections( + provider: OauthAppRow["provider"], + app: OAuthApp, + deps: OauthAppDeps, +): Promise { + try { + await refreshTokens({ + provider, + app, + refreshToken: SECRET_CHECK_TOKEN, + fetcher: fetcherOf(deps), + nowMs: nowMs(deps), + }); + } catch (error) { + if (!(error instanceof CalendarError)) throw error; + if (error.code === APP_REFUSED) throw refused(); + if (error.kind !== "auth") throw unanswered(); + } +} + +/** On a host's real token, keeping what comes back: Zoom's work once. */ +async function checkOnConnections( + row: OauthAppRow, + app: OAuthApp, + deps: OauthAppDeps, +): Promise { + const connections = await db + .select() + .from(schedulingCalendarConnection) + .where( + and( + eq(schedulingCalendarConnection.oauthAppId, row.id), + eq(schedulingCalendarConnection.organizationId, row.organizationId), + eq(schedulingCalendarConnection.status, "active"), + isNull(schedulingCalendarConnection.delegationId), + ), + ) + .limit(SECRET_CHECK_CONNECTIONS); + for (const connection of connections) { + if (!connection.encryptedCredentials) continue; + const stored = JSON.parse( + decryptSecret(connection.encryptedCredentials, deps.secretsKeys), + ) as OAuthTokens; + if (!stored.refreshToken) continue; + try { + const tokens = await refreshTokens({ + provider: row.provider, + app, + refreshToken: stored.refreshToken, + fetcher: fetcherOf(deps), + nowMs: nowMs(deps), + }); + await saveRefreshedTokens(connection, stored.refreshToken, tokens, deps); + return true; + } catch (error) { + if (!(error instanceof CalendarError)) throw error; + if (error.code === APP_REFUSED) throw refused(); + // That host's own access is gone, or another worker just spent their token: ask about another. + if (error.kind !== "auth") throw unanswered(); + } + } + return false; +} + +export async function createOauthApp( + env: SchedulingEnvironment, + params: CreateOauthAppRequestOutput, + deps: OauthAppDeps, +): Promise { + await deps.plans?.requireFeature(env.organizationId, "schedulingOauthApps"); + const redirectUri = checkedUrl(params.redirect_uri, "redirect_uri", env.mode); + const completionUrl = checkedUrl(params.completion_url, "completion_url", env.mode); + const tenantId = params.provider === "microsoft" ? (params.tenant_id ?? null) : null; + await checkWithoutConnections( + params.provider, + { clientId: params.client_id, clientSecret: params.client_secret, tenantId }, + deps, + ); + try { + const [row] = await db + .insert(schedulingOauthApp) + .values({ + organizationId: env.organizationId, + projectId: env.projectId, + environmentId: env.environmentId, + provider: params.provider, + clientId: params.client_id, + tenantId, + redirectUri: redirectUri ?? null, + completionUrl: completionUrl ?? null, + secretExpiresAt: params.secret_expires_at ? new Date(params.secret_expires_at) : null, + encryptedCredentials: encryptOauthAppCredentials( + { + clientSecret: params.client_secret, + webhookSecret: params.provider === "zoom" ? (params.webhook_secret ?? null) : null, + }, + deps.secretsKeys, + ), + }) + .returning(); + return objectOf(row as OauthAppRow, deps); + } catch (error) { + if (isUniqueViolation(error)) { + throw invalid( + `This environment already has a ${params.provider} OAuth app. Update it, or delete it first.`, + "provider", + ); + } + throw error; + } +} + +/** Not plan-gated: a secret that can't be rotated would take hosts' calendars out when it expires. */ +export async function updateOauthApp( + env: SchedulingEnvironment, + id: string, + params: UpdateOauthAppRequestOutput, + deps: OauthAppDeps, +): Promise { + const row = await findOauthApp(env, id); + if (params.tenant_id !== undefined && row.provider !== "microsoft") { + throw invalid("Only a Microsoft app has a tenant.", "tenant_id"); + } + if (params.webhook_secret !== undefined && row.provider !== "zoom") { + throw invalid("Only a Zoom app has a Secret Token.", "webhook_secret"); + } + const redirectUri = checkedUrl(params.redirect_uri, "redirect_uri", env.mode); + const completionUrl = checkedUrl(params.completion_url, "completion_url", env.mode); + const tenantId = params.tenant_id !== undefined ? params.tenant_id : row.tenantId; + const rotating = params.client_secret !== undefined; + if (params.client_secret !== undefined) { + const app = { clientId: row.clientId, clientSecret: params.client_secret, tenantId }; + if (!(await checkOnConnections(row, app, deps))) { + await checkWithoutConnections(row.provider, app, deps); + } + } + const credentialsChanged = rotating || params.webhook_secret !== undefined; + const thisRow = and( + eq(schedulingOauthApp.id, row.id), + eq(schedulingOauthApp.organizationId, row.organizationId), + ); + await db.transaction(async (tx) => { + // Read again under the lock: trying that secret took a while, and whatever was saved meanwhile + // has to survive, like a Secret Token this request isn't changing. + const [locked] = await tx.select().from(schedulingOauthApp).where(thisRow).for("update"); + if (!locked) throw new SchedulingError("not_found", `No OAuth app "${id}" here.`); + const current = oauthAppCredentials(locked, deps.secretsKeys); + const [updated] = await tx + .update(schedulingOauthApp) + .set({ + ...(params.tenant_id !== undefined ? { tenantId } : {}), + ...(redirectUri !== undefined ? { redirectUri } : {}), + ...(completionUrl !== undefined ? { completionUrl } : {}), + // A new secret's expiry is what's asked for now: the old one's says nothing about it. + ...(params.secret_expires_at !== undefined || rotating + ? { + secretExpiresAt: params.secret_expires_at ? new Date(params.secret_expires_at) : null, + } + : {}), + ...(credentialsChanged + ? { + encryptedCredentials: encryptOauthAppCredentials( + { + clientSecret: params.client_secret ?? current.clientSecret, + webhookSecret: + params.webhook_secret !== undefined + ? params.webhook_secret + : (current.webhookSecret ?? null), + }, + deps.secretsKeys, + ), + } + : {}), + }) + .where(thisRow) + .returning(); + if (rotating && updated) await restoreOauthApp(updated, new Date(nowMs(deps)), tx); + }); + return getOauthApp(env, id, deps); +} + +/** + * Refused while connections use it: nothing else can refresh their tokens, and hosts' calendars aren't + * coss.com's to drop. Taking the row first means a consent finishing now fails on its foreign key. + */ +export async function deleteOauthApp(scope: Scope, id: string): Promise<{ id: string }> { + const row = await findOauthApp(scope, id); + await db.transaction(async (tx) => { + await tx + .select({ id: schedulingOauthApp.id }) + .from(schedulingOauthApp) + .where( + and( + eq(schedulingOauthApp.id, row.id), + eq(schedulingOauthApp.organizationId, row.organizationId), + ), + ) + .for("update"); + const [used] = await tx + .select({ connections: sql`count(*)::int` }) + .from(schedulingCalendarConnection) + .where( + and( + eq(schedulingCalendarConnection.oauthAppId, row.id), + eq(schedulingCalendarConnection.organizationId, row.organizationId), + ), + ); + const connections = used?.connections ?? 0; + if (connections > 0) { + throw new SchedulingError( + "oauth_app_in_use", + `${connections} calendar ${connections === 1 ? "connection uses" : "connections use"} this app, and only it can refresh their access. Disconnect them first.`, + ); + } + await tx + .delete(schedulingOauthApp) + .where( + and( + eq(schedulingOauthApp.id, row.id), + eq(schedulingOauthApp.organizationId, row.organizationId), + ), + ); + }); + return { id }; +} + +/** For the dashboard's banner: which of the environment's apps are refused right now. */ +export async function failingOauthApps(scope: Scope): Promise { + const rows = await db + .select({ provider: schedulingOauthApp.provider }) + .from(schedulingOauthApp) + .where( + and( + eq(schedulingOauthApp.organizationId, scope.organizationId), + eq(schedulingOauthApp.environmentId, scope.environmentId), + eq(schedulingOauthApp.status, "failing"), + ), + ); + return rows.map((row) => row.provider); +} + +/** + * Owners and admins hear before a client secret expires. Once per secret (the dedupe key), and it + * walks every app in the window: those already told stay in it for two weeks, so a batch that always + * started at the beginning would never reach the ones behind them. `limit` is the batch's size. + */ +export async function warnOfExpiringOauthAppSecrets( + deps: Pick, + opts: Scan = {}, +): Promise { + const now = deps.now?.() ?? new Date(); + const batchSize = opts.limit ?? 200; + let checked = 0; + let after: string | null = null; + for (;;) { + const rows: { id: string; organizationId: string; expires: Date | null }[] = await db + .select({ + id: schedulingOauthApp.id, + organizationId: schedulingOauthApp.organizationId, + expires: schedulingOauthApp.secretExpiresAt, + }) + .from(schedulingOauthApp) + .where( + and( + gt(schedulingOauthApp.secretExpiresAt, now), + lte(schedulingOauthApp.secretExpiresAt, new Date(now.getTime() + SECRET_WARNING_MS)), + opts.scope + ? inArray(schedulingOauthApp.organizationId, [...opts.scope.organizationIds]) + : undefined, + after ? gt(schedulingOauthApp.id, after) : undefined, + ), + ) + .orderBy(asc(schedulingOauthApp.id)) + .limit(batchSize); + for (const row of rows) { + await enqueueNotification(db, { + organizationId: row.organizationId, + kind: "scheduling.oauth_app_secret_expiring", + dedupeKey: `${row.id}:${(row.expires as Date).toISOString()}`, + payload: { oauthAppId: row.id }, + now, + }); + } + checked += rows.length; + const last = rows.at(-1); + if (!last || rows.length < batchSize) return checked; + after = last.id; + } +} diff --git a/packages/scheduling/src/calendar/oauth.ts b/packages/scheduling/src/calendar/oauth.ts index 4130318f8..f955e22cc 100644 --- a/packages/scheduling/src/calendar/oauth.ts +++ b/packages/scheduling/src/calendar/oauth.ts @@ -1,10 +1,19 @@ -/** OAuth for calendar access: coss.com's own apps, PKCE, and refresh. */ +/** OAuth for calendar access: coss.com's apps or a customer's own, PKCE, and refresh. */ import { createHash, randomBytes } from "node:crypto"; import { CalendarError, type HttpFetch } from "./client.ts"; export type OAuthProvider = "google" | "microsoft" | "zoom"; -export type OAuthApp = { clientId: string; clientSecret: string }; +export type OAuthApp = { + clientId: string; + clientSecret: string; + /** Microsoft only. Unset: `common`, which a single-tenant app can't use. */ + tenantId?: string | null | undefined; +}; + +/** The provider refused the app itself, rather than one host's access. */ +export const APP_REFUSED = "app_refused"; +const APP_ERRORS = new Set(["invalid_client", "deleted_client"]); export type OAuthTokens = { accessToken: string; @@ -30,8 +39,8 @@ const ENDPOINTS = { scopes: GOOGLE_SCOPES, }, microsoft: { - authorize: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", - token: "https://login.microsoftonline.com/common/oauth2/v2.0/token", + authorize: "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize", + token: "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token", revoke: null, scopes: MICROSOFT_SCOPES, }, @@ -44,6 +53,9 @@ const ENDPOINTS = { }, } as const; +const inTenant = (url: string, app: OAuthApp) => + url.replace("{tenant}", encodeURIComponent(app.tenantId || "common")); + export function pkcePair(): { verifier: string; challenge: string } { const verifier = randomBytes(32).toString("base64url"); return { verifier, challenge: createHash("sha256").update(verifier).digest("base64url") }; @@ -75,7 +87,7 @@ export function authorizationUrl(input: { params.set("response_mode", "query"); params.set("prompt", "select_account"); } - return `${endpoints.authorize}?${params}`; + return `${inTenant(endpoints.authorize, input.app)}?${params}`; } const basicAuth = (app: OAuthApp) => @@ -88,7 +100,7 @@ async function tokenRequest( fetcher: HttpFetch, nowMs: number, ): Promise { - const res = await fetcher(ENDPOINTS[provider].token, { + const res = await fetcher(inTenant(ENDPOINTS[provider].token, app), { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", @@ -107,6 +119,11 @@ async function tokenRequest( error?: string; }; if (!res.ok || !json.access_token) { + // A wrong or deleted secret says nothing about any host, and every one of them would get an email. + // Not `unauthorized_client`: Microsoft answers that for one tenant that doesn't allow the app. + if (json.error && APP_ERRORS.has(json.error)) { + throw new CalendarError("transient", APP_REFUSED, "The provider refused this OAuth app."); + } // A refresh token that's revoked or expired needs the host back; anything else may pass. if (json.error === "invalid_grant" || res.status === 400 || res.status === 401) { throw new CalendarError( diff --git a/packages/scheduling/src/calendar/zoom.ts b/packages/scheduling/src/calendar/zoom.ts index 1d76191ef..2dd08284e 100644 --- a/packages/scheduling/src/calendar/zoom.ts +++ b/packages/scheduling/src/calendar/zoom.ts @@ -38,9 +38,9 @@ const errorOf = (res: Response) => const minutes = (event: CalendarEventWrite) => Math.round((event.endMs - event.startMs) / 60_000); const zoomTime = (ms: number) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z"); -/** Zoom has no idempotency key, so a meeting carries this: a hash, as the host can read it. */ -const meetingMarker = (key: string) => - `coss.com appointment ${createHash("sha256").update(key).digest("hex")}`; +/** Zoom has no idempotency key, so a meeting carries a hash of one, as the host can read it. */ +const keyHash = (key: string) => createHash("sha256").update(key).digest("hex"); +const meetingMarker = (key: string) => `Appointment ${keyHash(key)}`; export function zoomCalendarClient(input: { accessToken: string; @@ -67,7 +67,8 @@ export function zoomCalendarClient(input: { }); // `upcoming` is reported to leave out the agenda; `scheduled` has it, cut to 250 characters. const meetingsWithKey = async (key: string): Promise => { - const marker = meetingMarker(key); + // The hash alone: meetings from before this said "coss.com appointment". + const marker = keyHash(key); const found: ZoomMeeting[] = []; let token = ""; do { diff --git a/packages/scheduling/src/errors.ts b/packages/scheduling/src/errors.ts index fc7406f4c..94dea09aa 100644 --- a/packages/scheduling/src/errors.ts +++ b/packages/scheduling/src/errors.ts @@ -28,6 +28,9 @@ export type SchedulingErrorCode = | "calendar_managed" | "invalid_calendar_delegation" | "calendar_delegation_refused" + | "invalid_oauth_app" + | "oauth_app_refused" + | "oauth_app_in_use" | "scheduling_unavailable" | "account_suspended" | "billing_hold" @@ -66,6 +69,9 @@ const STATUS_BY_CODE = { calendar_managed: 422, invalid_calendar_delegation: 422, calendar_delegation_refused: 422, + invalid_oauth_app: 422, + oauth_app_refused: 422, + oauth_app_in_use: 422, scheduling_unavailable: 503, account_suspended: 403, billing_hold: 402, diff --git a/packages/scheduling/src/index.ts b/packages/scheduling/src/index.ts index 7de5be923..ae2c214b7 100644 --- a/packages/scheduling/src/index.ts +++ b/packages/scheduling/src/index.ts @@ -8,6 +8,7 @@ export * from "./calendar/delegation.ts"; export { SCHEDULING_NOTIFICATION_KINDS } from "./calendar/delegation-notifications.ts"; export { buildIcs } from "./calendar/ical.ts"; export * from "./calendar/oauth.ts"; +export * from "./calendar/oauth-apps.ts"; export * from "./calendar/sync.ts"; export * from "./calendar/writes.ts"; export * from "./calendar/zoom.ts"; diff --git a/packages/scheduling/src/schemas.ts b/packages/scheduling/src/schemas.ts index 37f0f7b57..33db93ab8 100644 --- a/packages/scheduling/src/schemas.ts +++ b/packages/scheduling/src/schemas.ts @@ -50,12 +50,17 @@ export const SCHEDULING_OUT_OF_OFFICE_EVENT_TYPES = [ ] as const; export type OutOfOfficeWebhookEventType = (typeof SCHEDULING_OUT_OF_OFFICE_EVENT_TYPES)[number]; export const SCHEDULING_REASSIGNMENT_EVENT_TYPES = ["scheduling.reassignment.completed"] as const; +export const SCHEDULING_OAUTH_APP_EVENT_TYPES = [ + "scheduling.oauth_app.failing", + "scheduling.oauth_app.restored", +] as const; export const SCHEDULING_WEBHOOK_EVENT_TYPES = [ ...SCHEDULING_BOOKING_EVENT_TYPES, ...SCHEDULING_CALENDAR_EVENT_TYPES, ...SCHEDULING_DELEGATION_EVENT_TYPES, ...SCHEDULING_OUT_OF_OFFICE_EVENT_TYPES, ...SCHEDULING_REASSIGNMENT_EVENT_TYPES, + ...SCHEDULING_OAUTH_APP_EVENT_TYPES, ] as const; export type SchedulingWebhookEventType = (typeof SCHEDULING_WEBHOOK_EVENT_TYPES)[number]; @@ -1840,6 +1845,10 @@ export const SettingsObject = z logo: SettingsLogo.nullable().meta({ description: "At the top of the hosted pages, and of emails sent from `email_from`.", }), + privacy_url: z.string().nullable().meta({ + description: + "Your privacy policy, linked on a host's page beside what connecting a calendar does, once this environment has its own OAuth app and a `brand_name`.", + }), email_from: z.string().nullable().meta({ description: "Sender of booking emails, on a verified domain.", }), @@ -1895,6 +1904,7 @@ export const SettingsObject = z brand_name: "Acme Health", accent_color: null, logo: null, + privacy_url: null, email_from: "appointments@example.com", sms_from: null, name_host_in_attendee_emails: false, @@ -1935,6 +1945,15 @@ export const UpdateSettingsRequest = z .nullable() .optional() .meta({ description: "Your logo, a PNG or JPEG. `null` removes it." }), + privacy_url: z + .url({ protocol: /^https$/ }) + .max(2000) + .nullable() + .optional() + .meta({ + description: + "Your privacy policy, as an https URL, for hosts connecting a calendar through your own OAuth app. `null` removes it.", + }), email_from: z.email().max(320).nullable().optional().meta({ description: "Sender of booking emails, on a verified domain. `null`: coss.com's own address.", @@ -3004,6 +3023,35 @@ export const SchedulingCalendarWebhookEvent = z ], }); +export const SchedulingOauthAppWebhookEvent = z + .object({ + ...webhookEnvelope, + type: z.enum(SCHEDULING_OAUTH_APP_EVENT_TYPES), + data: z.object({ + oauth_app_id: z.string(), + provider: z.string(), + code: z.string().nullable().meta({ description: "What the provider said, when it refused." }), + occurred_at: z.string(), + }), + }) + .meta({ + id: "SchedulingOauthAppWebhookEvent", + examples: [ + { + id: "whev_01kd2m9a0b1c2d3e4f5g6h7j8m", + object: "event", + type: "scheduling.oauth_app.failing", + created_at: "2027-01-01T10:00:00.000Z", + data: { + oauth_app_id: "calapp_01kd2m9a0b1c2d3e4f5g6h7j8m", + provider: "google", + code: "app_refused", + occurred_at: "2027-01-01T10:00:00.000Z", + }, + }, + ], + }); + export const SchedulingCalendarDelegationWebhookEvent = z .object({ ...webhookEnvelope, @@ -3182,6 +3230,7 @@ const CONNECTION_EXAMPLE = { account: "ana@example.com", status: "active" as const, delegation: null, + oauth_app: null, calendars: [ { id: "cal_01kd2m9a0b1c2d3e4f5g6h7j8m", @@ -3209,6 +3258,10 @@ export const CalendarConnectionObject = z status: z.enum(["active", "needs_reauth"]).meta({ description: "`needs_reauth`: access was lost, and this host isn't offered until it's back.", }), + oauth_app: z.string().nullable().meta({ + description: + "Your own OAuth app this connection's access came through. `null`: coss.com's, or no consent was involved.", + }), delegation: z.string().nullable().meta({ description: "The calendar delegation that manages this connection. It can't be disconnected or reconnected by itself.", @@ -3231,6 +3284,164 @@ export const CalendarConnectionObject = z }) .meta({ id: "SchedulingCalendarConnection", examples: [CONNECTION_EXAMPLE] }); +const ClientId = z.string().min(1).max(500).regex(/^\S+$/, "A client ID has no spaces.").meta({ + description: "The app's client ID at the provider.", +}); + +const ClientSecret = z.string().min(1).max(1000).meta({ + description: "The app's client secret. Never returned.", +}); + +const OauthAppTenant = z + .string() + .regex(/^[a-zA-Z0-9](?:[a-zA-Z0-9.-]{0,253})$/, "A tenant ID or domain, like contoso.com.") + .meta({ + description: + "The Directory (tenant) ID, for an app that's only for your own tenant. Leave it out for a multi-tenant app.", + }); + +const RedirectUri = z.string().max(2000).meta({ + description: + "A page on your own domain, registered with the provider, that forwards `code` and `state` to `coss_callback_url`. Google requires this for an external app, since it has to verify the redirect's domain as yours. Leave it out to use `coss_callback_url` directly.", +}); + +const CompletionUrl = z.string().max(2000).meta({ + description: + "Where the browser lands after a consent that didn't name a `return_url`, with `coss_calendar` set to `connected`, `denied` or `invalid`. Leave it out for a plain page saying what happened.", +}); + +const OauthAppSecretExpiresAt = z.iso.datetime({ offset: true }).meta({ + description: "When the client secret expires. Owners and admins are warned 14 days before.", +}); + +const ZoomWebhookSecret = z.string().min(1).max(1000).meta({ + description: + "The Secret Token of your Zoom app, which signs its events at `zoom_event_url`. Never returned.", +}); + +export const CreateOauthAppRequest = z + .discriminatedUnion("provider", [ + z.strictObject({ + provider: z.literal("google"), + client_id: ClientId, + client_secret: ClientSecret, + redirect_uri: RedirectUri.optional(), + completion_url: CompletionUrl.optional(), + secret_expires_at: OauthAppSecretExpiresAt.optional(), + }), + z.strictObject({ + provider: z.literal("microsoft"), + client_id: ClientId, + client_secret: ClientSecret, + tenant_id: OauthAppTenant.optional(), + redirect_uri: RedirectUri.optional(), + completion_url: CompletionUrl.optional(), + secret_expires_at: OauthAppSecretExpiresAt.optional(), + }), + z.strictObject({ + provider: z.literal("zoom"), + client_id: ClientId, + client_secret: ClientSecret, + webhook_secret: ZoomWebhookSecret.optional(), + redirect_uri: RedirectUri.optional(), + completion_url: CompletionUrl.optional(), + secret_expires_at: OauthAppSecretExpiresAt.optional(), + }), + ]) + .meta({ + id: "CreateOauthAppRequest", + examples: [ + { + provider: "google", + client_id: "1234567890-abc.apps.googleusercontent.com", + client_secret: "GOCSPX-abcdefghijklmnopqrstuvwxyz", + redirect_uri: "https://example.com/oauth/google", + completion_url: "https://example.com/calendar-connected", + }, + ], + }); +export type CreateOauthAppRequestOutput = z.output; + +export const UpdateOauthAppRequest = z + .strictObject({ + client_secret: ClientSecret.optional().meta({ + description: + "A new client secret. It's tried on a connection before it replaces the old one.", + }), + tenant_id: OauthAppTenant.nullable().optional(), + webhook_secret: ZoomWebhookSecret.nullable().optional(), + redirect_uri: RedirectUri.nullable().optional(), + completion_url: CompletionUrl.nullable().optional(), + secret_expires_at: OauthAppSecretExpiresAt.nullable().optional(), + }) + .meta({ + id: "UpdateOauthAppRequest", + examples: [{ client_secret: "GOCSPX-abcdefghijklmnopqrstuvwxyz" }], + }); +export type UpdateOauthAppRequestOutput = z.output; + +const OAUTH_APP_EXAMPLE = { + object: "scheduling.oauth_app" as const, + id: "calapp_01kd2m9a0b1c2d3e4f5g6h7j8m", + provider: "google" as const, + client_id: "1234567890-abc.apps.googleusercontent.com", + tenant_id: null, + redirect_uri: "https://example.com/oauth/google", + coss_callback_url: "https://api.coss.com/scheduling/oauth/google/callback", + completion_url: "https://example.com/calendar-connected", + zoom_event_url: null, + scopes: ["https://www.googleapis.com/auth/calendar.events.freebusy"], + status: "active" as const, + last_error_code: null, + last_error_at: null, + secret_expires_at: null, + verified_at: "2027-01-01T10:00:00.000Z", + connections: 12, + created_at: "2027-01-01T10:00:00.000Z", + updated_at: "2027-01-01T10:00:00.000Z", +}; + +export const OauthAppObject = z + .object({ + object: z.literal("scheduling.oauth_app"), + id: z.string(), + provider: z.enum(["google", "microsoft", "zoom"]), + client_id: z.string(), + tenant_id: z.string().nullable(), + redirect_uri: z.string().nullable(), + coss_callback_url: z.string().meta({ + description: + "Register this with the provider if `redirect_uri` isn't set. If it is, that page forwards `code` and `state` here.", + }), + completion_url: z.string().nullable(), + zoom_event_url: z.string().nullable().meta({ + description: + "Zoom only: the Event notification endpoint URL for your app's deauthorization events.", + }), + scopes: z.array(z.string()).meta({ + description: "What to enable on your app at the provider.", + }), + status: z.enum(["active", "failing"]).meta({ + description: + "`failing`: the provider refused the app's credentials. New consents are refused, and calendars connected through it can't be read until they work again.", + }), + last_error_code: z.string().nullable(), + last_error_at: z.string().nullable(), + secret_expires_at: z.string().nullable(), + verified_at: z.string().nullable().meta({ + description: "When a consent first completed through this app, which proves its secret.", + }), + connections: z.number().int().meta({ + description: "Calendar connections using this app. It can't be deleted while there are any.", + }), + created_at: z.string(), + updated_at: z.string(), + }) + .meta({ id: "SchedulingOauthApp", examples: [OAUTH_APP_EXAMPLE] }); +export type OauthAppObjectOutput = z.output; + +export const OauthAppList = listOf(OauthAppObject, "SchedulingOauthAppList", OAUTH_APP_EXAMPLE); + const DelegationDomain = z .string() .max(253) @@ -3375,6 +3586,12 @@ export const CalendarDelegationList = listOf( DELEGATION_EXAMPLE, ); +export const DeletedOauthApp = deleted( + "scheduling.oauth_app", + "DeletedSchedulingOauthApp", + "calapp_01kd2m9a0b1c2d3e4f5g6h7j8m", +); + export const DeletedCalendarDelegation = deleted( "scheduling.calendar_delegation", "DeletedSchedulingCalendarDelegation", diff --git a/packages/scheduling/src/settings.ts b/packages/scheduling/src/settings.ts index a17844dc8..fd95a0d8e 100644 --- a/packages/scheduling/src/settings.ts +++ b/packages/scheduling/src/settings.ts @@ -29,6 +29,7 @@ function toSettingsObject( allowed_origins: row?.allowedOrigins ?? [], brand_name: row?.brandName ?? null, accent_color: row?.accentColor ?? null, + privacy_url: row?.privacyUrl ?? null, logo: logo ? { content_type: logo.contentType as "image/png" | "image/jpeg", @@ -49,6 +50,21 @@ function toSettingsObject( }; } +/** It becomes a link on a host's page, so nothing but https: the dashboard skips the wire schema. */ +function checkedPrivacyUrl(value: string | null | undefined): string | null | undefined { + if (value === null || value === undefined) return value; + let protocol: string | null = null; + try { + protocol = new URL(value).protocol; + } catch {} + if (protocol !== "https:") { + throw new SchedulingError("invalid_event_type", "Use an https URL for your privacy policy.", { + param: "privacy_url", + }); + } + return value; +} + /** `https://host[:port]`, or also localhost in a sandbox. */ export function checkedOrigin(value: string, mode: "live" | "sandbox"): string { const refused = new SchedulingError( @@ -92,6 +108,7 @@ export async function updateSettings( opts: ReminderChecks = {}, ): Promise { refuseLinks(params.brand_name, "brand_name"); + checkedPrivacyUrl(params.privacy_url); if (params.brand_name) await screenSetup(env, params.brand_name, opts.screen, "brand_name"); for (const custom of params.out_of_office_reasons ?? []) { refuseLinks(custom.label, "out_of_office_reasons"); @@ -139,6 +156,7 @@ export async function updateSettings( : {}), ...(params.brand_name !== undefined ? { brandName: params.brand_name } : {}), ...(params.accent_color !== undefined ? { accentColor: params.accent_color } : {}), + ...(params.privacy_url !== undefined ? { privacyUrl: params.privacy_url } : {}), ...(params.email_from !== undefined ? { emailFrom: params.email_from } : {}), ...(params.email_reply_to !== undefined ? { emailReplyTo: params.email_reply_to } : {}), ...(reminders !== undefined ? { reminders } : {}), diff --git a/packages/scheduling/src/testing.ts b/packages/scheduling/src/testing.ts index 84c1fd11c..5f2c08073 100644 --- a/packages/scheduling/src/testing.ts +++ b/packages/scheduling/src/testing.ts @@ -85,6 +85,16 @@ export class FakeGoogleCalendarApi { /** The next event's conference is still being created, so its answer has no link yet. */ conferencePendingNext = false; refreshFails = false; + /** A client secret Google says no to, as when it's been deleted in the Cloud console. */ + refusedSecret: string | null = null; + /** Every OAuth exchange and refresh, with the app it was made with. */ + readonly tokenRequests: { + grantType: string | null; + clientId: string | null; + clientSecret: string | null; + redirectUri: string | null; + refreshToken: string | null; + }[] = []; emptyFreeBusy = false; /** Set to accept a service account's assertions, checked against this key. */ serviceAccountPublicKey: string | null = null; @@ -182,6 +192,16 @@ export class FakeGoogleCalendarApi { this.minted.push(sub); return json(200, { access_token: `dwd:${sub}`, expires_in: 3600 }); } + this.tokenRequests.push({ + grantType: form.get("grant_type"), + clientId: form.get("client_id"), + clientSecret: form.get("client_secret"), + redirectUri: form.get("redirect_uri"), + refreshToken: form.get("refresh_token"), + }); + if (form.get("client_secret") === this.refusedSecret) { + return json(401, { error: "invalid_client" }); + } if (this.refreshFails) return json(400, { error: "invalid_grant" }); return json(200, { access_token: "access", refresh_token: "refresh", expires_in: 3600 }); } @@ -423,6 +443,8 @@ export class FakeZoomApi { readonly bodies: Record[] = []; readonly requests: string[] = []; readonly revoked: string[] = []; + /** The Basic credentials of every token request: which app asked. */ + readonly tokenAuthorizations: string[] = []; userId = "zoom_user_1"; loseNextAnswer = false; failNext: { path: RegExp; status: number } | null = null; @@ -451,6 +473,7 @@ export class FakeZoomApi { return json(200, { status: "success" }); } const form = new URLSearchParams(String(init?.body)); + this.tokenAuthorizations.push(new Headers(init?.headers).get("authorization") ?? ""); if (form.get("grant_type") === "refresh_token") { if (form.get("refresh_token") !== this.refreshToken) { return json(400, { error: "invalid_grant" }); diff --git a/packages/scheduling/src/webhook-types.ts b/packages/scheduling/src/webhook-types.ts index c197e0584..b471afbb3 100644 --- a/packages/scheduling/src/webhook-types.ts +++ b/packages/scheduling/src/webhook-types.ts @@ -3,9 +3,12 @@ export type { SchedulingWebhookEventType } from "./schemas.ts"; import type { SCHEDULING_CALENDAR_EVENT_TYPES, SCHEDULING_DELEGATION_EVENT_TYPES, + SCHEDULING_OAUTH_APP_EVENT_TYPES, } from "./schemas.ts"; export type SchedulingCalendarWebhookEventType = (typeof SCHEDULING_CALENDAR_EVENT_TYPES)[number]; export type SchedulingDelegationWebhookEventType = (typeof SCHEDULING_DELEGATION_EVENT_TYPES)[number]; + +export type SchedulingOauthAppWebhookEventType = (typeof SCHEDULING_OAUTH_APP_EVENT_TYPES)[number]; diff --git a/packages/scheduling/test/oauth-apps.test.ts b/packages/scheduling/test/oauth-apps.test.ts new file mode 100644 index 000000000..8b53e4d0d --- /dev/null +++ b/packages/scheduling/test/oauth-apps.test.ts @@ -0,0 +1,876 @@ +import { describe, expect, test } from "bun:test"; +import { createHash, randomUUID } from "node:crypto"; +import { db, schema } from "@coss/db"; +import { createTestTenant } from "@coss/tenancy/testing"; +import { decryptSecret, reencryptWebhookSecrets, secretsKeyRing } from "@coss/webhooks"; +import { and, eq } from "drizzle-orm"; +import { markOauthAppFailing } from "../src/calendar/oauth-app-state.ts"; +import { + APP_REFUSED, + type CalendarDeps, + CalendarError, + clientFor, + completeCalendarAuthorization, + createBooking, + createEventType, + createHost, + createOauthApp, + createSchedule, + deleteOauthApp, + disconnectCalendar, + getOauthApp, + handleZoomDeauthorization, + listConnections, + listOauthApps, + runDueCalendarSyncs, + runDueCalendarWrites, + type SchedulingEnvironment, + SchedulingError, + startCalendarAuthorization, + updateOauthApp, + warnOfExpiringOauthAppSecrets, +} from "../src/index.ts"; +import { FakeGoogleCalendarApi, FakeZoomApi, testSchedulingDeps } from "../src/testing.ts"; + +const NOW = new Date("2031-01-06T00:00:00Z"); +// Past the hour an access token lasts, so `clientFor` has to refresh. +const LATER = new Date(NOW.getTime() + 2 * 3_600_000); +const keys = secretsKeyRing([createHash("sha256").update("oauth apps test").digest()]); +const scopeOf = (env: SchedulingEnvironment) => ({ organizationIds: [env.organizationId] }); + +function depsOf( + fetcher: CalendarDeps["fetcher"], + now: () => Date = () => NOW, + zoom = false, +): CalendarDeps { + return { + secretsKeys: keys, + apps: { + google: { clientId: "coss-google", clientSecret: "coss-secret" }, + microsoft: { clientId: "coss-ms", clientSecret: "coss-ms-secret" }, + ...(zoom + ? { zoom: { clientId: "coss-zoom", clientSecret: "coss-zoom-secret", webhookSecret: "t" } } + : {}), + }, + publicUrl: "https://api.example.test", + fetcher: fetcher as NonNullable, + now, + }; +} + +async function setUp() { + const tenant = await createTestTenant(); + const env: SchedulingEnvironment = { + organizationId: tenant.organization.id, + projectId: tenant.project.id, + environmentId: tenant.production.id, + mode: "live", + }; + const host = await createHost(env, { name: "Dr. Reyes", time_zone: "UTC" }); + return { tenant, env, host }; +} + +const googleApp = { + provider: "google" as const, + client_id: "acme-google", + client_secret: "acme-secret", +}; + +async function consent( + env: SchedulingEnvironment, + hostId: string, + deps: CalendarDeps, + provider: "google" | "zoom" = "google", +) { + const { url } = await startCalendarAuthorization( + env, + hostId, + { provider, returnUrl: null }, + deps, + ); + const parsed = new URL(url); + const state = parsed.searchParams.get("state") as string; + const result = await completeCalendarAuthorization( + { provider, state, code: "code", error: null }, + deps, + ); + return { parsed, result }; +} + +const connectionsOf = (hostId: string) => + db + .select() + .from(schema.schedulingCalendarConnection) + .where(eq(schema.schedulingCalendarConnection.hostId, hostId)); + +const errorCode = async (run: Promise) => { + try { + await run; + return "ok"; + } catch (error) { + if (error instanceof SchedulingError) return error.code; + throw error; + } +}; + +describe("which app a consent uses", () => { + test("an environment's own app replaces coss.com's on the consent screen and the exchange", async () => { + const { env, host } = await setUp(); + const api = new FakeGoogleCalendarApi(); + const deps = depsOf(api.fetch); + const app = await createOauthApp(env, googleApp, deps); + const { parsed, result } = await consent(env, host.id, deps); + expect(result.outcome).toBe("connected"); + expect(parsed.searchParams.get("client_id")).toBe("acme-google"); + expect(api.tokenRequests.at(-1)).toMatchObject({ + grantType: "authorization_code", + clientId: "acme-google", + clientSecret: "acme-secret", + }); + const [connection] = await connectionsOf(host.id); + expect(connection?.oauthAppId).toBe(app.id); + expect((await listConnections(env, host.id))[0]?.oauth_app).toBe(app.id); + expect((await getOauthApp(env, app.id, deps)).verified_at).not.toBeNull(); + expect((await getOauthApp(env, app.id, deps)).connections).toBe(1); + }); + + test("a forwarder on the customer's domain is the redirect URI, at both ends", async () => { + const { env, host } = await setUp(); + const api = new FakeGoogleCalendarApi(); + const deps = depsOf(api.fetch); + await createOauthApp( + env, + { + ...googleApp, + redirect_uri: "https://acme.example/oauth/google", + completion_url: "https://acme.example/connected", + }, + deps, + ); + const { parsed, result } = await consent(env, host.id, deps); + expect(parsed.searchParams.get("redirect_uri")).toBe("https://acme.example/oauth/google"); + expect(api.tokenRequests.at(-1)?.redirectUri).toBe("https://acme.example/oauth/google"); + expect(result.returnUrl).toBe("https://acme.example/connected"); + }); + + test("the consent's own return_url wins over the app's completion URL", async () => { + const { env, host } = await setUp(); + const api = new FakeGoogleCalendarApi(); + const deps = depsOf(api.fetch); + await createOauthApp( + env, + { ...googleApp, completion_url: "https://acme.example/connected" }, + deps, + ); + const { url } = await startCalendarAuthorization( + env, + host.id, + { provider: "google", returnUrl: "https://acme.example/this-one" }, + deps, + ); + const state = new URL(url).searchParams.get("state") as string; + const result = await completeCalendarAuthorization( + { provider: "google", state, code: "code", error: null }, + deps, + ); + expect(result.returnUrl).toBe("https://acme.example/this-one"); + }); + + test("editing the app during a consent can't change what its code is exchanged with", async () => { + const { env, host } = await setUp(); + const api = new FakeGoogleCalendarApi(); + const deps = depsOf(api.fetch); + const app = await createOauthApp( + env, + { ...googleApp, redirect_uri: "https://acme.example/before" }, + deps, + ); + const { url } = await startCalendarAuthorization( + env, + host.id, + { provider: "google", returnUrl: null }, + deps, + ); + await updateOauthApp(env, app.id, { redirect_uri: "https://acme.example/after" }, deps); + const state = new URL(url).searchParams.get("state") as string; + await completeCalendarAuthorization( + { provider: "google", state, code: "code", error: null }, + deps, + ); + expect(api.tokenRequests.at(-1)?.redirectUri).toBe("https://acme.example/before"); + }); + + test("a Microsoft consent is exchanged at the tenant it started with", async () => { + const { env, host } = await setUp(); + const urls: string[] = []; + const deps = depsOf(async (url) => { + urls.push(url); + return new Response(JSON.stringify({ error: "invalid_grant" }), { status: 400 }); + }); + const app = await createOauthApp( + env, + { provider: "microsoft", client_id: "c", client_secret: "s", tenant_id: "before.example" }, + deps, + ); + const { url } = await startCalendarAuthorization( + env, + host.id, + { provider: "microsoft", returnUrl: null }, + deps, + ); + await updateOauthApp(env, app.id, { tenant_id: "after.example" }, deps); + urls.length = 0; + await completeCalendarAuthorization( + { + provider: "microsoft", + state: new URL(url).searchParams.get("state") as string, + code: "code", + error: null, + }, + deps, + ).catch(() => {}); + expect(new URL(urls[0] as string).pathname).toBe("/before.example/oauth2/v2.0/token"); + }); + + test("an environment with no coss.com app can still connect through its own", async () => { + const { env, host } = await setUp(); + const api = new FakeGoogleCalendarApi(); + const deps: CalendarDeps = { + ...depsOf(api.fetch), + apps: { google: null, microsoft: null }, + }; + expect( + await errorCode( + startCalendarAuthorization(env, host.id, { provider: "google", returnUrl: null }, deps), + ), + ).toBe("calendar_unavailable"); + await createOauthApp(env, googleApp, deps); + expect((await consent(env, host.id, deps)).result.outcome).toBe("connected"); + }); + + test("a single-tenant Microsoft app uses its tenant, never common", async () => { + const { env, host } = await setUp(); + const urls: string[] = []; + const fetcher = async (url: string) => { + urls.push(url); + return new Response(JSON.stringify({ error: "invalid_grant" }), { status: 400 }); + }; + const deps = depsOf(fetcher); + await createOauthApp( + env, + { + provider: "microsoft", + client_id: "acme-ms", + client_secret: "s", + tenant_id: "contoso.example", + }, + deps, + ); + const { url } = await startCalendarAuthorization( + env, + host.id, + { provider: "microsoft", returnUrl: null }, + deps, + ); + expect(new URL(url).pathname).toBe("/contoso.example/oauth2/v2.0/authorize"); + expect(new URL(urls[0] as string).pathname).toBe("/contoso.example/oauth2/v2.0/token"); + }); +}); + +describe("which app a connection refreshes with", () => { + test("a connection from before the app existed stays on coss.com's, until the host consents again", async () => { + const { env, host } = await setUp(); + const api = new FakeGoogleCalendarApi(); + await consent(env, host.id, depsOf(api.fetch)); + const before = (await connectionsOf(host.id))[0]; + expect(before?.oauthAppId).toBeNull(); + + const app = await createOauthApp(env, googleApp, depsOf(api.fetch)); + await clientFor( + before as NonNullable, + depsOf(api.fetch, () => LATER), + ); + expect(api.tokenRequests.at(-1)).toMatchObject({ + grantType: "refresh_token", + clientId: "coss-google", + }); + + await consent(env, host.id, depsOf(api.fetch)); + const after = (await connectionsOf(host.id))[0]; + expect(after?.id).toBe(before?.id); + expect(after?.oauthAppId).toBe(app.id); + await clientFor( + after as NonNullable, + depsOf(api.fetch, () => LATER), + ); + expect(api.tokenRequests.at(-1)).toMatchObject({ + grantType: "refresh_token", + clientId: "acme-google", + }); + }); + + test("two environments' apps never use each other's credentials", async () => { + const { tenant, env, host } = await setUp(); + const sandbox: SchedulingEnvironment = { + ...env, + environmentId: tenant.development.id, + mode: "sandbox", + }; + const sandboxHost = await createHost(sandbox, { name: "Sam", time_zone: "UTC" }); + const api = new FakeGoogleCalendarApi(); + const deps = depsOf(api.fetch); + await createOauthApp(env, googleApp, deps); + await createOauthApp( + sandbox, + { ...googleApp, client_id: "acme-dev", client_secret: "dev-secret" }, + deps, + ); + await consent(env, host.id, deps); + expect(api.tokenRequests.at(-1)?.clientId).toBe("acme-google"); + await consent(sandbox, sandboxHost.id, deps); + expect(api.tokenRequests.at(-1)?.clientId).toBe("acme-dev"); + const [live] = await connectionsOf(host.id); + await clientFor( + live as NonNullable, + depsOf(api.fetch, () => LATER), + ); + expect(api.tokenRequests.at(-1)).toMatchObject({ + clientId: "acme-google", + clientSecret: "acme-secret", + }); + }); +}); + +describe("when the provider refuses the app", () => { + async function connected() { + const fixture = await setUp(); + const api = new FakeGoogleCalendarApi(); + const deps = depsOf(api.fetch); + const app = await createOauthApp(fixture.env, googleApp, deps); + await consent(fixture.env, fixture.host.id, deps); + const [connection] = await connectionsOf(fixture.host.id); + return { ...fixture, api, deps, app, connection: connection as NonNullable }; + } + + const notificationsOf = (organizationId: string, kind: string) => + db + .select() + .from(schema.notification) + .where( + and( + eq(schema.notification.organizationId, organizationId), + eq(schema.notification.kind, kind), + ), + ); + + test("it's one outage of the app: no host is marked or emailed", async () => { + const { env, host, api, app, connection } = await connected(); + api.refusedSecret = "acme-secret"; + const later = depsOf(api.fetch, () => LATER); + for (let i = 0; i < 2; i++) { + const refused = await clientFor(connection, later).catch((error: unknown) => error); + expect(refused).toBeInstanceOf(CalendarError); + expect((refused as CalendarError).code).toBe(APP_REFUSED); + expect((refused as CalendarError).kind).toBe("transient"); + } + expect((await getOauthApp(env, app.id, later)).status).toBe("failing"); + expect((await getOauthApp(env, app.id, later)).last_error_code).toBe(APP_REFUSED); + expect((await connectionsOf(host.id))[0]?.status).toBe("active"); + expect(await notificationsOf(env.organizationId, "scheduling.oauth_app_failing")).toHaveLength( + 1, + ); + const hostEmails = await db + .select() + .from(schema.schedulingNotification) + .where(eq(schema.schedulingNotification.hostId, host.id)); + expect(hostEmails).toHaveLength(0); + }); + + test("a sync through a refused app leaves the host's connection alone", async () => { + const { env, host, api, app } = await connected(); + api.refusedSecret = "acme-secret"; + await runDueCalendarSyncs( + depsOf(api.fetch, () => LATER), + { scope: scopeOf(env) }, + ); + expect((await connectionsOf(host.id))[0]?.status).toBe("active"); + expect((await getOauthApp(env, app.id, depsOf(api.fetch))).status).toBe("failing"); + }); + + test("while it's failing, consents are refused rather than falling back to coss.com's", async () => { + const { env, host, api, connection } = await connected(); + api.refusedSecret = "acme-secret"; + const later = depsOf(api.fetch, () => LATER); + await clientFor(connection, later).catch(() => {}); + expect( + await errorCode( + startCalendarAuthorization(env, host.id, { provider: "google", returnUrl: null }, later), + ), + ).toBe("calendar_unavailable"); + }); + + test("a good new secret ends the outage, and a bad one keeps the old", async () => { + const { env, api, app, connection } = await connected(); + api.refusedSecret = "acme-secret"; + const later = depsOf(api.fetch, () => LATER); + await clientFor(connection, later).catch(() => {}); + + expect( + await errorCode(updateOauthApp(env, app.id, { client_secret: "acme-secret" }, later)), + ).toBe("oauth_app_refused"); + expect((await getOauthApp(env, app.id, later)).status).toBe("failing"); + + const updated = await updateOauthApp(env, app.id, { client_secret: "new-secret" }, later); + expect(updated.status).toBe("active"); + expect(updated.last_error_code).toBeNull(); + // It was tried on a real connection's token before being stored. + expect(api.tokenRequests.at(-1)).toMatchObject({ + grantType: "refresh_token", + clientSecret: "new-secret", + }); + const [fresh] = await connectionsOf(connection.hostId); + await clientFor( + fresh as NonNullable, + depsOf(api.fetch, () => new Date(LATER.getTime() + 7_200_000)), + ); + expect(api.tokenRequests.at(-1)?.clientSecret).toBe("new-secret"); + }); + + test("a refusal of a secret that's since been replaced doesn't mark the new one", async () => { + const { env, api, app, connection } = await connected(); + const [row] = await db + .select() + .from(schema.schedulingOauthApp) + .where(eq(schema.schedulingOauthApp.id, app.id)); + const later = depsOf(api.fetch, () => LATER); + await updateOauthApp(env, app.id, { client_secret: "new-secret" }, later); + await markOauthAppFailing(row as NonNullable, APP_REFUSED, LATER); + expect((await getOauthApp(env, app.id, later)).status).toBe("active"); + expect(connection.status).toBe("active"); + }); + + async function bookable() { + const fixture = await connected(); + await createSchedule(fixture.env, fixture.host.id, { + rules: [{ days: [0, 1, 2, 3, 4, 5, 6], start: "09:00", end: "17:00" }], + }); + const eventType = await createEventType(fixture.env, { + title: "Consultation", + kind: "one_on_one", + duration_minutes: 30, + hosts: [{ host: fixture.host.id, role: "member", weight: 100, schedule: null }], + } as never); + await runDueCalendarSyncs(fixture.deps, { scope: scopeOf(fixture.env) }); + return { ...fixture, eventType }; + } + + const bookAt = Date.parse("2031-01-06T09:00:00Z"); + const attendee = { uid: "p", name: "Sam Lee", email: "sam@example.com", time_zone: "UTC" }; + const actor = { kind: "system" } as const; + + test("a booking still goes through on what's fresh, and a stale calendar hides the host", async () => { + const { env, host, api, app, eventType } = await bookable(); + // Read just before the token comes due, so the booking's live check is what has to refresh it. + const justRead = new Date(NOW.getTime() + 57 * 60_000); + await runDueCalendarSyncs( + depsOf(api.fetch, () => justRead), + { scope: scopeOf(env) }, + ); + api.refusedSecret = "acme-secret"; + const soon = new Date(NOW.getTime() + 59 * 60_000); + const { booking } = await createBooking( + env, + { eventType: eventType.id, startMs: bookAt, attendee }, + testSchedulingDeps({ calendars: depsOf(api.fetch, () => soon), now: () => soon }), + actor, + ); + expect(booking.status).toBe("confirmed"); + expect((await getOauthApp(env, app.id, depsOf(api.fetch))).status).toBe("failing"); + + // Hours on, nothing's been read: an unreadable calendar never reads as free. + const stale = new Date(NOW.getTime() + 6 * 3_600_000); + expect( + await errorCode( + createBooking( + env, + { + eventType: eventType.id, + startMs: bookAt + 3_600_000, + attendee: { ...attendee, uid: "q" }, + }, + testSchedulingDeps({ calendars: depsOf(api.fetch, () => stale), now: () => stale }), + actor, + ), + ), + ).toBe("slot_unavailable"); + expect((await connectionsOf(host.id))[0]?.status).toBe("active"); + }); + + test("a calendar write waits out the outage, and lands once the secret's fixed", async () => { + const { env, host, api, app, eventType } = await bookable(); + const { booking } = await createBooking( + env, + { eventType: eventType.id, startMs: bookAt, attendee }, + testSchedulingDeps({ now: () => NOW }), + actor, + ); + api.refusedSecret = "acme-secret"; + const failingDeps = { + ...depsOf(api.fetch, () => LATER), + dataKeys: testSchedulingDeps().dataKeys, + }; + await runDueCalendarWrites(failingDeps, { scope: scopeOf(env) }); + const eventsOf = () => + db + .select() + .from(schema.schedulingCalendarEvent) + .where(eq(schema.schedulingCalendarEvent.bookingId, booking.id)); + const [waiting] = await eventsOf(); + expect(waiting?.lastErrorCode).toBe(APP_REFUSED); + expect(waiting?.providerEventId).toBeNull(); + expect(waiting?.appliedVersion).not.toBe(waiting?.desiredVersion); + expect((await connectionsOf(host.id))[0]?.status).toBe("active"); + + await updateOauthApp(env, app.id, { client_secret: "new-secret" }, failingDeps); + const muchLater = new Date(LATER.getTime() + 3_600_000); + await runDueCalendarWrites({ ...failingDeps, now: () => muchLater }, { scope: scopeOf(env) }); + const [written] = await eventsOf(); + expect(written?.providerEventId).not.toBeNull(); + expect(written?.lastErrorCode).toBeNull(); + }); + + test("a refresh that works ends an outage that began while it was on its way", async () => { + const { env, api, app, connection } = await connected(); + const later = depsOf(api.fetch, () => LATER); + const [row] = await db + .select() + .from(schema.schedulingOauthApp) + .where(eq(schema.schedulingOauthApp.id, app.id)); + const held = api.hold("POST", /\/token$/); + const refreshing = clientFor(connection, later); + await held.arrived; + // Read as active by the refresh above, then refused for someone else before it gets its answer. + await markOauthAppFailing(row as NonNullable, APP_REFUSED, LATER); + held.release(); + await refreshing; + expect((await getOauthApp(env, app.id, later)).status).toBe("active"); + }); + + test("refusing one host's token doesn't touch the app", async () => { + const { env, host, api, app, connection } = await connected(); + api.refreshFails = true; + const later = depsOf(api.fetch, () => LATER); + const refused = (await clientFor(connection, later).catch((e: unknown) => e)) as CalendarError; + expect(refused.kind).toBe("auth"); + expect((await getOauthApp(env, app.id, later)).status).toBe("active"); + expect((await connectionsOf(host.id))[0]?.oauthAppId).toBe(app.id); + }); + + test("coss.com's own app being refused doesn't mark any host either", async () => { + const { env, host } = await setUp(); + const api = new FakeGoogleCalendarApi(); + await consent(env, host.id, depsOf(api.fetch)); + const [connection] = await connectionsOf(host.id); + api.refusedSecret = "coss-secret"; + const original = console.error; + console.error = () => {}; + try { + await runDueCalendarSyncs( + depsOf(api.fetch, () => LATER), + { scope: scopeOf(env) }, + ); + await clientFor( + connection as NonNullable, + depsOf(api.fetch, () => LATER), + ).catch(() => {}); + } finally { + console.error = original; + } + expect((await connectionsOf(host.id))[0]?.status).toBe("active"); + }); +}); + +describe("managing an app", () => { + test("a wrong secret is refused when it's added", async () => { + const { env } = await setUp(); + const api = new FakeGoogleCalendarApi(); + api.refusedSecret = "wrong"; + const deps = depsOf(api.fetch); + expect( + await errorCode(createOauthApp(env, { ...googleApp, client_secret: "wrong" }, deps)), + ).toBe("oauth_app_refused"); + expect(await listOauthApps(env, deps)).toHaveLength(0); + }); + + test("one per provider per environment", async () => { + const { env } = await setUp(); + const deps = depsOf(new FakeGoogleCalendarApi().fetch); + await createOauthApp(env, googleApp, deps); + expect(await errorCode(createOauthApp(env, googleApp, deps))).toBe("invalid_oauth_app"); + }); + + test("a secret never appears on the wire, and is stored encrypted", async () => { + const { env } = await setUp(); + const deps = depsOf(new FakeGoogleCalendarApi().fetch); + const app = await createOauthApp(env, googleApp, deps); + expect(JSON.stringify(app)).not.toContain("acme-secret"); + const [row] = await db + .select() + .from(schema.schedulingOauthApp) + .where(eq(schema.schedulingOauthApp.id, app.id)); + expect(row?.encryptedCredentials).not.toContain("acme-secret"); + expect(JSON.parse(decryptSecret(row?.encryptedCredentials as string, keys))).toMatchObject({ + clientSecret: "acme-secret", + }); + }); + + test("another organization or environment can't see or change an app", async () => { + const { env } = await setUp(); + const other = await setUp(); + const deps = depsOf(new FakeGoogleCalendarApi().fetch); + const app = await createOauthApp(env, googleApp, deps); + expect(await errorCode(getOauthApp(other.env, app.id, deps))).toBe("not_found"); + expect(await errorCode(updateOauthApp(other.env, app.id, { client_secret: "x" }, deps))).toBe( + "not_found", + ); + expect(await errorCode(deleteOauthApp(other.env, app.id))).toBe("not_found"); + }); + + test("addresses have to be https, except localhost in a sandbox", async () => { + const { tenant, env } = await setUp(); + const deps = depsOf(new FakeGoogleCalendarApi().fetch); + for (const redirect_uri of [ + "http://acme.example/oauth", + "http://localhost:3000/oauth", + "https://user:pass@acme.example/oauth", + "https://acme.example/oauth#x", + "javascript:alert(1)", + "not a url", + ]) { + expect(await errorCode(createOauthApp(env, { ...googleApp, redirect_uri }, deps))).toBe( + "invalid_oauth_app", + ); + } + const sandbox: SchedulingEnvironment = { + ...env, + environmentId: tenant.development.id, + mode: "sandbox", + }; + expect( + await errorCode( + createOauthApp( + sandbox, + { ...googleApp, redirect_uri: "http://localhost:3000/oauth" }, + deps, + ), + ), + ).toBe("ok"); + }); + + test("creating needs the plan, and rotating or deleting never does", async () => { + const { env } = await setUp(); + const deps = depsOf(new FakeGoogleCalendarApi().fetch); + const app = await createOauthApp(env, googleApp, deps); + class PlanRefused extends Error {} + const lapsed = { + ...deps, + plans: { + requireFeature: async () => { + throw new PlanRefused(); + }, + }, + }; + await expect( + createOauthApp(env, { ...googleApp, provider: "zoom" }, lapsed), + ).rejects.toBeInstanceOf(PlanRefused); + expect((await updateOauthApp(env, app.id, { client_secret: "new" }, lapsed)).id).toBe(app.id); + expect(await deleteOauthApp(env, app.id)).toEqual({ id: app.id }); + }); + + test("it can't be deleted while connections use it, and can once they've gone", async () => { + const { env, host } = await setUp(); + const api = new FakeGoogleCalendarApi(); + const deps = depsOf(api.fetch); + const app = await createOauthApp(env, googleApp, deps); + await consent(env, host.id, deps); + expect(await errorCode(deleteOauthApp(env, app.id))).toBe("oauth_app_in_use"); + const [connection] = await connectionsOf(host.id); + await disconnectCalendar(env, host.id, (connection as NonNullable).id, deps); + expect(await deleteOauthApp(env, app.id)).toEqual({ id: app.id }); + }); + + test("deleting an environment takes its apps and connections together", async () => { + const { tenant, env, host } = await setUp(); + const api = new FakeGoogleCalendarApi(); + const deps = depsOf(api.fetch); + const app = await createOauthApp(env, googleApp, deps); + await consent(env, host.id, deps); + await db.delete(schema.environment).where(eq(schema.environment.id, tenant.production.id)); + expect( + await db + .select() + .from(schema.schedulingOauthApp) + .where(eq(schema.schedulingOauthApp.id, app.id)), + ).toHaveLength(0); + }); + + test("a secret about to expire warns once, and a rotation forgets the old date", async () => { + const { env } = await setUp(); + const deps = depsOf(new FakeGoogleCalendarApi().fetch); + const soon = new Date(NOW.getTime() + 3 * 86_400_000).toISOString(); + const app = await createOauthApp(env, { ...googleApp, secret_expires_at: soon }, deps); + await warnOfExpiringOauthAppSecrets(deps, { scope: scopeOf(env) }); + await warnOfExpiringOauthAppSecrets(deps, { scope: scopeOf(env) }); + const sent = await db + .select() + .from(schema.notification) + .where( + and( + eq(schema.notification.organizationId, env.organizationId), + eq(schema.notification.kind, "scheduling.oauth_app_secret_expiring"), + ), + ); + expect(sent).toHaveLength(1); + const rotated = await updateOauthApp(env, app.id, { client_secret: "new" }, deps); + expect(rotated.secret_expires_at).toBeNull(); + }); + + test("every app about to expire hears, however many there are", async () => { + const { env } = await setUp(); + const other = await setUp(); + const deps = depsOf(new FakeGoogleCalendarApi().fetch); + const soon = new Date(NOW.getTime() + 3 * 86_400_000).toISOString(); + await createOauthApp(env, { ...googleApp, secret_expires_at: soon }, deps); + await createOauthApp(other.env, { ...googleApp, secret_expires_at: soon }, deps); + const scope = { organizationIds: [env.organizationId, other.env.organizationId] }; + // A batch of one: the second app is only reached by going past the first. + expect(await warnOfExpiringOauthAppSecrets(deps, { scope, limit: 1 })).toBe(2); + for (const { organizationId } of [env, other.env]) { + const sent = await db + .select() + .from(schema.notification) + .where( + and( + eq(schema.notification.organizationId, organizationId), + eq(schema.notification.kind, "scheduling.oauth_app_secret_expiring"), + ), + ); + expect(sent).toHaveLength(1); + } + }); + + test("a new secret doesn't lose a Secret Token saved while it was being tried", async () => { + const { env } = await setUp(); + const api = new FakeZoomApi(); + let arrive = () => {}; + let release = () => {}; + const arrived = new Promise((resolve) => { + arrive = resolve; + }); + const gate = new Promise((resolve) => { + release = resolve; + }); + let holding = false; + const deps = depsOf(async (url, init) => { + if (holding && String(url).includes("/oauth/token")) { + holding = false; + arrive(); + await gate; + } + return api.fetch(url, init); + }); + const app = await createOauthApp( + env, + { provider: "zoom", client_id: "c", client_secret: "old", webhook_secret: "old-token" }, + deps, + ); + holding = true; + const rotating = updateOauthApp(env, app.id, { client_secret: "new" }, deps); + await arrived; + await updateOauthApp(env, app.id, { webhook_secret: "new-token" }, deps); + release(); + await rotating; + const [row] = await db + .select() + .from(schema.schedulingOauthApp) + .where(eq(schema.schedulingOauthApp.id, app.id)); + expect(JSON.parse(decryptSecret(row?.encryptedCredentials as string, keys))).toEqual({ + clientSecret: "new", + webhookSecret: "new-token", + }); + }); + + test("re-encryption moves an app's credentials to the new key", async () => { + const { env } = await setUp(); + const deps = depsOf(new FakeGoogleCalendarApi().fetch); + const app = await createOauthApp(env, googleApp, deps); + const rotated = secretsKeyRing([ + createHash("sha256").update("the next key").digest(), + createHash("sha256").update("oauth apps test").digest(), + ]); + await reencryptWebhookSecrets({ + keys: rotated, + scope: scopeOf(env) as { organizationIds: string[] }, + }); + const [row] = await db + .select() + .from(schema.schedulingOauthApp) + .where(eq(schema.schedulingOauthApp.id, app.id)); + const onlyNew = secretsKeyRing([createHash("sha256").update("the next key").digest()]); + expect(JSON.parse(decryptSecret(row?.encryptedCredentials as string, onlyNew))).toMatchObject({ + clientSecret: "acme-secret", + }); + }); +}); + +describe("Zoom's deauthorization events", () => { + const zoomApp = { provider: "zoom" as const, client_id: "acme-zoom", client_secret: "acme-zs" }; + + test("a customer's event removes only that app's connections, in no other organization", async () => { + const mine = await setUp(); + const theirs = await setUp(); + const onCoss = await setUp(); + const api = new FakeZoomApi(); + // The test database is shared, and coss.com's own events reach any organization's connections. + api.userId = `zoom_${randomUUID()}`; + const deps = depsOf(api.fetch, () => NOW, true); + const app = await createOauthApp(mine.env, zoomApp, deps); + const other = await createOauthApp(theirs.env, zoomApp, deps); + // The same Zoom user, reached through three different apps. + await consent(mine.env, mine.host.id, deps, "zoom"); + await consent(theirs.env, theirs.host.id, deps, "zoom"); + await consent(onCoss.env, onCoss.host.id, deps, "zoom"); + expect((await connectionsOf(onCoss.host.id))[0]?.oauthAppId).toBeNull(); + + await handleZoomDeauthorization(api.userId, deps, app.id); + expect(await connectionsOf(mine.host.id)).toHaveLength(0); + expect(await connectionsOf(theirs.host.id)).toHaveLength(1); + expect(await connectionsOf(onCoss.host.id)).toHaveLength(1); + + // Coss.com's app reaches its own connections, and nobody on a customer's. + await handleZoomDeauthorization(api.userId, deps); + expect(await connectionsOf(onCoss.host.id)).toHaveLength(0); + expect(await connectionsOf(theirs.host.id)).toHaveLength(1); + expect((await connectionsOf(theirs.host.id))[0]?.oauthAppId).toBe(other.id); + }); + + test("revoking on disconnect uses the app that issued the token, and the token as it is now", async () => { + const { env, host } = await setUp(); + const api = new FakeZoomApi(); + const deps = depsOf(api.fetch, () => NOW, true); + await createOauthApp(env, zoomApp, deps); + await consent(env, host.id, deps, "zoom"); + const [connection] = await connectionsOf(host.id); + const later = depsOf(api.fetch, () => LATER, true); + await disconnectCalendar( + env, + host.id, + (connection as NonNullable).id, + later, + ); + const basic = `Basic ${Buffer.from("acme-zoom:acme-zs").toString("base64")}`; + expect(api.tokenAuthorizations.at(-1)).toBe(basic); + // The refresh during the disconnect spent the old token, so that's not the one revoked. + expect(api.revoked).toEqual([api.refreshToken]); + }); +}); diff --git a/packages/scheduling/test/reencrypt.test.ts b/packages/scheduling/test/reencrypt.test.ts index c4640f1cf..8c561e4f9 100644 --- a/packages/scheduling/test/reencrypt.test.ts +++ b/packages/scheduling/test/reencrypt.test.ts @@ -158,6 +158,7 @@ describe("reencryptSchedulingData", () => { "scheduling_calendar_connection.encrypted_credentials", "scheduling_holiday_calendar.encrypted_ics_url", "scheduling_calendar_delegation.encrypted_credentials", + "scheduling_oauth_app.encrypted_credentials", "scheduling_oauth_state.encrypted_code_verifier", ]; expect(rows.map((row) => row.name)).toEqual( diff --git a/packages/sdk/src/core/routes.generated.ts b/packages/sdk/src/core/routes.generated.ts index b05dc7906..1caa25f25 100644 --- a/packages/sdk/src/core/routes.generated.ts +++ b/packages/sdk/src/core/routes.generated.ts @@ -161,6 +161,8 @@ export const ROUTES: readonly string[] = [ "/v1/scheduling/hosts/{host}/sessions", "/v1/scheduling/hosts/{host}/simulated_busy", "/v1/scheduling/manager_sessions", + "/v1/scheduling/oauth_apps", + "/v1/scheduling/oauth_apps/{id}", "/v1/scheduling/out_of_office", "/v1/scheduling/out_of_office/by_external_id/{external_id}", "/v1/scheduling/out_of_office/{id}", diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 934b6ddbd..ff54666fb 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -71,6 +71,7 @@ export type { SchedulingHoldsResource, SchedulingHolidayCalendarsResource, SchedulingHostsResource, + SchedulingOauthAppsResource, SchedulingOutOfOfficeResource, SchedulingReassignmentsResource, SchedulingResource, diff --git a/packages/sdk/src/resources/scheduling.ts b/packages/sdk/src/resources/scheduling.ts index 990bc045f..0c9c24f28 100644 --- a/packages/sdk/src/resources/scheduling.ts +++ b/packages/sdk/src/resources/scheduling.ts @@ -16,12 +16,14 @@ import type { CreateSchedulingHostParams, CreateSchedulingHostSessionParams, CreateSchedulingManagerSessionParams, + CreateSchedulingOauthAppParams, CreateSchedulingOutOfOfficeParams, CreateSchedulingReassignmentParams, CreateSchedulingSeriesParams, DeletedSchedulingCalendarConnection, DeletedSchedulingCalendarDelegation, DeletedSchedulingHost, + DeletedSchedulingOauthApp, DeletedSchedulingOutOfOffice, DeletedSchedulingSchedule, ExplainSchedulingSlotParams, @@ -47,6 +49,7 @@ import type { SchedulingHoliday, SchedulingHolidayCalendar, SchedulingHost, + SchedulingOauthApp, SchedulingOutOfOffice, SchedulingOutOfOfficeConflict, SchedulingPageParams, @@ -70,6 +73,7 @@ import type { UpdateSchedulingEventTypeParams, UpdateSchedulingHolidayCalendarParams, UpdateSchedulingHostParams, + UpdateSchedulingOauthAppParams, UpdateSchedulingOutOfOfficeParams, UpdateSchedulingSettingsParams, UpsertSchedulingScheduleParams, @@ -792,6 +796,73 @@ export class SchedulingCalendarDelegationsResource { } } +/** + * `coss.scheduling.oauthApps`: your own Google, Microsoft or Zoom app, so calendar consent screens show + * your brand. Connections stay on the app that issued their tokens. + */ +export class SchedulingOauthAppsResource { + readonly #http: HttpClient; + + /** @internal Use `new Coss().scheduling.oauthApps`. */ + constructor(http: HttpClient) { + this.#http = http; + } + + /** Adds one, trying the secret first. New consents in this environment use it. Not retried. */ + create( + params: CreateSchedulingOauthAppParams, + options?: ReadOptions, + ): Promise> { + return this.#http.request({ + method: "POST", + path: "/v1/scheduling/oauth_apps", + body: params, + retry: false, + signal: options?.signal, + }); + } + + /** The environment's apps, at most one per provider. */ + list(options?: ReadOptions): Promise>> { + return this.#http.request>({ + method: "GET", + path: "/v1/scheduling/oauth_apps", + signal: options?.signal, + }); + } + + get(id: string, options?: ReadOptions): Promise> { + return this.#http.request({ + method: "GET", + path: `/v1/scheduling/oauth_apps/${part(id)}`, + signal: options?.signal, + }); + } + + /** A new client secret is tried before it replaces the old one, and ends an outage if it works. */ + update( + id: string, + params: UpdateSchedulingOauthAppParams, + options?: ReadOptions, + ): Promise> { + return this.#http.request({ + method: "PATCH", + path: `/v1/scheduling/oauth_apps/${part(id)}`, + body: params, + signal: options?.signal, + }); + } + + /** Refused while connections use it, since only it can refresh their access. */ + delete(id: string, options?: ReadOptions): Promise> { + return this.#http.request({ + method: "DELETE", + path: `/v1/scheduling/oauth_apps/${part(id)}`, + signal: options?.signal, + }); + } +} + /** `coss.scheduling.sessions`: a browser's authority, without your API key. */ export class SchedulingSessionsResource { readonly #http: HttpClient; @@ -1212,6 +1283,7 @@ export class SchedulingResource { readonly series: SchedulingSeriesResource; readonly calendars: SchedulingCalendarsResource; readonly calendarDelegations: SchedulingCalendarDelegationsResource; + readonly oauthApps: SchedulingOauthAppsResource; readonly sessions: SchedulingSessionsResource; readonly attendees: SchedulingAttendeesResource; readonly outOfOffice: SchedulingOutOfOfficeResource; @@ -1234,6 +1306,7 @@ export class SchedulingResource { this.series = new SchedulingSeriesResource(http); this.calendars = new SchedulingCalendarsResource(http); this.calendarDelegations = new SchedulingCalendarDelegationsResource(http); + this.oauthApps = new SchedulingOauthAppsResource(http); this.sessions = new SchedulingSessionsResource(http); this.attendees = new SchedulingAttendeesResource(http); this.settings = new SchedulingSettingsResource(http); diff --git a/packages/sdk/src/types/scheduling.ts b/packages/sdk/src/types/scheduling.ts index b501846cd..e4b28e604 100644 --- a/packages/sdk/src/types/scheduling.ts +++ b/packages/sdk/src/types/scheduling.ts @@ -675,6 +675,8 @@ export interface SchedulingSettings { allowed_origins: string[]; brand_name: string | null; accent_color: string | null; + /** Linked on a host's page once this environment has its own OAuth app and a `brand_name`. */ + privacy_url: string | null; logo: SchedulingLogo | null; email_from: string | null; sms_from: string | null; @@ -695,6 +697,8 @@ export interface UpdateSchedulingSettingsParams { allowed_origins?: string[]; brand_name?: string | null; accent_color?: string | null; + /** An https URL. */ + privacy_url?: string | null; /** `content` is a PNG or JPEG's bytes, base64-encoded, 200 KB at most. */ logo?: { content: string } | null; email_from?: string | null; @@ -736,6 +740,8 @@ export interface SchedulingCalendarConnection { status: "active" | "needs_reauth"; /** The calendar delegation that manages it, which can't be disconnected or reconnected by itself. */ delegation: string | null; + /** Your own OAuth app its access came through. `null`: coss.com's, or no consent was involved. */ + oauth_app: string | null; calendars: { id: string; external_id: string; @@ -824,6 +830,80 @@ export interface DeletedSchedulingCalendarDelegation { deleted: true; } +interface CreateSchedulingOauthAppBase { + client_id: string; + /** Never returned. */ + client_secret: string; + /** + * A page on your own domain, registered with the provider, that forwards `code` and `state` to + * `coss_callback_url`. Google requires this for an external app. + */ + redirect_uri?: string; + /** Where the browser lands after a consent that didn't name a `return_url`. */ + completion_url?: string; + /** Owners and admins are warned 14 days before. */ + secret_expires_at?: string; +} + +export type CreateSchedulingOauthAppParams = + | (CreateSchedulingOauthAppBase & { provider: "google" }) + | (CreateSchedulingOauthAppBase & { + provider: "microsoft"; + /** For an app that's only for your own tenant. */ + tenant_id?: string; + }) + | (CreateSchedulingOauthAppBase & { + provider: "zoom"; + /** The app's Secret Token, which signs its events at `zoom_event_url`. Never returned. */ + webhook_secret?: string; + }); + +export interface UpdateSchedulingOauthAppParams { + /** Tried on a connection before it replaces the old one. */ + client_secret?: string; + tenant_id?: string | null; + webhook_secret?: string | null; + redirect_uri?: string | null; + completion_url?: string | null; + secret_expires_at?: string | null; +} + +export interface SchedulingOauthApp { + object: "scheduling.oauth_app"; + id: string; + provider: "google" | "microsoft" | "zoom"; + client_id: string; + tenant_id: string | null; + redirect_uri: string | null; + /** Register this with the provider if `redirect_uri` isn't set. If it is, that page forwards here. */ + coss_callback_url: string; + completion_url: string | null; + /** Zoom only: the Event notification endpoint URL for your app's deauthorization events. */ + zoom_event_url: string | null; + /** What to enable on your app at the provider. */ + scopes: string[]; + /** + * `failing`: the provider refused the app's credentials. New consents are refused, and calendars + * connected through it can't be read until they work again. + */ + status: "active" | "failing"; + last_error_code: string | null; + last_error_at: string | null; + secret_expires_at: string | null; + /** When a consent first completed through this app, which proves its secret. */ + verified_at: string | null; + /** Calendar connections using this app. It can't be deleted while there are any. */ + connections: number; + created_at: string; + updated_at: string; +} + +export interface DeletedSchedulingOauthApp { + object: "scheduling.oauth_app"; + id: string; + deleted: true; +} + export interface SetSchedulingSimulatedBusyParams { intervals: { start: string; end: string }[]; } @@ -953,13 +1033,17 @@ export type SchedulingOutOfOfficeWebhookEventType = | "scheduling.out_of_office.ended"; export type SchedulingReassignmentWebhookEventType = "scheduling.reassignment.completed"; +export type SchedulingOauthAppWebhookEventType = + | "scheduling.oauth_app.failing" + | "scheduling.oauth_app.restored"; export type SchedulingWebhookEventType = | SchedulingBookingWebhookEventType | SchedulingCalendarWebhookEventType | SchedulingCalendarDelegationWebhookEventType | SchedulingOutOfOfficeWebhookEventType - | SchedulingReassignmentWebhookEventType; + | SchedulingReassignmentWebhookEventType + | SchedulingOauthAppWebhookEventType; /** Ids, times and statuses only: a person's details and answers come from retrieving the booking. */ export interface SchedulingBookingWebhookEventData { @@ -1005,6 +1089,21 @@ export interface SchedulingCalendarWebhookEvent { data: { host_id: string; connection_id: string; provider: string; occurred_at: string }; } +export interface SchedulingOauthAppWebhookEvent { + id: string; + object: "event"; + type: SchedulingOauthAppWebhookEventType; + api_version: string; + created_at: string; + data: { + oauth_app_id: string; + provider: string; + /** What the provider said, when it refused. */ + code: string | null; + occurred_at: string; + }; +} + export interface SchedulingCalendarDelegationWebhookEvent { id: string; object: "event"; diff --git a/packages/sdk/src/types/webhook.ts b/packages/sdk/src/types/webhook.ts index 21ed59e74..6bb63d5a1 100644 --- a/packages/sdk/src/types/webhook.ts +++ b/packages/sdk/src/types/webhook.ts @@ -17,6 +17,7 @@ import type { SchedulingBookingWebhookEvent, SchedulingCalendarDelegationWebhookEvent, SchedulingCalendarWebhookEvent, + SchedulingOauthAppWebhookEvent, SchedulingOutOfOfficeWebhookEvent, SchedulingReassignmentWebhookEvent, } from "./scheduling.ts"; @@ -494,4 +495,5 @@ export type WebhookEvent = | SchedulingCalendarWebhookEvent | SchedulingCalendarDelegationWebhookEvent | SchedulingOutOfOfficeWebhookEvent - | SchedulingReassignmentWebhookEvent; + | SchedulingReassignmentWebhookEvent + | SchedulingOauthAppWebhookEvent; diff --git a/packages/sdk/src/webhook-event-types.ts b/packages/sdk/src/webhook-event-types.ts index 97f36afac..8d39d4982 100644 --- a/packages/sdk/src/webhook-event-types.ts +++ b/packages/sdk/src/webhook-event-types.ts @@ -100,6 +100,8 @@ export const WEBHOOK_EVENT_TYPES: readonly WebhookEventType[] = [ "scheduling.out_of_office.started", "scheduling.out_of_office.ended", "scheduling.reassignment.completed", + "scheduling.oauth_app.failing", + "scheduling.oauth_app.restored", ]; /** Types only live environments receive: `domain.*`, `account.*`, `subaccount.*` and `billing.*`. */ diff --git a/packages/sdk/test/diagnose-products.test.ts b/packages/sdk/test/diagnose-products.test.ts index 0f390742b..8292d09f3 100644 --- a/packages/sdk/test/diagnose-products.test.ts +++ b/packages/sdk/test/diagnose-products.test.ts @@ -804,6 +804,7 @@ describe("diagnoseSchedulingSlots", () => { { object: "scheduling.calendar_connection" as const, delegation: null, + oauth_app: null, id: "scc_1", host: "shost_1", provider: "caldav" as const, @@ -850,6 +851,7 @@ describe("diagnoseSchedulingSlots", () => { { object: "scheduling.calendar_connection" as const, delegation: null, + oauth_app: null, id: "scc_1", host: "shost_1", provider: "google" as const, @@ -906,6 +908,7 @@ describe("diagnoseSchedulingSlots", () => { const connection = (provider: "zoom" | "simulated") => ({ object: "scheduling.calendar_connection" as const, delegation: null, + oauth_app: null, id: "scc_1", host: "shost_1", provider, @@ -945,6 +948,7 @@ describe("diagnoseSchedulingSlots", () => { { object: "scheduling.calendar_connection", delegation: null, + oauth_app: null, id: "scc_1", host: "shost_1", provider: "google", diff --git a/packages/webhooks/AGENTS.md b/packages/webhooks/AGENTS.md index dcffd43b6..a4e64f11c 100644 --- a/packages/webhooks/AGENTS.md +++ b/packages/webhooks/AGENTS.md @@ -11,7 +11,7 @@ repository root. `v2:` prefix is the AAD; ids are derived from the keys), encrypted with the first key, decrypted with the one named. Keyless `v1:` rows from before are still read by trying every key until `webhooks:reencrypt` rewrites them. Every write of a secret column goes through `encryptSecret`, and `reencryptWebhookSecrets` must cover every - encrypted column (today the webhook and Hooks endpoints' `secret_ciphertext` and `previous_secret_ciphertext`, `log_drain.secret_ciphertext` and `auth_header_ciphertext`, `risk_settings.captcha_secret_ciphertext`, `scheduling_calendar_connection.encrypted_credentials`, `scheduling_calendar_delegation.encrypted_credentials` and `migration.credentials_ciphertext`, the provider key an import from another provider holds until it finishes). Rotation runbook: README.md, + encrypted column (today the webhook and Hooks endpoints' `secret_ciphertext` and `previous_secret_ciphertext`, `log_drain.secret_ciphertext` and `auth_header_ciphertext`, `risk_settings.captcha_secret_ciphertext`, `scheduling_calendar_connection.encrypted_credentials`, `scheduling_calendar_delegation.encrypted_credentials`, `scheduling_oauth_app.encrypted_credentials` and `migration.credentials_ciphertext`, the provider key an import from another provider holds until it finishes). Rotation runbook: README.md, "Rotating `WEBHOOK_SECRETS_KEY`". The initial upgrade from v1-only readers must use README.md's "Upgrading from v1 webhook secret storage" coordinated cutover, not a rolling deployment. - **Delivery engine** (`@coss/webhooks/engine`): webhook deliveries, log drain batches (and future products) share diff --git a/packages/webhooks/src/reencrypt.ts b/packages/webhooks/src/reencrypt.ts index fc4ad3ff2..d021456e5 100644 --- a/packages/webhooks/src/reencrypt.ts +++ b/packages/webhooks/src/reencrypt.ts @@ -20,6 +20,7 @@ const { schedulingCalendarConnection, schedulingCalendarDelegation, schedulingHolidayCalendar, + schedulingOauthApp, smsAccount, webhookEndpoint, } = schema; @@ -77,6 +78,7 @@ export async function reencryptWebhookSecrets(opts: { await reencryptCalendarCredentials({ ...opts, batchSize }, result); await reencryptCalendarDelegations({ ...opts, batchSize }, result); await reencryptHolidayFeeds({ ...opts, batchSize }, result); + await reencryptSchedulingOauthApps({ ...opts, batchSize }, result); return result; } @@ -520,6 +522,77 @@ async function reencryptCalendarDelegations( } } +async function reencryptSchedulingOauthApps( + opts: { + keys: SecretsKeyRing; + dryRun?: boolean; + batchSize: number; + scope?: { organizationIds: string[] }; + }, + result: ReencryptResult, +): Promise { + const { keys } = opts; + let after = ""; + for (;;) { + const rows = await db + .select({ + id: schedulingOauthApp.id, + organizationId: schedulingOauthApp.organizationId, + stored: schedulingOauthApp.encryptedCredentials, + }) + .from(schedulingOauthApp) + .where( + and( + gt(schedulingOauthApp.id, after), + opts.scope + ? inArray(schedulingOauthApp.organizationId, opts.scope.organizationIds) + : undefined, + ), + ) + .orderBy(asc(schedulingOauthApp.id)) + .limit(opts.batchSize); + for (const row of rows) { + const { stored } = row; + result.checked++; + if (isEncryptedWithPrimary(stored, keys)) continue; + let encryptedCredentials: string; + try { + encryptedCredentials = encryptSecret(decryptSecret(stored, keys), keys); + } catch (error) { + result.unreadable.push({ + id: row.id, + organizationId: row.organizationId, + error: error instanceof Error ? error.message : String(error), + }); + continue; + } + if (opts.dryRun) { + result.reencrypted++; + continue; + } + const [updated] = await db + .update(schedulingOauthApp) + .set({ + encryptedCredentials, + updatedAt: sql`${schedulingOauthApp.updatedAt}`, + }) + .where( + and( + eq(schedulingOauthApp.id, row.id), + eq(schedulingOauthApp.organizationId, row.organizationId), + eq(schedulingOauthApp.encryptedCredentials, stored), + ), + ) + .returning({ id: schedulingOauthApp.id }); + if (updated) result.reencrypted++; + else result.conflicts++; + } + const last = rows.at(-1); + if (!last || rows.length < opts.batchSize) break; + after = last.id; + } +} + /** Provider API keys of imports from other providers (`@coss/migrate`), held until they finish. */ async function reencryptMigrationCredentials( opts: { diff --git a/tooling/sdkgen/src/methods.ts b/tooling/sdkgen/src/methods.ts index 144781c68..424ca04d3 100644 --- a/tooling/sdkgen/src/methods.ts +++ b/tooling/sdkgen/src/methods.ts @@ -60,6 +60,11 @@ export const SDK_METHODS: Readonly> = { "PATCH /v1/scheduling/calendar_delegations/{id}": "scheduling.calendarDelegations.update", "POST /v1/scheduling/calendar_delegations/{id}/verify": "scheduling.calendarDelegations.verify", "DELETE /v1/scheduling/calendar_delegations/{id}": "scheduling.calendarDelegations.delete", + "POST /v1/scheduling/oauth_apps": "scheduling.oauthApps.create", + "GET /v1/scheduling/oauth_apps": "scheduling.oauthApps.list", + "GET /v1/scheduling/oauth_apps/{id}": "scheduling.oauthApps.get", + "PATCH /v1/scheduling/oauth_apps/{id}": "scheduling.oauthApps.update", + "DELETE /v1/scheduling/oauth_apps/{id}": "scheduling.oauthApps.delete", "POST /v1/scheduling/hosts/{host}/sessions": "scheduling.sessions.createForHost", "POST /v1/scheduling/booker_sessions": "scheduling.sessions.createForBooker", "POST /v1/scheduling/bookings/{id}/manage_sessions": "scheduling.sessions.createForBooking", @@ -371,6 +376,7 @@ export const NO_RETRY: ReadonlySet = new Set([ "POST /v1/forms", "POST /v1/scheduling/hosts", "POST /v1/scheduling/calendar_delegations", + "POST /v1/scheduling/oauth_apps", "POST /v1/scheduling/hosts/{host}/schedules", "POST /v1/scheduling/out_of_office", "POST /v1/scheduling/bookings/{id}/reassign",