> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gavai.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Event catalogue

> Every event family the platform emits — what triggers it, what's in the payload, and what to subscribe to it for.

export const ExpectedOutput = ({caption = "Expected output", children}) => <div className="gv-output">
    <div className="gv-output__label">
      <svg className="gv-output__icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <polyline points="15 10 20 15 15 20" />
        <path d="M4 4v7a4 4 0 0 0 4 4h12" />
      </svg>
      <span>{caption}</span>
    </div>
    <div className="gv-output__body">{children}</div>
  </div>;

export const Def = ({term, type, defaultValue, required, children}) => <>
    <dt className="gv-def-list__term">
      <span className="gv-def-list__name">{term}</span>
      {type && <span className="gv-def-list__type">{type}</span>}
      {required && <span className="gv-def-list__required">required</span>}
      {defaultValue !== undefined && <span className="gv-def-list__default">
          default <code>{defaultValue}</code>
        </span>}
    </dt>
    <dd className="gv-def-list__description">{children}</dd>
  </>;

export const DefList = ({children}) => <dl className="gv-def-list">{children}</dl>;

export const BrandCard = ({eyebrow, title, href, icon, badge, cta, featured = false, horizontal = false, children}) => {
  const classes = ["gv-card", featured && "gv-card--featured", horizontal && "gv-card--horizontal"].filter(Boolean).join(" ");
  const inner = <>
      {icon && <span className="gv-card__icon" aria-hidden="true">
          <Icon icon={icon} />
        </span>}
      <div className="gv-card__main">
        {(eyebrow || badge) && <div className="gv-card__meta">
            {eyebrow && <Eyebrow label={eyebrow} tone="muted" />}
            {badge && <span className="gv-card__badge">{badge}</span>}
          </div>}
        <h3 className="gv-card__title">{title}</h3>
        {children && <div className="gv-card__body">{children}</div>}
        {cta && <span className="gv-card__cta">
            {cta}
            <svg className="gv-card__cta-arrow" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <line x1="7" y1="17" x2="17" y2="7" />
              <polyline points="7 7 17 7 17 17" />
            </svg>
          </span>}
      </div>
    </>;
  return href ? <a className={classes} href={href}>
      {inner}
    </a> : <div className={classes}>{inner}</div>;
};

export const FeatureTable = ({columns, rows, featuredRow, caption, variant}) => <div className={`gv-feature-table ${variant ? `gv-feature-table--${variant}` : ""}`}>
    <table>
      {caption && <caption className="gv-feature-table__caption">{caption}</caption>}
      <thead>
        <tr>
          {columns.map(col => <th key={col.key} scope="col">
              <span className="gv-feature-table__header">{col.label}</span>
            </th>)}
        </tr>
      </thead>
      <tbody>
        {rows.map((row, i) => {
  const rowClass = [i === featuredRow ? "gv-feature-table__row--featured" : "", row.chosen ? "gv-feature-table__row--chosen" : ""].filter(Boolean).join(" ");
  return <tr key={i} className={rowClass}>
              {columns.map(col => {
    const v = row[col.key];
    if (variant === "tradeoff" && Array.isArray(v)) {
      const pill = col.key === "pros" ? "gv-feature-table__pill--pro" : col.key === "cons" ? "gv-feature-table__pill--con" : "";
      return <td key={col.key}>
                      {v.map((item, j) => <span key={j} className={`gv-feature-table__pill ${pill}`}>{item}</span>)}
                    </td>;
    }
    return <td key={col.key}>{v}</td>;
  })}
            </tr>;
})}
      </tbody>
    </table>
  </div>;

export const Eyebrow = ({label, tone = "default"}) => <div className={`gv-eyebrow gv-eyebrow--${tone}`}>
    <span className="gv-eyebrow__bar" aria-hidden="true" />
    <span className="gv-eyebrow__label">{label}</span>
  </div>;

<Eyebrow label="WEBHOOKS" />

This page is the catalogue of every event family the platform emits over webhooks. Each entry lists what triggers the event, what shape the `data` payload carries, and the subscriptions it is useful for. The envelope (the wrapping `id`, `type`, `created_at`, `workspace`, `version`, `data`) is the same across all of them — see [Webhooks overview](/webhooks/overview) for the wrapper. This page only documents the `type` value and the contents of `data`.

## How event types are named

Every type is dotted: `<resource>.<verb>`. The resource is the thing the event is about. The verb is the state transition that fired the event. Subscribe to a specific type to receive only that one, or to a prefix (`pages.*`) to receive every event on the resource.

