Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions content/docs/admin-api/guides/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"external-checkout",
"order-management",
"subscription-management",
"metadata-and-correlation",
"exports",
"testing-guide",
"iframe-payment-form",
Expand Down
132 changes: 132 additions & 0 deletions content/docs/admin-api/guides/metadata-and-correlation.mdx
Original file line number Diff line number Diff line change
@@ -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.

<Callout type="warn">
**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.
</Callout>

## 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.

<Callout type="info">
**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).
</Callout>

## 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.
53 changes: 53 additions & 0 deletions content/docs/webhooks/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

<Callout type="info">
**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`.
</Callout>

### 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.
Expand Down