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
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Subscribfy API key |
cancellation_reason | string | No | Filter by reason alias (see Cancellation Reasons) |
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
| Parameter | Type | Required | Description |
|---|---|---|---|
contract_id | integer | Yes | Subscription contract ID |
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
| Parameter | Type | Required | Description |
|---|---|---|---|
contract_id | integer | Yes | Subscription contract ID |
offer_id | integer | Yes | Offer ID from /offers endpoint |
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 %}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@subscribfy.com