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
The numeric ID of the subscription contract
The GraphQL Admin API ID of the subscription contract (format: gid://shopify/SubscriptionContract/{id})
admin_graphql_api_customer_id
The GraphQL Admin API ID of the customer associated with this subscription
admin_graphql_api_origin_order_id
The GraphQL Admin API ID of the originating order (null if not created from checkout)
The numeric ID of the customer
The numeric ID of the originating order (null if not created from checkout)
The current status of the subscription contract. One of: active, cancelled, expired, failed, paused, stale
The currency code for the subscription (e.g., “USD”, “EUR”)
The revision number of the subscription contract
The billing policy for the subscriptionThe billing interval unit (e.g., “day”, “week”, “month”, “year”)
The number of intervals between billings
Minimum number of billing cycles (null if no minimum)
Maximum number of billing cycles (null if unlimited)
The delivery policy for the subscriptionThe delivery interval unit
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
The numeric ID of the subscription contract
The GraphQL Admin API ID of the subscription contract
admin_graphql_api_customer_id
The GraphQL Admin API ID of the customer
admin_graphql_api_origin_order_id
The GraphQL Admin API ID of the originating order
The numeric ID of the customer
The numeric ID of the originating order
The new status of the subscription contract (will be cancelled)
The currency code for the subscription
The revision number after the status change
The billing policy for the subscription (same structure as create webhook)
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
- Idempotency: Webhook handlers should be idempotent as Shopify may send the same webhook multiple times
- Quick Response: Return a 200 response quickly and process heavy work in background jobs
- Error Handling: Log all webhook payloads for debugging and monitoring
- Verification: Always authenticate webhooks using Shopify’s webhook verification
- Job Queues: Use job queues for time-consuming operations like email sending or API calls