Churn API
API documentation for Churn API
Manage subscription cancellation offers programmatically. Retrieve available offers, view applied offers on contracts, and activate retention offers to reduce churn.
Overview
The Churn API enables custom retention flows by providing access to cancellation reasons and associated offers. Configure offers in Shopify Admin > Apps > Subscribfy > Settings > Churn Offers.
Endpoint
| Property | Value |
|---|---|
| Base URL | {store}.myshopify.com/apps/subscribfy-api/v1/membership/churn |
| Authentication | Subscribfy API Key (via key parameter) |
Authentication
All requests require your Subscribfy API key. Include it as a query parameter:
?key=your_subscribfy_api_keyGenerate your API key in Subscribfy > Settings > API.
Available Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /offers | List all cancellation reasons and offers |
GET | /{contract_id}/offers | Get offers applied to a contract |
POST | /{contract_id}/offers/{offer_id}/activation | Apply an offer to a contract |
List Offers
Retrieve all cancellation reasons and their associated retention offers.
Request
GET /apps/subscribfy-api/v1/membership/churn/offers?key={api_key}Query Parameters
Prop
Type
Example Request
curl "https://your-store.myshopify.com/apps/subscribfy-api/v1/membership/churn/offers?key=your_api_key&cancellation_reason=too_expensive"Response
[
{
"id": 6,
"title": "It's too expensive",
"description": null,
"alias": "too_expensive",
"offers": [
{
"id": 15,
"name": "Discount Subscription Price",
"description": "Discount the price of the subscription",
"type": "discount_price",
"rules": {
"discount_type": "percentage",
"discount_value": 20
}
},
{
"id": 16,
"name": "Change Subscription Frequency",
"description": "Change how often the subscription is billed",
"type": "change_frequency",
"rules": {
"interval_count": 2,
"interval_name": "month"
}
}
]
}
]Cancellation Reasons
| Alias | Display Label |
|---|---|
technical_issues | I'm having technical problems |
enough_items | I have enough items |
too_expensive | It's too expensive |
not_need_subscription | I don't need a subscription |
not_using_enough | I don't use it enough |
not_found_products | I couldn't find the products I liked |
order_issues | Problems with my order |
use_another_service | I'm using another service |
other | Other |
Offer Types
| Type | Description | Rules |
|---|---|---|
discount_price | Apply discount to subscription | discount_type, discount_value |
change_frequency | Change billing frequency | interval_count, interval_name |
add_store_credits | Add store credits to customer | credit_amount |
Get Contract Offers
Retrieve all offers that have been applied to a specific subscription contract, including their current status.
Request
GET /apps/subscribfy-api/v1/membership/churn/{contract_id}/offers?key={api_key}Path Parameters
Prop
Type
Getting Contract ID
The contract ID is available from the customer metafield:
{{ customer.metafields.exison.customer_subscription1.scid }}Response
[
{
"offer": {
"id": 15,
"name": "Discount Subscription Price",
"description": "Discount the price of the subscription",
"type": "discount_price",
"rules": {
"discount_type": "percentage",
"discount_value": 20
}
},
"reward": {
"gid": "gid://shopify/SubscriptionManualDiscount/039597e2-c43d-4698-a2ee-36fd3e3fb076",
"title": "Cancellation Offer",
"target_type": "LINE_ITEM",
"recurring_cycle_limit": null,
"usage_count": 0,
"applies_on_each_item": false,
"discount_type": "percentage",
"value": 20,
"deleted_at": null
},
"status": "Active",
"deleted_at": null
},
{
"offer": {
"id": 16,
"name": "Change Subscription Frequency",
"description": "Change how often the subscription is billed",
"type": "change_frequency",
"rules": {
"interval_count": 1,
"interval_name": "year"
}
},
"reward": {
"interval_count": 1,
"interval_name": "YEAR",
"next_billing_date": "2026-10-01T10:00:00.000000Z"
},
"status": "Cancelled",
"deleted_at": "2025-10-20T10:11:28.000000Z"
}
]Response Schema by Offer Type
Discount Price Offer
{
"offer": {
"type": "discount_price",
"rules": {
"discount_type": "percentage | fixed_amount",
"discount_value": "float"
}
},
"reward": {
"gid": "Shopify_Discount_GID",
"title": "Cancellation Offer",
"target_type": "LINE_ITEM | SHIPPING_LINE",
"recurring_cycle_limit": "int | null",
"usage_count": "int",
"applies_on_each_item": "boolean",
"discount_type": "percentage | fixed_amount",
"value": "float",
"deleted_at": "datetime | null"
},
"status": "Active | Cancelled",
"deleted_at": "datetime | null"
}Change Frequency Offer
{
"offer": {
"type": "change_frequency",
"rules": {
"interval_count": "int",
"interval_name": "year | month | week | day"
}
},
"reward": {
"interval_count": "int",
"interval_name": "YEAR | MONTH | WEEK | DAY",
"next_billing_date": "datetime"
},
"status": "Active | Cancelled",
"deleted_at": "datetime | null"
}Apply Offer
Activate a retention offer on a subscription contract.
Only one offer can be active per contract at a time.
Request
POST /apps/subscribfy-api/v1/membership/churn/{contract_id}/offers/{offer_id}/activation?key={api_key}Path Parameters
Prop
Type
Success Response
{
"offer": {
"id": 16,
"name": "Change Subscription Frequency",
"description": "Change how often the subscription is billed",
"type": "change_frequency",
"rules": {
"interval_count": 1,
"interval_name": "year"
}
},
"reward": {
"interval_count": 1,
"interval_name": "YEAR",
"next_billing_date": "2026-10-01T10:00:00.000000Z"
},
"status": "Active",
"deleted_at": null
}Error Response
HTTP 403 Forbidden
{
"message": "Only one offer is allowed per contract."
}Code Examples
class ChurnAPI {
constructor(store, apiKey) {
this.baseUrl = `https://${store}/apps/subscribfy-api/v1/membership/churn`;
this.apiKey = apiKey;
}
async getOffers(cancellationReason = null) {
const params = new URLSearchParams({ key: this.apiKey });
if (cancellationReason) {
params.append('cancellation_reason', cancellationReason);
}
const response = await fetch(`${this.baseUrl}/offers?${params}`);
return response.json();
}
async getContractOffers(contractId) {
const response = await fetch(
`${this.baseUrl}/${contractId}/offers?key=${this.apiKey}`
);
return response.json();
}
async applyOffer(contractId, offerId) {
const response = await fetch(
`${this.baseUrl}/${contractId}/offers/${offerId}/activation?key=${this.apiKey}`,
{ method: 'POST' }
);
return response.json();
}
}
// Usage
const churn = new ChurnAPI('your-store.myshopify.com', 'your_api_key');
// Get offers for "too expensive" reason
const offers = await churn.getOffers('too_expensive');
console.log(`Found ${offers.length} cancellation reasons`);
// Check existing offers on a contract
const contractOffers = await churn.getContractOffers(12345);
const activeOffer = contractOffers.find(o => o.status === 'Active');
// Apply a discount offer if no active offer exists
if (!activeOffer && offers[0]?.offers[0]) {
const result = await churn.applyOffer(12345, offers[0].offers[0].id);
console.log(`Applied offer: ${result.offer.name}`);
}class ChurnAPI {
private $store;
private $apiKey;
public function __construct($store, $apiKey) {
$this->store = $store;
$this->apiKey = $apiKey;
}
private function request($endpoint, $method = 'GET') {
$url = "https://{$this->store}/apps/subscribfy-api/v1/membership/churn{$endpoint}";
$url .= (strpos($url, '?') === false ? '?' : '&') . "key={$this->apiKey}";
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
public function getOffers($cancellationReason = null) {
$endpoint = '/offers';
if ($cancellationReason) {
$endpoint .= "?cancellation_reason={$cancellationReason}";
}
return $this->request($endpoint);
}
public function getContractOffers($contractId) {
return $this->request("/{$contractId}/offers");
}
public function applyOffer($contractId, $offerId) {
return $this->request("/{$contractId}/offers/{$offerId}/activation", 'POST');
}
}
// Usage
$churn = new ChurnAPI('your-store.myshopify.com', 'your_api_key');
// Get all offers
$offers = $churn->getOffers();
foreach ($offers as $reason) {
echo "Reason: {$reason['title']}\n";
foreach ($reason['offers'] as $offer) {
echo " - {$offer['name']} ({$offer['type']})\n";
}
}
// Apply offer to contract
$result = $churn->applyOffer(12345, 15);
if (isset($result['status']) && $result['status'] === 'Active') {
echo "Offer applied successfully!";
}import requests
class ChurnAPI:
def __init__(self, store, api_key):
self.base_url = f'https://{store}/apps/subscribfy-api/v1/membership/churn'
self.api_key = api_key
def get_offers(self, cancellation_reason=None):
params = {'key': self.api_key}
if cancellation_reason:
params['cancellation_reason'] = cancellation_reason
response = requests.get(f'{self.base_url}/offers', params=params)
return response.json()
def get_contract_offers(self, contract_id):
response = requests.get(
f'{self.base_url}/{contract_id}/offers',
params={'key': self.api_key}
)
return response.json()
def apply_offer(self, contract_id, offer_id):
response = requests.post(
f'{self.base_url}/{contract_id}/offers/{offer_id}/activation',
params={'key': self.api_key}
)
return response.json()
# Usage
churn = ChurnAPI('your-store.myshopify.com', 'your_api_key')
# Get offers for customers who find it too expensive
offers = churn.get_offers('too_expensive')
for reason in offers:
print(f"Reason: {reason['title']}")
for offer in reason['offers']:
print(f" - {offer['name']}: {offer['type']}")
# Apply first available offer
if offers and offers[0]['offers']:
contract_id = 12345
offer_id = offers[0]['offers'][0]['id']
result = churn.apply_offer(contract_id, offer_id)
print(f"Result: {result['status']}")Liquid Integration
Access contract information in Shopify themes:
{% assign subscription = customer.metafields.exison.customer_subscription1 %}
{% if subscription %}
<p>Contract ID: {{ subscription.scid }}</p>
<p>Status: {{ subscription.status }}</p>
{% endif %}Disabling the Retention Flow per Theme
To suppress churn offers for a specific test or condition, define window.__sdrfMode = 'b' before the page loads. Set it via your A/B testing platform, theme code, or any script that runs in the page <head>.
When set to 'b', the cancel button skips the offer modal and cancels the subscription directly. Any other value, or leaving it unset, keeps the standard retention flow.
{% if <variant condition> %}
<script>window.__sdrfMode = 'b';</script>
{% endif %}A/B testing retention offers
Use the 'b' variant as your control group: customers in this branch cancel without seeing any offer, so you can measure how much the retention flow actually reduces churn against a baseline.
The value must be exactly the string 'b'. The script has to run before the cancellation button handler fires, so place it in the page <head> rather than after the cancel form.
Tracking is your responsibility
Subscribfy only reads the window.__sdrfMode flag to decide whether to show the offer modal. It does not record which variant a customer was assigned to or report any results. As the store owner, you are responsible for the tracking logic, assigning customers to a variant, recording the assignment, and measuring cancellations and conversions in your own A/B platform or analytics.
Error Responses
| Status | Error | Cause |
|---|---|---|
| 403 | Only one offer is allowed per contract | Contract already has an active offer |
| 404 | Not Found | Contract or offer does not exist |
| 422 | Validation Error | Invalid cancellation_reason parameter |
Offer Lifecycle
- Active - Offer is currently applied to the subscription
- Cancelled - Offer was removed (contract cancelled or offer revoked)
- 24-hour grace period - After cancellation, offers remain active for 24 hours before being fully revoked
Best Practices
- Match reason to offer - Show relevant offers based on the cancellation reason
- Check existing offers - Verify no active offer exists before applying a new one
- Handle errors gracefully - Display user-friendly messages for API errors
- Track offer usage - Monitor which offers are most effective at retention
Questions? Contact support+developer@subscribfy.com
Was this page helpful?