<FeatureTable
  columns={[
{ key: "resource", label: "Resource" },
{ key: "what", label: "What it covers" }
]}
  rows={[
{ resource: "`pages`", what: "Page document edits, draft saves, publishes, rollbacks" },
{ resource: "`forms`", what: "Submissions to a Form block on a published page" },
{ resource: "`deployments`", what: "Build lifecycle for page and theme publishes" },
{ resource: "`payments`", what: "End-user payment lifecycle via the collectPayment capability" },
{ resource: "`subscriptions`", what: "End-user subscription lifecycle on monetized workspaces" },
{ resource: "`agents`", what: "Agent run lifecycle — start, complete, fail, deny" },
{ resource: "`approvals`", what: "Approval gate lifecycle — pending, decided, expired" },
{ resource: "`workflows`", what: "Workflow run lifecycle, useful for cross-system orchestration" },
{ resource: "`domains`", what: "Custom domain verification and routing transitions" },
{ resource: "`secrets`", what: "Secret lifecycle — set, rotated, deleted (no values, only metadata)" },
{ resource: "`workspace`", what: "Workspace-level events — billing state changes, plan changes, lifecycle transitions" }
]}
/>

## `pages.*`

### pages.published

Fires when a page reaches `live` status after a successful build. Subscribe to this if you mirror page metadata into another system (a CMS index, a search service, a CDN cache invalidator).

<ExpectedOutput caption="data payload">
  ```json theme={null}
  {
    "page_id":      "pag_01H...",
    "slug":         "/pricing",
    "title":        "Pricing",
    "version":      42,
    "published_by": "usr_01H..."
  }
  ```
</ExpectedOutput>

### pages.rolled\_back

Fires when a page is rolled back to an earlier version. The payload names both versions so a downstream consumer can reconcile.

<ExpectedOutput caption="data payload">
  ```json theme={null}
  {
    "page_id":         "pag_01H...",
    "from_version":   42,
    "to_version":     41,
    "rolled_back_by": "usr_01H..."
  }
  ```
</ExpectedOutput>

## `forms.*`

### forms.submitted

Fires once per submission to a published Form block. The payload includes the field values exactly as the end user entered them — verify the signature before trusting any field as user identity.

<ExpectedOutput caption="data payload">
  ```json theme={null}
  {
    "form_id":       "frm_01H...",
    "submission_id": "sub_01H...",
    "fields":        { "name": "Alex", "email": "alex@example.com" },
    "submitter_ip":  "203.0.113.1",
    "submitted_at":  "2026-05-09T09:22:11Z"
  }
  ```
</ExpectedOutput>

`submitter_ip` is the originating client IP after trusted-proxy unwrapping. It is `null` when the form was submitted from a trusted internal source (an agent run, a Builder preview).

## `deployments.*`

### deployments.completed

Fires when a deployment reaches a terminal status — `live`, `failed`, or `cancelled`. One event per terminal transition.

<ExpectedOutput caption="data payload">
  ```json theme={null}
  {
    "deployment_id": "dep_01H...",
    "status":        "live",
    "kind":          "page_publish",
    "duration_ms":   1180
  }
  ```
</ExpectedOutput>

## `payments.*`

These fire on the end-user payment lifecycle that the [`collectPayment` capability](/capabilities/collect-payment) initiated. Use them to mark orders paid, grant access, or trigger fulfillment.

### payments.succeeded

The payment cleared. This is the event you wire fulfillment to.

<ExpectedOutput caption="data payload">
  ```json theme={null}
  {
    "payment_id":     "pay_01H...",
    "plan_id":        "plan_pro_monthly",
    "amount":         4900,
    "currency":       "USD",
    "customer_email": "alex@example.com",
    "checkout_session_id": "cs_..."
  }
  ```
</ExpectedOutput>

### payments.failed

The payment did not clear. `failure_code` is one of the standard provider codes (`card_declined`, `insufficient_funds`, `expired_card`, etc.).

<ExpectedOutput caption="data payload">
  ```json theme={null}
  {
    "payment_id":   "pay_01H...",
    "plan_id":      "plan_pro_monthly",
    "failure_code": "card_declined",
    "failure_message": "Your card was declined."
  }
  ```
</ExpectedOutput>

### payments.refunded

Issued on a refund — full or partial. `amount` is the refunded amount in the original currency's minor unit.

## `subscriptions.*`

Fires on end-user subscription state changes for monetized workspaces. The provider-driven lifecycle states are normalized to platform terms so a workspace subscribed to this stream does not need provider-specific glue.

### subscriptions.created

