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",
"subscription-charge-webhooks",
"exports",
"testing-guide",
"iframe-payment-form",
Expand Down
118 changes: 118 additions & 0 deletions content/docs/admin-api/guides/subscription-charge-webhooks.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: Handle Subscription Charge Webhooks
sidebar_label: Subscription Charge Webhooks
sidebar_position: 2
tags:
- Guide
---
import { Callout } from 'fumadocs-ui/components/callout';

Use `transaction.created` webhooks to react to successful subscription charges, distinguish an initial charge from a renewal, and correlate the charge with a record in an external system.

## Detect a Successful Charge

A `transaction.created` event represents a payment attempt, not necessarily a successful payment. Process the event as a charge only when both of these conditions are true:

- `data.type` is `debit`
- `data.status` is `succeeded`

This is more precise than using `order.created`, which indicates that an order was placed, or `order.updated`, which can fire repeatedly as related order data changes.

```json title="Successful Subscription Charge"
{
"event_id": "f7eb1338-0934-4cda-8128-d6a77761a368",
"event_type": "transaction.created",
"data": {
"id": 10416,
"type": "debit",
"status": "succeeded",
"amount": "29.99",
"order": "116663",
"subscription": {
"id": 12345,
"billing_cycle": 3
},
"attribution": {
"metadata": {
"plan_id": "plan_abc123"
}
}
}
}
```

<Callout type="warn">
Webhook deliveries are retried when your endpoint does not return `200`. Store the `event_id` or transaction `data.id` before performing side effects so a repeated delivery does not trigger fulfillment or another external action twice.
</Callout>

## Identify Initial and Renewal Charges

For a subscription charge, `data.subscription` contains the subscription ID and its billing-cycle counter:

- `billing_cycle: 0` — initial subscription charge
- `billing_cycle: 1` — first renewal
- `billing_cycle: 2` — second renewal
- and so on

A value greater than `0` identifies a renewal. The subscription block is empty for transactions that are not associated with a subscription.

```javascript title="Filter and Classify a Subscription Charge"
function handleTransactionCreated(payload) {
const transaction = payload.data;

if (transaction.type !== "debit" || transaction.status !== "succeeded") {
return;
}

if (!transaction.subscription?.id) {
return;
}

const isRenewal = transaction.subscription.billing_cycle > 0;
const externalPlanId = transaction.attribution?.metadata?.plan_id;

// Deduplicate payload.event_id or transaction.id before side effects.
// Process the initial or renewal charge in your external system.
}
```

## Correlate Charges with an External System

If an external identifier should accompany every charge for a subscription, store it in the subscription's attribution metadata. Renewal orders and their transactions include that attribution in webhook payloads, so the receiver does not need a follow-up API request.

### 1. Define the Metadata Field

Create an attribution metadata definition once per store. The field type for a string value is `text`.

```json title="Define Attribution Metadata" http-method="POST" http-target="https://{store}.29next.store/api/admin/metadata/"
{
"object": "attribution",
"key": "plan_id",
"name": "Plan ID",
"type": "text"
}
```

See [metadataCreate](/docs/admin-api/reference/metadata/metadataCreate) for available field types and validation options.

### 2. Set the Identifier on the Subscription

Set the value when the subscription is created, or add it later with [subscriptionsPartialUpdate](/docs/admin-api/reference/subscriptions/subscriptionsPartialUpdate).

```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"
}
}
}
```

### 3. Read the Identifier from the Webhook

Read the value from `data.attribution.metadata.plan_id` after confirming that the transaction succeeded. This pattern also works for other external identifiers, such as a CRM subscription ID or fulfillment-plan reference.

<Callout type="info">
Use customer metadata for values that belong to the customer regardless of subscription. Use order metadata for values that apply to one order only. Attribution metadata is the recommended surface for an identifier that should accompany each charge for a specific subscription.
</Callout>
4 changes: 4 additions & 0 deletions content/docs/webhooks/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ 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) |

<Callout type="info" title="Handling subscription charges">
To react to a successful subscription charge, use `transaction.created` and check both the transaction type and status. See [Handle Subscription Charge Webhooks](/docs/admin-api/guides/subscription-charge-webhooks) for renewal detection, external-ID correlation, and retry-safe processing.
</Callout>

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