Skip to main content

Overview

Subscription contract webhooks are fired when subscription contracts are created or their status changes. These webhooks allow you to track subscription lifecycles and trigger automated workflows.

Webhook Topics

subscription_contracts/create

Triggered when a new subscription contract is created in Shopify.
This webhook is typically fired after a customer completes checkout with a subscription product or when a subscription contract is manually created.

Webhook Configuration

[[webhooks.subscriptions]]
topics = [ "subscription_contracts/create" ]
uri = "/webhooks/subscription_contracts/create"

Payload Structure

id
number
required
The numeric ID of the subscription contract
admin_graphql_api_id
string
required
The GraphQL Admin API ID of the subscription contract (format: gid://shopify/SubscriptionContract/{id})
admin_graphql_api_customer_id
string
required
The GraphQL Admin API ID of the customer associated with this subscription
admin_graphql_api_origin_order_id
string
The GraphQL Admin API ID of the originating order (null if not created from checkout)
customer_id
number
required
The numeric ID of the customer
origin_order_id
number
The numeric ID of the originating order (null if not created from checkout)
status
string
required
The current status of the subscription contract. One of: active, cancelled, expired, failed, paused, stale
currency_code
string
required
The currency code for the subscription (e.g., “USD”, “EUR”)
revision_id
number
required
The revision number of the subscription contract
billing_policy
object
required
The billing policy for the subscription
interval
string
The billing interval unit (e.g., “day”, “week”, “month”, “year”)
interval_count
number
The number of intervals between billings
min_cycles
number
Minimum number of billing cycles (null if no minimum)
max_cycles
number
Maximum number of billing cycles (null if unlimited)
delivery_policy
object
required
The delivery policy for the subscription
interval
string
The delivery interval unit
interval_count
number
The number of intervals between deliveries

Example Payload

{
  "id": 123456789,
  "admin_graphql_api_id": "gid://shopify/SubscriptionContract/123456789",
  "admin_graphql_api_customer_id": "gid://shopify/Customer/987654321",
  "admin_graphql_api_origin_order_id": "gid://shopify/Order/555555555",
  "customer_id": 987654321,
  "origin_order_id": 555555555,
  "status": "active",
  "currency_code": "USD",
  "revision_id": 1,
  "billing_policy": {
    "interval": "month",
    "interval_count": 1,
    "min_cycles": null,
    "max_cycles": null
  },
  "delivery_policy": {
    "interval": "month",
    "interval_count": 1
  }
}

Handler Implementation

The reference app implementation sends a welcome email to the customer and tags the origin order:
// app/routes/webhooks.subscription_contracts.create.tsx
import type {ActionFunctionArgs} from '@remix-run/node';
import {authenticate} from '~/shopify.server';
import {logger} from '~/utils/logger.server';
import {CustomerSendEmailJob, jobs, TagSubscriptionOrderJob} from '~/jobs';
import {CustomerEmailTemplateName} from '~/services/CustomerSendEmailService';

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

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

  const {admin_graphql_api_origin_order_id: orderId} = payload;
  
  // Send welcome email if order was created from checkout
  if (orderId !== null) {
    jobs.enqueue(new CustomerSendEmailJob({
      shop,
      payload: {
        ...payload,
        emailTemplate: CustomerEmailTemplateName.NewSubscription,
      },
    }));
  }

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

  return new Response();
};
The handler authenticates the webhook using Shopify’s built-in webhook verification, then enqueues background jobs for email sending and order tagging.

subscription_contracts/activate

Triggered when a subscription contract is activated (e.g., resuming from a paused state).

Webhook Configuration

[[webhooks.subscriptions]]
topics = [ "subscription_contracts/activate" ]
uri = "/webhooks/subscription_contracts/activate"

Payload Structure

The payload structure is the same as subscription_contracts/cancel below, containing the status change information.

Handler Implementation

// app/routes/webhooks.subscription_contracts.activate.tsx
import type {ActionFunctionArgs} from '@remix-run/node';
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');

  return new Response();
};

subscription_contracts/cancel

Triggered when a subscription contract is cancelled by the customer or merchant.

Webhook Configuration

[[webhooks.subscriptions]]
topics = [ "subscription_contracts/cancel" ]
uri = "/webhooks/subscription_contracts/cancel"

Payload Structure

id
number
required
The numeric ID of the subscription contract
admin_graphql_api_id
string
required
The GraphQL Admin API ID of the subscription contract
admin_graphql_api_customer_id
string
required
The GraphQL Admin API ID of the customer
admin_graphql_api_origin_order_id
string
required
The GraphQL Admin API ID of the originating order
customer_id
number
required
The numeric ID of the customer
origin_order_id
number
required
The numeric ID of the originating order
status
string
required
The new status of the subscription contract (will be cancelled)
currency_code
string
required
The currency code for the subscription
revision_id
number
required
The revision number after the status change
billing_policy
object
required
The billing policy for the subscription (same structure as create webhook)
delivery_policy
object
required
The delivery policy for the subscription (same structure as create webhook)

Example Payload

{
  "id": 123456789,
  "admin_graphql_api_id": "gid://shopify/SubscriptionContract/123456789",
  "admin_graphql_api_customer_id": "gid://shopify/Customer/987654321",
  "admin_graphql_api_origin_order_id": "gid://shopify/Order/555555555",
  "customer_id": 987654321,
  "origin_order_id": 555555555,
  "status": "cancelled",
  "currency_code": "USD",
  "revision_id": 2,
  "billing_policy": {
    "interval": "month",
    "interval_count": 1,
    "min_cycles": null,
    "max_cycles": null
  },
  "delivery_policy": {
    "interval": "month",
    "interval_count": 1
  }
}

Handler Implementation

The reference app sends cancellation confirmation emails to both the customer and merchant:
// app/routes/webhooks.subscription_contracts.cancel.tsx
import type {ActionFunctionArgs} from '@remix-run/node';
import {authenticate} from '~/shopify.server';
import {logger} from '~/utils/logger.server';
import {CustomerSendEmailJob, jobs, MerchantSendEmailJob} from '~/jobs';
import {CustomerEmailTemplateName} from '~/services/CustomerSendEmailService';

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

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

  // Send cancellation email to customer
  jobs.enqueue(new CustomerSendEmailJob({
    shop,
    payload: {
      ...payload,
      emailTemplate: CustomerEmailTemplateName.SubscriptionCancelled,
    },
  }));

  // Notify merchant about cancellation
  jobs.enqueue(new MerchantSendEmailJob({
    shop,
    payload: {
      admin_graphql_api_id: payload.admin_graphql_api_id,
    },
  }));

  return new Response();
};

subscription_contracts/pause

Triggered when a subscription contract is paused.

Webhook Configuration

[[webhooks.subscriptions]]
topics = [ "subscription_contracts/pause" ]
uri = "/webhooks/subscription_contracts/pause"

Payload Structure

The payload structure is the same as the cancel webhook, with the status field set to paused.

Handler Implementation

// app/routes/webhooks.subscription_contracts.pause.tsx
import type {ActionFunctionArgs} from '@remix-run/node';
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');

  return new Response();
};

Best Practices

  1. Idempotency: Webhook handlers should be idempotent as Shopify may send the same webhook multiple times
  2. Quick Response: Return a 200 response quickly and process heavy work in background jobs
  3. Error Handling: Log all webhook payloads for debugging and monitoring
  4. Verification: Always authenticate webhooks using Shopify’s webhook verification
  5. Job Queues: Use job queues for time-consuming operations like email sending or API calls

Build docs developers (and LLMs) love