A new subscription record exists. Status is `active` or `trialing`.

### subscriptions.updated

The plan, price, or quantity changed. The payload carries `before` and `after` snapshots of the relevant fields.

### subscriptions.canceled

The subscription was cancelled. `cancel_at` distinguishes immediate cancellation from end-of-period cancellation.

## `agents.*`

Fires on the lifecycle of [agent runs](/concepts/agents). Use these if you want to mirror run history into a dashboard or alert when a run fails.

### agents.run\_started

A run was opened. Carries the trigger reason and the agent id.

### agents.run\_completed

A run reached `completed` status. The payload includes summary statistics — duration, steps executed, capabilities dispatched.

### agents.run\_failed

A run failed. The payload includes the failing step and an error envelope matching the [API error shape](/api-reference/errors).

## `approvals.*`

### approvals.pending

A run hit a gate and an approval record was created. Subscribe to this when you route reviews into a Slack or email channel.

<ExpectedOutput caption="data payload">
  ```json theme={null}
  {
    "approval_id": "apr_01H...",
    "agent_id":    "agt_01H...",
    "run_id":      "run_01H...",
    "action":      "send_email",
    "payload":     { "to": "customers@list", "subject": "Q4 promo" },
    "reason_set":  ["policy:rule_outbound_audience_size"]
  }
  ```
</ExpectedOutput>

### approvals.decided

An operator approved or denied a pending record. `decision` is `approved` or `denied`; `reviewer` is the principal that decided.

### approvals.expired

A pending record reached the workspace's approval TTL without a decision. The parent run was stopped.

## `workflows.*`

Mirror events to `agents.*` but for workflow runs. Useful when a workflow is fired by something other than an agent — a direct API call, a block binding — and you need lifecycle visibility into the run without polling.

* `workflows.run_started`
* `workflows.run_completed`
* `workflows.run_failed`

## `domains.*`

### domains.verified

A custom domain transitioned to `verified` after a successful DNS check.

### domains.failed

A verification attempt found the DNS record missing or incorrect. The payload includes the expected and observed values.

### domains.detached

A previously-verified domain was detached from the workspace.

## `secrets.*`

These carry **metadata only**. No event ever includes a secret value, the encrypted blob, or anything derivable from the value.

### secrets.set

A value was written (created or overwritten). Payload: `name`, `purpose`, `set_by`, `updated_at`.

### secrets.rotated

A rotation completed. Payload: `name`, `rotated_by`, `rotation_trigger` (one of `scheduled`, `manual`, `upstream_forced`).

### secrets.deleted

A secret was deleted. Payload: `name`, `deleted_by`.

## `workspace.*`

### workspace.billing\_state\_changed

A workspace transitioned between billing states — `active` → `past_due`, `past_due` → `canceled`, `canceled` → `active`, and so on. Subscribe to this if you mirror customer state into a CRM.

### workspace.plan\_changed

The workspace plan was swapped. Payload includes `from_plan_id` and `to_plan_id`.

### workspace.locked

The workspace was placed on a compliance or billing hold. Capability dispatch is paused until the hold is resolved.

## Subscribing to a subset

Webhook subscriptions take a list of event types. Use either a fully-qualified `pages.published` or a prefix `pages.*` to receive everything on a resource. There is no global `*` — you list the families you care about.

## Versioning

The envelope `version` field starts at `1`. New event families ship without bumping the version. The version increments only when the envelope shape itself changes; the platform commits to delivering both shapes during a deprecation window when that happens.

Inside a `data` payload, fields are additive. New optional fields may appear on existing event types without notice. Existing fields are not renamed or repurposed; if a payload needs a breaking change, a new event type ships alongside the old one and the old one is deprecated.

## Related

<Columns cols={2}>
  <BrandCard eyebrow="WEBHOOKS" title="Webhooks overview" href="/webhooks/overview">
    The envelope, the signature headers, and the at-least-once delivery model that wraps every event listed here.
  </BrandCard>

  <BrandCard eyebrow="WEBHOOKS" title="Verify webhook signatures" href="/webhooks/verification">
    Validate that an event really came from gavAI before reading anything in `data`.
  </BrandCard>

  <BrandCard eyebrow="WEBHOOKS" title="Idempotency for receivers" href="/webhooks/idempotency">
    Why the same event id can arrive twice and how to handle it.
  </BrandCard>

  <BrandCard eyebrow="GUIDE" title="Handle webhooks end-to-end" href="/guides/handle-webhooks">
    Register a subscription, verify, dedupe, and test against a real event.
  </BrandCard>
</Columns>
