> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/Shopify/subscriptions-reference-app/llms.txt
> Use this file to discover all available pages before exploring further.

# Billing Attempt Webhooks

> Webhooks triggered by subscription billing attempts and billing cycle events

## Overview

Billing attempt webhooks notify your app when subscription billing attempts succeed or fail, and when billing cycles are skipped. These webhooks are essential for implementing dunning management, payment retry logic, and customer notifications.

## Webhook Topics

### subscription\_billing\_attempts/success

Triggered when a billing attempt for a subscription contract succeeds and an order is created.

<Note>
  This webhook fires after Shopify successfully charges the customer's payment method and creates a new order for the subscription.
</Note>

#### Webhook Configuration

```toml theme={null}
[[webhooks.subscriptions]]
topics = [ "subscription_billing_attempts/success" ]
uri = "/webhooks/subscription_billing_attempts/success"
```

#### Payload Structure

<ParamField path="id" type="number" required>
  The numeric ID of the billing attempt
</ParamField>

<ParamField path="admin_graphql_api_id" type="string" required>
  The GraphQL Admin API ID of the billing attempt (format: `gid://shopify/SubscriptionBillingAttempt/{id}`)
</ParamField>

<ParamField path="idempotency_key" type="string" required>
  A unique key that can be used to identify this billing attempt across retries
</ParamField>

<ParamField path="order_id" type="number">
  The numeric ID of the order created by this billing attempt (may be null initially)
</ParamField>

<ParamField path="admin_graphql_api_order_id" type="string">
  The GraphQL Admin API ID of the order created (may be null initially)
</ParamField>

<ParamField path="subscription_contract_id" type="number" required>
  The numeric ID of the subscription contract being billed
</ParamField>

<ParamField path="admin_graphql_api_subscription_contract_id" type="string" required>
  The GraphQL Admin API ID of the subscription contract
</ParamField>

<ParamField path="ready" type="boolean" required>
  Indicates whether the billing attempt is ready for processing
</ParamField>

<ParamField path="error_message" type="string">
  Error message if there were any issues (null for successful attempts)
</ParamField>

<ParamField path="error_code" type="string">
  Error code if there were any issues (null for successful attempts)
</ParamField>

#### Example Payload

```json theme={null}
{
  "id": 987654321,
  "admin_graphql_api_id": "gid://shopify/SubscriptionBillingAttempt/987654321",
  "idempotency_key": "abc123def456",
  "order_id": 555555555,
  "admin_graphql_api_order_id": "gid://shopify/Order/555555555",
  "subscription_contract_id": 123456789,
  "admin_graphql_api_subscription_contract_id": "gid://shopify/SubscriptionContract/123456789",
  "ready": true,
  "error_message": null,
  "error_code": null
}
```

#### Handler Implementation

The reference app stops any active dunning processes and tags the recurring order:

```typescript theme={null}
// app/routes/webhooks.subscription_billing_attempts.success.tsx
import type {ActionFunctionArgs} from '@remix-run/node';
import {DunningStopJob, jobs, TagSubscriptionOrderJob} from '~/jobs';
import {RECURRING_ORDER_TAGS} from '~/jobs/tags/constants';
import {authenticate} from '~/shopify.server';
import {logger} from '~/utils/logger.server';

export const action = async ({request}: ActionFunctionArgs) => {
  const {topic, shop, payload} = await authenticate.webhook(request);

  logger.info({topic, shop, payload}, 'Received webhook');

  // Stop dunning process for this subscription
  jobs.enqueue(
    new DunningStopJob({
      shop,
      payload: payload,
    }),
  );

  // Tag the recurring order
  jobs.enqueue(
    new TagSubscriptionOrderJob({
      shop,
      payload: {
        orderId: payload.admin_graphql_api_order_id,
        tags: ['subscription', 'recurring-order'],
      },
    }),
  );

  return new Response();
};
```

