$schema
| string | | |
brand
| object | | |
brand.colors
| map | | |
brand.faviconUrl
| string | | |
brand.fonts
| map | | |
brand.logoMarkUrl
| string | | |
brand.logoUrl
| string | | |
brand.name
| string | | |
brand.voice
| string | | |
connectors
| object[] | | |
connectors[].appearance
| object | | |
connectors[].config
| map | | |
connectors[].embedEnabled
| boolean | | |
connectors[].embedOptions
| object | | |
connectors[].externalRef
| string | | Your own name for this resource (printable ASCII, no spaces, ≤200). A repeat create naming a reference already held answers that resource (200, idempotent: true) instead of making a second one. |
connectors[].id
| string | | |
connectors[].knowledgeEnabled
| boolean | | |
connectors[].knowledgeScopes
| "chunks" | "search" | "ask" | "llms" | "questions" | "suggestions"[]
| | |
connectors[].metadata
| map | | Free-form flat tags (≤50 keys; values string, number, boolean or null), filterable on the list with ?metadata[key]=value. The older metadata.forgedeck_ref is still accepted as an alias of externalRef. |
connectors[].providerId
| string | yes | |
connectors[].space
| string | yes | |
consent
| object | | |
consent.provider
| string | yes | |
domains
| object[] | | |
domains[].alsoRedirectWww
| boolean | | Also answer www.<hostname> (same path) with a permanent redirect to this address, written into our proxy. Default false: a domain is registered for ONE hostname, so without it www.acme.com/docs reaches whatever else answers that name (often your own app, which 404s). Leave it off when your own app already redirects www; GET …/setup reports wwwNotCovered either way. Refused (422) on a hostname that already starts with www.. Point the www name’s DNS where the bare name points; for a whole-host domain we also request its certificate. |
domains[].basePath
| string | | |
domains[].hostname
| string | yes | |
domains[].id
| string | | |
domains[].ipAllowList
| string[] | | IP addresses and CIDR blocks that may READ this address — 203.0.113.7, 203.0.113.0/24, 2001:db8::/32. An empty list means anywhere, which is what every domain has by default. Enforced before a page is rendered, against the client address the edge resolved; an entry that is not an address or a block is a 422, because one that nothing can parse would never match and would lock out the world silently. |
domains[].onUnverified
| "hold" | "fail"
| | When state is verified or active and the ownership proof is not served yet: hold (the default) keeps the domain claimed, reports the step waiting: <what each proof method saw> and carries on with every other unit (the apply exits 0; re-apply once the file is served); fail stops the run at this domain, as before. |
domains[].redirectTo
| string | null | | |
domains[].site
| string | | |
domains[].space
| string | | |
domains[].state
| "claimed" | "verified" | "active"
| | How far apply takes the domain — a MINIMUM, forward only: a domain already further along is never walked back (an active domain declared verified stays active). claimed (the default): the domain is added and held; manual lists the ownership TXT record (and the .well-known file) to publish. verified: ownership is checked (verify with activate: false) and traffic is NOT switched — your own application keeps serving the address. active: verified, then activated — we start answering it. The steps run in order, verify → activate. When no proof is served yet, onUnverified decides: hold (the default) keeps the domain claimed, reports the step waiting with what each proof method saw, and carries on with every other unit — the next apply takes it forward; fail stops the run there. A plan reports the same waiting before anything is written. A step that just activated reports settling: true: routing takes about a minute and GET …/setup may answer pending (pendingReason: settling) meanwhile. |
executor
| object | | |
executor.completionWebhook
| object | | |
executor.deletionMode
| "erase" | "anonymize"
| | |
executor.ensureKey
| boolean | | |
executor.retention
| object | | |
force
| boolean | | |
mail
| object | | The organization’s own outbound mail: {provider, sender, apiKey: ${MAIL_API_KEY}, domain?, region?, baseUrl?, webhookSecret?} — the body of PUT /orgs/{orgId}/mail/provider. Status notices, newsletters and community mail then go out from sender through that provider. apiKey and webhookSecret are write-only: plan prints <write-only>, export leaves them out. |
mail.apiKey
| string | | Write-only. Use ${ENV} (apiKey: ${MAIL_API_KEY}): substituted by the CLI, never printed by plan, never exported. Omit to keep the stored key (required the first time). |
mail.baseUrl
| string | null | | The provider’s API origin, for a provider that runs at an address you choose (hermetica: default https://hermeticor.com; a self-hosted or regional install sets its own). Ignored for the others. |
mail.domain
| string | null | | |
mail.provider
| "brevo" | "sendgrid" | "postmark" | "mailgun" | "resend" | "hermetica"
| yes | |
mail.region
| "us" | "eu" | null
| | |
mail.sender
| string | null | | The From this organization’s mail goes out as through this provider ("Acme Status status@acme.com") — status notices, newsletters and community mail alike. Null: the organization’s From address (PATCH /orgs/{orgId}/email-from), else ours. Its domain must be verified at the provider (GET …/mail/provider/domain). |
mail.webhookSecret
| string | null | | Write-only. The signing secret of the provider’s bounce webhook (hermetica: the endpoint secret behind Hermetica-Signature); once stored, an unsigned or wrongly signed call is refused. Null removes it; omit to keep it. The GET answers webhookSecretSet only. |
org
| string | | |
orgSettings
| map | | Organization settings by registry key (GET /orgs/{orgId}/settings lists them), each PUT …/settings/{key}. |
pages
| object[] | | |
privacy
| object | | |
privacy.executor
| "cookiemunch"
| yes | |
prune
| boolean | | |
readerAuth
| "atlas" | object | object
| | How readers sign in, for the whole organization. Three forms: "atlas" — the Atlas reader provider, on the instance linked to this organization or, failing that, the live atlas_byo provider (whose order and settings are kept, so it plans as unchanged); {method: "atlas", instanceId?, frontendApi?, audience?} — the same, naming the instance (ins_…), a custom frontend-API domain, or an audience other than forumulate:<orgId>; {providers, signInUrl?, signOutUrl?} — the DECLARED registry: apply replaces the providers (a stored provider left out is removed) and the URLs. Do not combine with an organization-level reader_auth entry. Guide: https://forumulate.com/guides/reader-sign-in-with-your-app#change-a-spaces-sign-in-method. |
readerIdentity
| "space" | "org"
| | The org default reader-identity scope, space or org; shorthand for the reader.identity_scope org setting. |
reader_auth
| object[] | | |
reader_auth[].approveNewMembers
| boolean | null | | |
reader_auth[].hostedSignup
| boolean | null | | |
reader_auth[].identityPolicy
| object | null | | Per-space override of the reader identity keys (reader.idp_email_collision, reader.idp_user_deleted, reader.idp_deletion_grace_days, reader.idp_user_banned). Each field is resolved on its own: space → org setting → default. Only meaningful on a space override. |
reader_auth[].outboundSync
| object | null | | |
reader_auth[].providers
| object[] | | |
reader_auth[].signInUrl
| string | null | | Your own sign-in page (https). When set, the reader sign-in surface shows one "Sign in to continue" link to this URL instead of the provider buttons (an Atlas provider keeps its own sign-in page), with these query parameters appended: returnTo — the absolute URL of the page the reader was on (always set, replacing any returnTo already in the URL); space — the space slug; spaceId — the space id. Your page must read returnTo and send the reader back there after sign-in, for example https://app.example.com/sign-in receives https://app.example.com/sign-in?returnTo=https%3A%2F%2Fdocs.example.com%2Fguides%2Fintro&space=guides&spaceId=…. A {returnTo} placeholder anywhere in the URL (https://app.example.com/login?next={returnTo}) is replaced with the URL-encoded address instead, and returnTo is then not appended again. space/spaceId are only added when the URL does not already carry them. Configured once for the org, one page serves every space: send the reader on to /api/public/orgs/{orgId}/spaces/{spaceId}/visitor-oidc/start (or your own flow) using spaceId, then back to returnTo. returnTo is at most 2048 characters: a longer page address is sent without its query string and fragment, and failing that as the space root. Saving it answers warnings[] (sign_in_url_overrides_providers, signin_url_unreachable, sign_in_url_return_to_unconfirmed). null clears it. |
reader_auth[].signOutUrl
| string | null | | |
reader_auth[].sourceOfTruth
| "customer" | "forumulate" | null
| | |
reader_auth[].space
| string | | |
sites
| object[] | | |
sites[].config
| map | | Exactly what PATCH /orgs/{orgId}/sites/{siteId} takes as config, checked by that door. |
sites[].configMode
| "merge" | "replace"
| | |
sites[].id
| string | | |
sites[].name
| string | | |
sites[].slug
| string | yes | |
sites[].spaces
| string[] | | |
sites[].subdomain
| string | null | | The site's platform subdomain label (acme → acme.<platform domain>), claimed on apply. null releases it; absent leaves it alone. |
sources
| object[] | | |
sources[].content
| string | | |
sources[].kind
| "url" | "text"
| yes | |
sources[].spaceId
| string | null | | |
sources[].title
| string | yes | |
sources[].url
| string | | |
spaces
| object[] | | |
spaces[].about
| object | string | | |
spaces[].authors
| map | | A BLOG space's author registry by key (PUT …/authors/{key} each); a byline names one as {author: <key>}. |
spaces[].categories
| object[] | | |
spaces[].collections
| object[] | | |
spaces[].compare
| object | | A COMPARE catalog: the keys inline, and/or catalog: (a YAML/JSON file beside this one) and matrix: (CSV text, or the CLI reads it from a file). |
spaces[].contactForm
| object | string | | |
spaces[].description
| string | | One sentence about the space (up to 300 characters): the description search engines and social cards show. Shorthand for seo.description (the same stored value, kept in step on every apply); naming both with different text is refused. |
spaces[].frontDoor
| map | | |
spaces[].homeDoc
| object | string | | The space intro (PUT …/home-doc): {body, translations}, or a Markdown file whose .<code>.md twins beside it are its languages. |
spaces[].id
| string | | |
spaces[].legalDocuments
| object[] | | A LEGAL space's documents: binds each page (by slug) as kind (POST …/legal/bind). version: bump re-editions only when the agreed words changed. |
spaces[].locales
| object[] | | |
spaces[].monitors
| object[] | | |
spaces[].name
| string | | |
spaces[].openapi
| object | object[] | | An API reference (or several): url or spec (the document, or a file name the CLI reads), the collection by name, and the import's options (mode, slugPrefix, pages, …). Re-imported when the spec changes. |
spaces[].piecePreset
| string | | |
spaces[].posts
| object[] | | A BLOG space's posts by slug: the blog door's fields, body as Markdown, status (published by default, published without mailing anyone), and translations per declared language. |
spaces[].preset
| string | | One of this space type's named layouts, as published by GET /space-types. Unset means the recommended one, resolved at read — so an improvement to the default reaches every space that never chose. An unknown name for the type is a 422 that lists the valid ones. |
spaces[].redirects
| object[] | | The whole table, replacing what is stored — except the rules the platform wrote itself when a page or a collection was renamed, which are kept unless your list claims the same from. Renaming a collection moves one address — its hub, /c/<slug> — and we write that rule for you; the pages filed under it are addressed by their own slug within the space and do not move. |
spaces[].releases
| object[] | object | object | | |
spaces[].seed
| boolean | | Whether to create the starter structure this kind of space ships with. Left unset it follows who is asking: a console session gets it (an empty space is a worse first screen than an example one), a personal access token does not — an agent creating a space is about to write its own structure, and ours arrives as content it has to find and delete first. |
spaces[].seo
| object | | |
spaces[].settings
| object | | Merged into the space (PATCH …/spaces/{id} {settings}), STRICT: an unknown key is refused by name. blog, forum and status are the type's own settings doors. |
spaces[].shape
| object | | |
spaces[].slug
| string | yes | |
spaces[].theme
| object | | |
spaces[].topics
| object[] | | A FORUM space's staff topics by slug: title, body (Markdown), category by name, tags, pin (true, or {pinned, global, banner, until}), featuredLink, poll, customization, who it credits (postAs, byline, bylines), createdAt (a backdate, applied when the topic is created) and translations. Members' topics are never touched. |
spaces[].translations
| map | | |
spaces[].type
| "DOCS" | "FORUM" | "BLOG" | "ROADMAP" | "CHANGELOG" | "STATUS" | "LEGAL" | "FAQ" | "CAREERS" | "CONTACT" | "EVENTS" | "COURSES" | "CUSTOM" | "CONTENT" | "ABOUT" | "COMPARE"
| | |
spaces[].visibility
| "PUBLIC" | "PRIVATE"
| | |
status_monitors
| object[] | | |
status_monitors[].alertsEnabled
| boolean | | |
status_monitors[].allowGetPing
| boolean | | Heartbeat only: whether GET /status/ping/{monitorId}/{token} is accepted. A GET URL ends up in browser history, proxy logs and link previews, so it is off (405) for a monitor created without it; monitors created before it existed keep GET on. POST (path or header form) always works. |
status_monitors[].audience
| object | | Who may see this on the public page: everyone (default), signedIn (any signed-in reader) or groups (a signed-in member of one of groups). Hidden rows are dropped by the API before anything is sent. The machine doors — summary.json, api/v2, the feeds, the badge and the widget — always answer as an anonymous reader. |
status_monitors[].barDays
| 30 | 60 | 90 | null
| | Its own day strip length on the board; null follows the theme’s bars.days. |
status_monitors[].bodyExpect
| string | null | | |
status_monitors[].bodyPath
| string | null | | |
status_monitors[].certExpiryDays
| integer | null | | |
status_monitors[].confirmRegions
| integer | | How many regions must see it failing before it reads down (capped at how many report). Default 1. |
status_monitors[].declaredState
| "operational" | "degraded" | "partial_outage" | "major_outage" | "maintenance" | null
| | A state a person declares, which wins over the checks until it is cleared with null. Every field of this body — this one, alertsEnabled and both thresholds included — is applied on create exactly as on PATCH. |
status_monitors[].description
| string | null | | What the component is, in a sentence — shown to readers beside its name. |
status_monitors[].display
| "bars" | "status" | "hidden-unless-degraded"
| | bars (default) draws its day bars; status only its state; hidden-unless-degraded leaves it off the page while it is operational.
|
status_monitors[].expectStatus
| integer | null | | |
status_monitors[].failureThreshold
| integer | | |
status_monitors[].group
| string | null | | |
status_monitors[].headers
| object[] | null | | |
status_monitors[].id
| string | | |
status_monitors[].intervalSeconds
| integer | | Seconds between checks (http, clamped to 30–3600, default 60) or the longest wait for a ping (heartbeat, clamped to 60–604800, default 3600). PATCH clamps by the same rule. |
status_monitors[].json
| object | null | | |
status_monitors[].keyword
| string | null | | |
status_monitors[].keywordAbsent
| boolean | | Invert keyword: the check FAILS while the term is in the body ("error", "maintenance mode") — Better Stack's "keyword absent". Default false. |
status_monitors[].kind
| "http" | "heartbeat" | "manual" | "space" | "statuspage" | "tcp" | "udp" | "icmp" | "dns" | "tls" | "smtp" | "imap" | "pop3" | "domain" | "browser" | "instatus" | "betterstack" | "feed" | "watchtower"
| | http (default) polls url; heartbeat waits for your job to ping; manual is probed by nothing — its state is declaredState (else operational) or what an open incident linked to it says; space rolls up another status page of this organization (targetSpaceId) on a hub page; statuspage mirrors a vendor’s public Statuspage (url, plus vendorComponent); watchtower mirrors one Watchtower monitor (watchtower), read every intervalSeconds from its status badge. Fixed at creation: a PATCH cannot turn one kind into another — delete and re-create instead.
|
status_monitors[].linkUrl
| string | null | | Where the component’s own docs or product live; its name links there. |
status_monitors[].maxLatencyMs
| integer | null | | |
status_monitors[].method
| "GET" | "HEAD" | "POST"
| | |
status_monitors[].metricThresholds
| object | null | | |
status_monitors[].name
| string | yes | |
status_monitors[].paused
| boolean | | Stop probing without deleting: the row and its history stay on the page, no new checks are taken and no alert is sent, and the component reads unknown (or its declaredState) until it is resumed. alertsEnabled: false only mutes the alerts while the probe keeps running. |
status_monitors[].pingSecretVersion
| integer | | |
status_monitors[].probe
| object | | What a protocol monitor probes (kinds tcp, udp, icmp, dns, tls, smtp, imap, pop3, domain, browser): host (+ port), a DNS record/resolver/expect, the days a certificate or registration must have left, a browser check’s url and selector. Validated per kind; every host must resolve to a public address. |
status_monitors[].region
| string | null | | Where it runs ("EU", "US-East"). theme.status.components.groupBy: region groups by it. |
status_monitors[].regions
| string[] | | Probe regions this component is checked from (GET …/status/regions). Empty: the primary checker only. |
status_monitors[].requestBody
| string | null | | |
status_monitors[].requestHeaders
| map | null | | |
status_monitors[].showUptime
| boolean | | Show its uptime figure beside the strip. Default true. |
status_monitors[].slug
| string | | Its stable address within the page (component page, per-component subscription), normalised to lowercase words joined by hyphens. Minted from the name on create when omitted; unique per page — a taken one is a 409. Renaming the component does not move it. |
status_monitors[].space
| string | yes | |
status_monitors[].startsOn
| string | null | | The day it started existing. Earlier days draw as "not yet" rather than as no data. Null: from its first check (a manual component: from the day it was created). |
status_monitors[].successThreshold
| integer | | |
status_monitors[].targetSpaceId
| string | | Kind space: the status page it rolls up (state, uptime and days are that page’s). |
status_monitors[].url
| string | null | | |
status_monitors[].vendorComponent
| string | null | | Kind statuspage: the vendor component’s id or exact name in its summary.json. Null: the vendor page’s overall indicator. |
status_monitors[].visible
| boolean | | |
status_monitors[].watchtower
| object | | Kind watchtower: the monitor it mirrors. Its state follows the monitor within one interval; an inbound watchtower hook naming the same monitor opens its incidents on this component. |
tokens
| object[] | | |
tokens[].capabilities
| string[] | | |
tokens[].expiresInDays
| integer | | |
tokens[].name
| string | yes | |
tokens[].ref
| string | yes | Your own name for this resource (printable ASCII, no spaces, ≤200). A repeat create naming a reference already held answers that resource (200, idempotent: true) instead of making a second one. |
tokens[].scopeGrants
| string[] | | |
tokens[].scopes
| string[] | yes | |
tokens[].spaces
| string[] | | |
translationOverrides
| map | | Interface strings per language, { <locale>: { <key>: <text> } } (PUT /orgs/{orgId}/translation-overrides each). |
version
| 2
| yes | |
webhooks
| object[] | | |
webhooks[].active
| boolean | | |
webhooks[].alertContacts
| string[] | null | | E-mail addresses told when this endpoint keeps failing: at 1 hour, at 24 hours, and when it is switched off. Up to 10. Omitted leaves them as they are; null or [] means the organization's owners. |
webhooks[].alert_contacts
| string[] | null | | E-mail addresses told when this endpoint keeps failing: at 1 hour, at 24 hours, and when it is switched off. Up to 10. Omitted leaves them as they are; null or [] means the organization's owners. |
webhooks[].bodyEncoding
| "json" | "form"
| | How the delivery body is encoded. json (the default) POSTs application/json; form POSTs application/x-www-form-urlencoded, for a receiver that cannot parse JSON bodies. |
webhooks[].categoryIds
| string[] | null | | Narrow delivery to events whose payload names one of these forum categories. Omitted or null means no filter — the endpoint hears the event wherever it happens. |
webhooks[].contentTypes
| string[] | null | | Narrow content.* events to entries of these content types (by key). Omitted or null means no filter. A model change to the whole model is always delivered. |
webhooks[].events
| "page.created" | "page.published" | "page.updated" | "page.deleted" | "item.stale" | "assertion.failed" | "change_request.merged" | "version.created" | "version.default_changed" | "locale.created" | "comment.created" | "comment.moderated" | "comment.resolved" | "comment.deleted" | "content_rule.created" | "content_rule.deleted" | "reusable.created" | "reusable.updated" | "reusable.deleted" | "variable.set" | "variable.deleted" | "page_template.created" | "page_template.deleted" | "topic.created" | "topic.published" | "topic.edited" | "topic.deleted" | "topic.recovered" | "post.created" | "post.edited" | "post.deleted" | "post.recovered" | "post.flagged" | "topic.solved" | "topic.moved" | "reaction.created" | "reaction.removed" | "chat.message_created" | "notification.created" | "category.created" | "category.updated" | "category.deleted" | "tag.created" | "tag.deleted" | "reviewable.created" | "reviewable.updated" | "user.joined" | "user.suspended" | "user.promoted" | "user.silenced" | "user.anonymized" | "badge.granted" | "group.member_added" | "group.member_removed" | "group.updated" | "group.deleted" | "form.submitted" | "status.incident.created" | "status.incident.updated" | "status.incident.resolved" | "event.published" | "event.updated" | "event.cancelled" | "rsvp.created" | "rsvp.updated" | "rsvp.deleted" | "rsvp.checked_in" | "roadmap.status_changed" | "roadmap.merged" | "roadmap.shipped" | "course.enrolled" | "lesson.completed" | "quiz.passed" | "course.completed" | "certificate.issued" | "course.purchased" | "course.refunded" | "quiz.failed" | "quiz.review_needed" | "blog.membership.started" | "blog.membership.ended" | "blog.review_requested" | "blog.reviewed" | "application.created" | "application.stage_changed" | "application.offer_answered" | "role.opened" | "role.closed" | "contact.submitted" | "about.updated" | "compare.catalog.updated" | "compare.stale" | "release.review_requested" | "release.reviewed" | "membership.started" | "membership.renewed" | "membership.canceled" | "membership.ended" | "shop.order.paid" | "shop.order.refunded" | "form.paid" | "content.entry.published" | "content.entry.unpublished" | "content.entry.deleted" | "content.entry.scheduled" | "content.route.changed" | "content.release.published" | "content.model.changed" | "content.entry.draft_saved" | "content.entry.unscheduled" | "content.asset.uploaded" | "content.asset.updated" | "content.asset.deleted" | "content.locale.added" | "content.locale.updated" | "content.locale.removed" | "domain.added" | "domain.verified" | "domain.failed" | "domain.activated" | "domain.removed" | "site.created" | "site.updated" | "site.deleted" | "theme.published" | "theme.saved" | "theme.deleted" | "theme.component_saved" | "theme.component_deleted" | "theme.component_installed" | "theme.component_uninstalled" | "theme.listing_published" | "theme.listing_withdrawn" | "legal.published" | "legal.removed" | "legal.access_requested" | "legal.privacy_requested" | "space.created" | "space.updated" | "space.deleted" | "space.restored" | "space.purged" | "snapshot.created" | "snapshot.restored" | "snapshot.deleted" | "collection.created" | "collection.updated" | "collection.deleted" | "form.created" | "form.updated" | "form.deleted" | "token.created" | "token.rotated" | "token.revoked" | "pat.expiring" | "integration_token.expiring" | "partner_link.expiring" | "executor_key.expiring" | "service_account.created" | "service_account.updated" | "service_account.deleted" | "role.created" | "role.updated" | "role.deleted" | "invite.created" | "invite.revoked" | "invite.accepted" | "user.removed" | "user.role_changed" | "group.grant_added" | "group.grant_removed" | "share_link.created" | "share_link.revoked" | "webhook.created" | "webhook.updated" | "webhook.deleted" | "webhook.secret_rotated" | "webhook.disabled" | "extension.installed" | "extension.updated" | "extension.uninstalled" | "automation.created" | "automation.updated" | "automation.deleted" | "library.created" | "library.updated" | "library.deleted" | "library.member_added" | "library.member_removed" | "openapi.spec.registered" | "openapi.spec.synced" | "openapi.spec.removed" | "openapi.changed" | "sdk.published" | "sso.connection_created" | "sso.connection_updated" | "sso.connection_deleted" | "sso.enforcement_changed" | "scim.token_created" | "scim.token_rotated" | "scim.token_revoked" | "oauth_client.created" | "oauth_client.secret_rotated" | "oauth_client.deleted" | "group.created" | "badge_rule.created" | "badge_rule.updated" | "badge_rule.deleted" | "badge_rule.ran" | "session.revoked" | "org.updated" | "org.deleted" | "org.subscribed" | "org.subscription_changed" | "org.unsubscribed" | "payments.connected" | "payments.updated" | "payments.disconnected" | "email_template.saved" | "email_template.reset" | "mail.suppression_added" | "mail.suppression_removed" | "mail_provider.connected" | "mail_provider.disconnected" | "connector.installed" | "connector.updated" | "connector.uninstalled" | "connector.token_minted" | "connector.token_revoked" | "chat_provider.created" | "chat_provider.updated" | "chat_provider.deleted" | "embed_host.saved" | "embed_host.removed" | "embed_feed.created" | "embed_feed.updated" | "embed_feed.deleted" | "github_agent.connected" | "github_agent.disconnected" | "integration.installed" | "integration.updated" | "integration.uninstalled" | "cloudflare.connected" | "cloudflare.updated" | "cloudflare.disconnected" | "kb.changed" | "status.component.created" | "status.component.updated" | "status.component.status_changed" | "status.component.removed" | "status.metric.created" | "status.metric.updated" | "status.metric.removed" | "status.template.created" | "status.template.updated" | "status.template.removed" | "status.translation.saved" | "status.translation.removed" | "status.inbound.created" | "status.inbound.removed" | "status.inbound.rotated" | "status.monitor.ping_secret_rotated" | "careers.ats.connected" | "careers.ats.updated" | "careers.ats.disconnected" | "careers.ats.synced" | "form_template.created" | "form_template.updated" | "form_template.deleted" | "form_template.attached" | "ai.provider.connected" | "ai.provider.disconnected" | "ai.embedding_provider.connected" | "ai.embedding_provider.disconnected" | "ai.source.added" | "ai.source.removed" | "translation.created" | "translation.updated" | "translation.deleted" | "speaker.created" | "speaker.updated" | "speaker.deleted" | "event_session.created" | "event_session.updated" | "event_session.deleted" | "ticket_tier.created" | "ticket_tier.updated" | "ticket_tier.deleted" | "ticket_order.refunded" | "ticket_order.paid" | "ticket_order.requested" | "promo_code.created" | "promo_code.updated" | "promo_code.deleted" | "course_path.created" | "course_path.updated" | "course_path.deleted" | "publish.completed" | "page.live" | "asset.uploaded" | "asset.updated" | "asset.deleted" | "asset_folder.created" | "asset_folder.updated" | "asset_folder.deleted" | "avatar.updated" | "avatar.removed" | "custom_field.created" | "custom_field.updated" | "custom_field.deleted" | "custom_field.values_changed" | "reader_domain.configured" | "reader_domain.verified" | "reader_domain.failed" | "reader_domain.removed" | "shape.updated" | "site_preview.created" | "site_preview.revoked" | "theme.origin_removed" | "theme.origin_restored" | "theme.component_listing_published" | "theme.component_listing_withdrawn" | "canned_reply.created" | "canned_reply.updated" | "canned_reply.deleted" | "flag_type.created" | "flag_type.updated" | "flag_type.deleted" | "reaction_palette.updated" | "custom_reaction.saved" | "custom_reaction.deleted" | "custom_emoji.saved" | "custom_emoji.deleted" | "saved_search.created" | "saved_search.updated" | "saved_search.deleted" | "member_note.created" | "member_note.deleted" | "conversation.created" | "conversation.replied" | "conversation.participant_added" | "conversation.participant_removed" | "relationship.muted" | "relationship.unmuted" | "relationship.followed" | "relationship.unfollowed" | "screening.rule_added" | "screening.rule_removed" | "job.completed" | "webhook_endpoint.failing" | "webhook_endpoint.disabled" | "audit.event"[]
| yes | The events this endpoint should hear, by name. At least one; GET /webhooks/events publishes the whole vocabulary with a sample payload for each. |
webhooks[].externalRef
| string | | Your own name for this resource (printable ASCII, no spaces, ≤200). A repeat create naming a reference already held answers that resource (200, idempotent: true) instead of making a second one. |
webhooks[].format
| "json" | "slack" | "discord"
| | The body shape. json (the default) sends our own event payload; slack and discord send a message that platform renders natively, so an incoming-webhook URL from either can be pasted in with nothing in between. |
webhooks[].groupIds
| string[] | null | | Narrow group, user and badge events to these groups. Omitted or null means no filter; it has no effect on events that name no group. |
webhooks[].id
| string | | |
webhooks[].kind
| "webhook" | "deploy_hook"
| | webhook (the default) delivers signed events. deploy_hook sends a plain POST with an EMPTY body (no signature, no payload) to a Vercel, Netlify, Cloudflare Pages or Seolyticize deploy-hook URL, on content.* events and the publish events page.published, page.live and topic.published only. Debounced: Vercel/Netlify/Cloudflare one call 30 s after the last change (at most 5 min after the first); Seolyticize one call 60 s after the last publish (at most 2 min after the first), per hook, so a 30-page apply is one call. The URL is write-only: reads answer its host and /********. Fixed at create.
|
webhooks[].locales
| string[] | null | | Narrow content.* events to entries in these locales (BCP-47 codes, or default for a space's source language). Omitted or null means no filter. |
webhooks[].metadata
| map | | Free-form flat tags (≤50 keys; values string, number, boolean or null), filterable on the list with ?metadata[key]=value. The older metadata.forgedeck_ref is still accepted as an alias of externalRef. |
webhooks[].ref
| string | | |
webhooks[].signatureVersion
| "both" | "v1"
| | Which signature headers each delivery carries. v1 is the timestamped scheme alone and is the default for a NEW endpoint; both adds the legacy body-only sha256=, which is replayable forever and exists only for receivers not yet migrated. |
webhooks[].space
| string | | |
webhooks[].tagIds
| string[] | null | | Narrow delivery to events whose payload carries one of these tags. Omitted or null means no filter. Tags are matched by name, not by id. |
webhooks[].url
| string | yes | Where deliveries are POSTed. Must be https outside development, and is called with a signature header your receiver verifies — see the webhooks guide for the scheme. |