From 5f245b0c9f490f70d2eb1b94da9343ab2b65c575 Mon Sep 17 00:00:00 2001 From: Devin Michael Date: Mon, 27 Apr 2026 12:47:15 +0700 Subject: [PATCH] docs: add webhook fan-out reference + metadata correlation guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two improvements to help external developers self-serve on common integration questions: 1. webhooks/index.mdx — new "Event Trigger Sources" section listing the models whose post_save fires each *.updated event (so subscribers can pick the leanest event for their use case), plus an "Identifying Rebill Orders" section covering the order subscriptions block and the transaction.subscription.billing_cycle counter. 2. admin-api/guides/metadata-and-correlation.mdx — new guide enumerating the 9 metadata-supported object types, with a callout that subscriptions/carts aren't directly metadata-managed. Walks through the canonical attribution.metadata pattern for cross-rebill external ID correlation (set once on subscription attribution, flows through subscription/order/transaction webhooks via FK inheritance with no follow-up API call). Includes a worked plan_id example and a "When to Use Which Surface" decision table. Surfaced via support inquiry from a merchant building integration to an external plan-management service. The fan-out + correlation patterns were institutional knowledge that took multiple rounds to produce authoritatively; this PR puts both into the public docs so future developers get the answer on first read. Co-Authored-By: Claude Opus 4.7 --- content/docs/admin-api/guides/meta.json | 1 + .../guides/metadata-and-correlation.mdx | 132 ++++++++++++++++++ content/docs/webhooks/index.mdx | 53 +++++++ 3 files changed, 186 insertions(+) create mode 100644 content/docs/admin-api/guides/metadata-and-correlation.mdx diff --git a/content/docs/admin-api/guides/meta.json b/content/docs/admin-api/guides/meta.json index 08916472..e41de839 100644 --- a/content/docs/admin-api/guides/meta.json +++ b/content/docs/admin-api/guides/meta.json @@ -4,6 +4,7 @@ "external-checkout", "order-management", "subscription-management", + "metadata-and-correlation", "exports", "testing-guide", "iframe-payment-form", diff --git a/content/docs/admin-api/guides/metadata-and-correlation.mdx b/content/docs/admin-api/guides/metadata-and-correlation.mdx new file mode 100644 index 00000000..12dd8f92 --- /dev/null +++ b/content/docs/admin-api/guides/metadata-and-correlation.mdx @@ -0,0 +1,132 @@ +--- +title: Metadata & External Correlation +sidebar_label: Metadata & External Correlation +sidebar_position: 2 +tags: + - Guide +--- +import { Callout } from 'fumadocs-ui/components/callout'; + +Use metadata to attach custom fields to commerce objects and to correlate events with external systems (CRMs, fulfillment partners, affiliate networks, healthcare platforms, plan-management services, etc.). This guide covers which objects support custom metadata, how to read it back through the API and webhooks, and the canonical pattern for correlating an external identifier across a customer's full subscription lifecycle. + +## Supported Objects + +The Metadata API lets you define custom fields ("definitions") on a fixed set of object types. The supported types are: + +- `attribution` — Marketing attribution +- `customer` — Customers +- `dispute` — Disputes +- `line` — Order line items +- `order` — Orders +- `product` — Products +- `variant` — Product variants +- `blog` — Blog posts +- `page` — CMS pages + +Define fields via [metadataCreate](/docs/admin-api/reference/metadata/metadataCreate). Each definition specifies the `object` type (from the list above), a `key`, a `name`, and a data type. Once defined, the field appears on the corresponding API responses and webhook payloads under that object's `metadata` block. + + +**Subscriptions, carts, and several other resources are not directly metadata-managed.** If you need to attach a custom field to a subscription (for example, an external `plan_id` that should travel with every rebill), use **`attribution.metadata`** instead — see [Cross-Rebill Correlation via Attribution](#cross-rebill-correlation-via-attribution) below. + + +## Setting and Reading Metadata + +After defining a field on a supported object type, set values on individual records via the resource's standard update endpoint. For example, to set order metadata: + +```json title="Set Order Metadata" http-method="PATCH" http-target="https://{store}.29next.store/api/admin/orders/{number}/" +{ + "metadata": { + "external_order_id": "ext_abc123", + "fulfillment_priority": "rush" + } +} +``` + +The values come back in: +- The corresponding GET endpoint (`GET /orders/{number}/`) +- The relevant `*.created` and `*.updated` webhooks for that object + +Field validation (data type, allowed values) is enforced based on the definitions you registered. + +## Cross-Rebill Correlation via Attribution + +A common integration pattern is: **store an external system's identifier on a customer's subscription once, and receive it inline on every charge event for the lifetime of that subscription** — without follow-up API calls. + +For example: a merchant integrating with an external plan-management service (subscription plans, healthcare plans, affiliate plans, etc.) wants the `plan_id` to travel with every rebill order and every capture transaction so their fulfillment service can correlate back to the external system. + +The pattern: store the identifier on the **subscription's marketing attribution metadata**. Every renewal order created by that subscription inherits the same attribution record, so `attribution.metadata` flows through automatically. + +### Step 1 — Define an attribution metadata field + +Create a metadata definition with `object: attribution` to register the field shape: + +```json title="Define attribution metadata field" http-method="POST" http-target="https://{store}.29next.store/api/admin/metadata/" +{ + "object": "attribution", + "key": "plan_id", + "name": "Plan ID", + "type": "string" +} +``` + +### Step 2 — Set the value on the subscription's attribution + +Patch the subscription with attribution metadata: + +```json title="Set subscription attribution metadata" http-method="PATCH" http-target="https://{store}.29next.store/api/admin/subscriptions/{id}/" +{ + "attribution": { + "metadata": { + "plan_id": "plan_abc123" + } + } +} +``` + +### Step 3 — Receive it inline on every charge event + +The same `attribution.metadata` object is included in: + +- `subscription.updated` payloads +- `order.created` payloads — including every renewal order, because rebill order placement reuses the subscription's attribution record +- `transaction.created` payloads — for both initial captures and rebill captures + +```json title="Excerpt of a transaction.created payload" +{ + "event_type": "transaction.created", + "data": { + "type": "debit", + "amount": "29.99", + "subscription": { + "id": 12345, + "billing_cycle": 3 + }, + "attribution": { + "metadata": { + "plan_id": "plan_abc123" + }, + "utm_source": "...", + "utm_campaign": "..." + } + } +} +``` + +Set the value once when the subscription is created (or when the external system first issues an identifier), and your webhook receiver can read it back from `data.attribution.metadata` on every subsequent charge. + + +**Why attribution and not a different field?** `attribution.metadata` is the only metadata surface that (a) lives on the subscription, (b) is inherited by every renewal order automatically, and (c) is included inline on `transaction.created` payloads. Other approaches require either per-order writes (storing on `order.metadata` for each rebill) or a follow-up API call (storing on `customer.metadata` and fetching on every charge). + + +## When to Use Which Surface + +| Need | Use | +|------|-----| +| Custom field on a single order (e.g., custom routing, gift message) | `order.metadata` | +| Custom field on a customer record, viewable in customer webhooks | `customer.metadata` | +| External ID that should travel with every charge for a subscription | `attribution.metadata` on the subscription | +| Per-line-item custom fields (e.g., engraving text, dosage instructions) | `line.metadata` | +| Custom catalog data (e.g., supplier SKU, regulatory codes) | `product.metadata` or `variant.metadata` | +| Dispute case notes / external reference | `dispute.metadata` | + +For data that doesn't fit any of these — for example, cart-level data or session-level data — you'll need to handle it in your application layer; those resources are not part of the public Metadata API surface. diff --git a/content/docs/webhooks/index.mdx b/content/docs/webhooks/index.mdx index b0ecbbd4..153f8c50 100644 --- a/content/docs/webhooks/index.mdx +++ b/content/docs/webhooks/index.mdx @@ -71,6 +71,59 @@ Returning a `410` response code indicates the target resource is no longer avail | `ticket.created` | Triggers a new support ticket is created. | [View Example](/docs/webhooks/reference/support/ticket.created) | | `ticket.updated` | Triggers when an existing support ticket is updated. | [View Example](/docs/webhooks/reference/support/ticket.updated) | +### Event Trigger Sources + +Most `*.created` events fire once per resource creation, but `*.updated` events typically trigger on changes to **multiple related models** — not just the headline resource. Subscribing to a noisy `*.updated` event without understanding the fan-out can produce unexpected event volume. + +The table below lists every model whose change fires each event. Use this when picking the leanest event for your use case — for example, if you only care about payment captures, `transaction.created` filtered to `type: "debit"` fires once per capture, whereas `order.updated` fires on any line, transaction, fulfillment, address, or tag change. + +| Event | Fires when these models change | +| ----- | ------------------------------ | +| `order.created` | `Order` (on creation) | +| `order.updated` | `Order`, `Line`, `Transaction`, `Refund`, `MarketingAttribution`, `ShippingAddress`, `BillingAddress`, `Fulfillment`, plus order tag changes | +| `transaction.created` | `Transaction` (on creation) | +| `transaction.updated` | `Transaction` (on update) | +| `subscription.created` | `Subscription` (on creation) | +| `subscription.updated` | `Subscription`, `SubscriptionLine`, `MarketingAttribution`, `SubscriptionRebillInfo`, `UserAddress` | +| `customer.created` | `User` (on creation) | +| `customer.updated` | `User`, `UserAddress`, plus user tag changes | +| `fulfillment.created` | `Fulfillment` (on creation) | +| `fulfillment.updated` | `Fulfillment`, `FulfillmentLine`, `Tracking` | +| `dispute.created` | `Dispute` (on creation) | +| `dispute.updated` | `Dispute`, related `Transaction` | +| `ticket.created` | `Ticket` (on creation) | +| `ticket.updated` | `Ticket`, `TicketComment`, plus ticket tag changes | +| `product.created` | `Product` (on creation) | +| `product.updated` | `Product`, `ProductImage`, `ProductAttribute`, `Category`, `ProductCategory`, `ProductPrice`, `StockRecord`, `AttributeOption*`, `Option` | +| `gateway.created` / `gateway.updated` | `PaymentGateway`, `PaymentGatewayGroup` | +| `gateway_group.created` / `gateway_group.updated` | `GatewayGroup`, `PaymentGateway` | +| `store.updated` | `SpreedlyConfig`, `AdditionalLanguage`, `Currency`, `Domain`, plus store config changes | + + +**Picking the right event for fulfillment-on-charge.** A common pattern is "do something when a customer is charged, including on subscription rebills." The leanest signal is `transaction.created` filtered to `data.type == "debit"` — this fires once per capture and avoids the broader fan-out of `order.updated`. + + +### Identifying Rebill Orders + +Renewal orders fire `order.created` like any other order. To distinguish a rebill from a new-customer order, check the order payload's `subscriptions` block (and the `subscription_lifecycle` field on the order) — both are populated only on renewal orders. + +For payment events, `transaction.created` payloads include a `subscription` block with the subscription `id` and the `billing_cycle` counter: + +```json title="Subscription block on transaction.created" +"subscription": { + "id": 12345, + "billing_cycle": 2 +} +``` + +`billing_cycle` is the integer cycle number for that subscription's charge: +- `0` — initial purchase +- `1` — first rebill +- `2` — second rebill +- ...and so on + +This is the canonical way to tell, from a charge event alone, which rebill cycle the customer is on. + ### Webhook Data Structure Webhook payloads follow the same structure as Admin API data serializers, which makes them predictable. In general, the data in a webhook payload matches the data you would get by retrieving the same resource through the API. You can set up test webhooks and view the webhook logs in the dashboard to help build and verify your receiver.