<Note>
  The handler immediately stops any dunning process (payment retry sequence) since the payment was successful, and tags the order for easy identification.
</Note>

***

### subscription\_billing\_attempts/failure

Triggered when a billing attempt for a subscription contract fails.

<Note>
  This webhook is critical for implementing dunning management - the process of retrying failed payments and notifying customers about payment issues.
</Note>

#### Webhook Configuration

```toml theme={null}
[[webhooks.subscriptions]]
topics = [ "subscription_billing_attempts/failure" ]
uri = "/webhooks/subscription_billing_attempts/failure"
```

#### Payload Structure

<ParamField path="id" type="number" required>
  The numeric ID of the billing attempt
</ParamField>

<ParamField path="admin_graphql_api_id" type="string" required>
  The GraphQL Admin API ID of the billing attempt
</ParamField>

<ParamField path="idempotency_key" type="string" required>
  A unique key for this billing attempt
</ParamField>

<ParamField path="order_id" type="number">
  The order ID (null for failed attempts)
</ParamField>

<ParamField path="admin_graphql_api_order_id" type="string">
  The GraphQL Admin API order ID (null for failed attempts)
</ParamField>

<ParamField path="subscription_contract_id" type="number" required>
  The numeric ID of the subscription contract
</ParamField>

<ParamField path="admin_graphql_api_subscription_contract_id" type="string" required>
  The GraphQL Admin API ID of the subscription contract
</ParamField>

<ParamField path="ready" type="boolean" required>
  Indicates whether the billing attempt is ready
</ParamField>

<ParamField path="error_message" type="string" required>
  A description of why the billing attempt failed
</ParamField>

<ParamField path="error_code" type="string" required>
  A code identifying the type of error. Common codes include:

  * `insufficient_inventory` - Not enough product inventory
  * `inventory_allocations_not_found` - Inventory allocation issues
  * `payment_method_declined` - Payment method was declined
  * `authentication_required` - Payment requires authentication (3D Secure)
</ParamField>

#### Example Payload

```json theme={null}
{
  "id": 987654321,
  "admin_graphql_api_id": "gid://shopify/SubscriptionBillingAttempt/987654321",
  "idempotency_key": "xyz789abc123",
  "order_id": null,
  "admin_graphql_api_order_id": null,
  "subscription_contract_id": 123456789,
  "admin_graphql_api_subscription_contract_id": "gid://shopify/SubscriptionContract/123456789",
  "ready": true,
  "error_message": "Payment method declined",
  "error_code": "payment_method_declined"
}
```

#### Handler Implementation

The reference app starts a dunning process to handle the failed payment:

```typescript theme={null}
// app/routes/webhooks.subscription_billing_attempts.failure.tsx
import type {ActionFunctionArgs} from '@remix-run/node';
import {DunningStartJob, jobs} from '~/jobs';
import {authenticate} from '~/shopify.server';
import {logger} from '~/utils/logger.server';

export const action = async ({request}: ActionFunctionArgs) => {
  const {topic, shop, payload} = await authenticate.webhook(request);

  logger.info({topic, shop, payload}, 'Received webhook');

  // Start dunning process to retry payment
  jobs.enqueue(
    new DunningStartJob({
      shop,
      payload: payload,
    }),
  );

  return new Response();
};
```

<Note>
  The dunning process typically includes:

  * Sending customer notifications about the failed payment
  * Attempting to retry the payment on a schedule
  * Pausing or cancelling the subscription after multiple failures
</Note>

***

### subscription\_billing\_cycles/skip

Triggered when a billing cycle is skipped for a subscription contract.

<Note>
  Customers can skip upcoming deliveries, which triggers this webhook. This is useful for notifying customers about the skipped cycle.
</Note>

#### Webhook Configuration

```toml theme={null}
[[webhooks.subscriptions]]
topics = [ "subscription_billing_cycles/skip" ]
uri = "/webhooks/subscription_billing_cycles/skip"
```

