|
| 1 | +# Webhooks Documentation |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Webhook reference docs are generated alongside the Admin API docs. The pipeline takes REST API object schemas from the Admin API OpenAPI specs, wraps them in a webhook envelope structure, and generates per-event reference pages with example payloads. |
| 6 | + |
| 7 | +The hand-written webhook overview page at `content/docs/webhooks/index.mdx` is maintained separately and is NOT generated — it contains the events table, payload structure docs, verification guide, and versioning info. |
| 8 | + |
| 9 | +## Generation Pipeline |
| 10 | + |
| 11 | +### Step 1: Python — Schema Generation (`tools/webhooks.py`) |
| 12 | + |
| 13 | +Called by `tools/update_api_docs.py` during spec download. For each webhook event defined in `tools/config.py`: |
| 14 | + |
| 15 | +1. **Resolves the schema** — Looks up the `schema_ref` (e.g. `#/components/schemas/Order`) from the Admin API spec |
| 16 | +2. **Cleans the schema** — Removes `readOnly` flags, recursively resolves `$ref`, `allOf`, `oneOf` references (depth limit: 4) |
| 17 | +3. **Generates example payload** — Builds realistic JSON example with format-aware defaults (UUIDs, dates, emails, etc.) |
| 18 | +4. **Wraps in envelope** — Creates the full webhook payload structure: |
| 19 | + |
| 20 | +```json |
| 21 | +{ |
| 22 | + "api_version": "2024-04-01", |
| 23 | + "object": "order", |
| 24 | + "data": { ... resolved object schema ... }, |
| 25 | + "event_id": "a7a26ff2-...", |
| 26 | + "event_type": "order.created", |
| 27 | + "webhook": { "id": 1, "store": "example", "events": ["order.created"], "target": "https://example.com/webhook/" } |
| 28 | +} |
| 29 | +``` |
| 30 | + |
| 31 | +5. **Injects into spec** — Adds the webhook to the OpenAPI spec's `webhooks` section, which fumadocs-openapi then renders |
| 32 | + |
| 33 | +### Step 2: Node.js — MDX Generation (`scripts/generate-api-docs.mjs`) |
| 34 | + |
| 35 | +1. **`fixWebhookSchemas()`** — Normalizes any malformed schemas from Python output (adds missing `type: object`, fixes array-style types, generates missing examples) |
| 36 | +2. **`webhookBeforeWrite()`** — Filters `generateFiles()` output to keep ONLY webhook files (dot-notation filenames like `cart.abandoned.mdx`) |
| 37 | +3. **Generates per-event MDX pages** grouped by tag into `content/docs/webhooks/reference/{tag}/` |
| 38 | +4. **Writes `meta.json`** with tag ordering from the spec |
| 39 | + |
| 40 | +## Webhook Event Registry |
| 41 | + |
| 42 | +All 22 events are defined in `tools/config.py` `WEBHOOKS` list. Each entry: |
| 43 | + |
| 44 | +```python |
| 45 | +{ |
| 46 | + "event": "order.created", # Dot-notation event name |
| 47 | + "object": "order", # Object type label |
| 48 | + "schema_ref": "#/components/schemas/Order", # Admin API schema to resolve |
| 49 | + "tag": "orders", # Sidebar grouping |
| 50 | + "description": "Triggers when a new order is created.", |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +**Events by tag:** |
| 55 | +- **apps**: `app.uninstalled` |
| 56 | +- **carts**: `cart.abandoned` |
| 57 | +- **customers**: `customer.created`, `customer.updated` |
| 58 | +- **fulfillment**: `fulfillment.created`, `fulfillment.updated` |
| 59 | +- **orders**: `order.created`, `order.updated` |
| 60 | +- **payments**: `dispute.created`, `dispute.updated`, `gateway.created`, `gateway.updated`, `transaction.created`, `transaction.updated` |
| 61 | +- **products**: `product.created`, `product.updated`, `product.deleted` |
| 62 | +- **subscriptions**: `subscription.created`, `subscription.updated` |
| 63 | +- **store**: `store.updated` |
| 64 | +- **support**: `ticket.created`, `ticket.updated` |
| 65 | + |
| 66 | +### Custom Payloads |
| 67 | + |
| 68 | +Some events don't follow their object's schema. These are defined in `CUSTOM_WEBHOOK_EVENT_PAYLOADS`: |
| 69 | + |
| 70 | +- `product.deleted` — Only includes `{ id: integer }` (no full product data since the product is deleted) |
| 71 | + |
| 72 | +Set `schema_ref: None` in the webhook config and add a custom payload entry. |
| 73 | + |
| 74 | +## Output Structure |
| 75 | + |
| 76 | +``` |
| 77 | +content/docs/webhooks/ |
| 78 | +├── index.mdx ← Hand-written overview (NOT generated) |
| 79 | +├── meta.json ← { root: true, pages: ["index", "reference"] } |
| 80 | +└── reference/ |
| 81 | + ├── apps/ |
| 82 | + │ └── app.uninstalled.mdx |
| 83 | + ├── carts/ |
| 84 | + │ └── cart.abandoned.mdx |
| 85 | + ├── orders/ |
| 86 | + │ ├── order.created.mdx |
| 87 | + │ └── order.updated.mdx |
| 88 | + ├── payments/ |
| 89 | + │ ├── dispute.created.mdx |
| 90 | + │ ├── gateway.created.mdx |
| 91 | + │ └── transaction.created.mdx |
| 92 | + ├── 2023-02-10/ ← Legacy version (root: true) |
| 93 | + ├── unstable/ ← Dev version (root: true) |
| 94 | + └── meta.json |
| 95 | +``` |
| 96 | + |
| 97 | +## Generated MDX Format |
| 98 | + |
| 99 | +```mdx |
| 100 | +--- |
| 101 | +title: order.created |
| 102 | +description: Triggers when a new order is created. |
| 103 | +full: true |
| 104 | +_openapi: |
| 105 | + method: POST |
| 106 | + webhook: true |
| 107 | +--- |
| 108 | +<APIPage document={"public/api/admin/2024-04-01.yaml"} webhooks={[{"name":"order.created","method":"post"}]} /> |
| 109 | +``` |
| 110 | + |
| 111 | +The `APIPage` component renders a two-column layout: webhook payload schema on the left, example JSON payload on the right. |
| 112 | + |
| 113 | +## Key Conventions |
| 114 | + |
| 115 | +- Webhook filenames use **dot notation** (`order.created.mdx`) vs REST endpoints which use camelCase (`ordersCreate.mdx`) — this is how `isWebhookFile()` distinguishes them during generation |
| 116 | +- The webhook overview page (`content/docs/webhooks/index.mdx`) is hand-maintained and includes the events table, payload structure, verification code, and versioning docs — do not overwrite it |
| 117 | +- Webhook payload `data` schemas mirror the Admin API's object schemas (serializers), so webhook data matches what you'd get from a GET request on the corresponding REST endpoint |
| 118 | +- Webhook response codes: `200` = success, `410` = auto-disable the webhook |
| 119 | +- Verification: `X-29Next-Signature` header contains HMAC-SHA256 of the payload signed with the webhook signing secret |
| 120 | +- Retry policy: up to 10 retries over several days with exponential backoff; failing webhooks trigger admin email notifications and eventual deactivation |
| 121 | + |
| 122 | +## Adding a New Webhook Event |
| 123 | + |
| 124 | +1. Add the event to `tools/config.py` `WEBHOOKS` list with: event name, object type, schema_ref, tag, description |
| 125 | +2. If the event has a non-standard payload, add it to `CUSTOM_WEBHOOK_EVENT_PAYLOADS` and set `schema_ref: None` |
| 126 | +3. Run `cd tools && python update_api_docs.py` to regenerate the OpenAPI specs with the new webhook |
| 127 | +4. Run `npm run generate-api-docs` to generate the webhook MDX page |
| 128 | +5. Update the events table in `content/docs/webhooks/index.mdx` with the new event row |
| 129 | + |
| 130 | +## Versioning |
| 131 | + |
| 132 | +Webhook versions follow Admin API versions. Each version gets its own subdirectory with `root: true`: |
| 133 | +- Stable (2024-04-01) → `content/docs/webhooks/reference/` (base directory) |
| 134 | +- Legacy (2023-02-10) → `content/docs/webhooks/reference/2023-02-10/` |
| 135 | +- Unstable → `content/docs/webhooks/reference/unstable/` |
| 136 | + |
| 137 | +The `api_version` field in webhook payloads matches the Admin API version the webhook was configured with. |
0 commit comments