#### Payload Structure

<ParamField path="subscription_contract_id" type="string" required>
  The numeric ID of the subscription contract (as a string)
</ParamField>

<ParamField path="cycle_start_at" type="string" required>
  ISO 8601 timestamp of when the cycle starts
</ParamField>

<ParamField path="cycle_end_at" type="string" required>
  ISO 8601 timestamp of when the cycle ends
</ParamField>

<ParamField path="cycle_index" type="number" required>
  The index number of the billing cycle being skipped (0-based)
</ParamField>

<ParamField path="billing_attempt_expected_date" type="string" required>
  ISO 8601 timestamp of when the billing attempt was expected
</ParamField>

<ParamField path="skipped" type="boolean" required>
  Indicates that the cycle was skipped (will be true)
</ParamField>

<ParamField path="edited" type="boolean" required>
  Indicates whether the cycle was edited
</ParamField>

<ParamField path="contract_edit" type="object">
  Contract edit details (typically null for skip operations)
</ParamField>

#### Example Payload

```json theme={null}
{
  "subscription_contract_id": "123456789",
  "cycle_start_at": "2024-03-01T00:00:00Z",
  "cycle_end_at": "2024-03-31T23:59:59Z",
  "cycle_index": 3,
  "billing_attempt_expected_date": "2024-03-01T00:00:00Z",
  "skipped": true,
  "edited": false,
  "contract_edit": null
}
```

#### Handler Implementation

The reference app sends a notification email to the customer about the skipped cycle:

```typescript theme={null}
// app/routes/webhooks.subscription_billing_cycles.skip.tsx
import type {ActionFunctionArgs} from '@remix-run/node';
import {composeGid} from '@shopify/admin-graphql-api-utilities';
import {CustomerSendEmailJob, jobs} from '~/jobs';
import {CustomerEmailTemplateName} from '~/services/CustomerSendEmailService';
import {authenticate} from '~/shopify.server';
import {logger} from '~/utils/logger.server';

export const action = async ({request}: ActionFunctionArgs) => {
  const {topic, shop, payload} = await authenticate.webhook(request);

  logger.info({topic, shop, payload}, 'Received webhook');

  const {subscription_contract_id: subContractId, cycle_index} = payload;

  // Convert numeric ID to GraphQL GID format
  const subscriptionContractGid = composeGid(
    'SubscriptionContract',
    subContractId,
  );

  // Send skip confirmation email
  jobs.enqueue(new CustomerSendEmailJob({
    shop,
    payload: {
      admin_graphql_api_id: subscriptionContractGid,
      emailTemplate: CustomerEmailTemplateName.SubscriptionSkipped,
      cycle_index,
    },
  }));

  return new Response();
};
```

<Note>
  The handler converts the numeric subscription contract ID to a GraphQL GID format using the `composeGid` utility from `@shopify/admin-graphql-api-utilities`.
</Note>

## Error Handling

When handling billing attempt failures, you should implement logic based on the error code:

| Error Code                        | Description                     | Recommended Action                       |
| --------------------------------- | ------------------------------- | ---------------------------------------- |
| `payment_method_declined`         | Payment method was declined     | Notify customer to update payment method |
| `authentication_required`         | 3D Secure authentication needed | Send customer link to authenticate       |
| `insufficient_inventory`          | Not enough product stock        | Notify merchant, pause subscription      |
| `inventory_allocations_not_found` | Inventory system issue          | Retry later, notify merchant             |

## Best Practices

1. **Dunning Management**: Implement a progressive dunning strategy that retries payments over several days before cancelling
2. **Customer Communication**: Always notify customers about failed payments and provide clear instructions for updating payment methods
3. **Inventory Errors**: Handle inventory-related failures differently from payment failures
4. **Monitoring**: Track billing attempt success/failure rates to identify issues early
5. **Idempotency**: Use the `idempotency_key` to prevent duplicate processing of the same billing attempt
