Get current billing cycle number for a subscription contract
Retrieves the current billing cycle number for a specific subscription contract. The cycle number represents how many successful billing attempts have occurred for this subscription, starting from 1 for the initial order.
What is a Billing Cycle? A billing cycle represents one completed billing period in a subscription’s lifetime. Each successful billing attempt increments the cycle count. This number is crucial for:
- Tracking subscription progress towards minimum/maximum cycle limits
- Applying cycle-based pricing adjustments (discounts after N cycles)
- Determining eligibility for cancellation (minimum cycles requirement)
- Calculating customer lifetime value
- Analyzing subscription retention metrics
How Cycle Counting Works:
Initial Order:
- Cycle 1 starts when subscription is first created
- Initial order counts as the first billing cycle
- Includes origin order that created the subscription
Subsequent Orders:
- Each successful billing attempt increments cycle by 1
- Only SUCCESS status billing attempts are counted
- Failed/skipped billing attempts do NOT increment cycle
- Paused subscriptions maintain their current cycle number
Calculation Formula:
Current Cycle = 1 + (Number of Successful Billing Attempts)
Example Timeline:
- Day 1: Subscription created, initial order → Cycle 1
- Day 30: First recurring order successful → Cycle 2
- Day 60: Second recurring order successful → Cycle 3
- Day 90: Billing fails (payment declined) → Still Cycle 3
- Day 95: Retry successful → Cycle 4
Use Cases:
1. Cancellation Eligibility:
- Verify customer has met minimum cycle requirement
- Enforce contract terms (e.g., “3 month minimum”)
- Display “Can cancel after N more orders” messaging
- Block premature cancellations
2. Pricing Adjustments:
- Apply introductory pricing for first N cycles
- Trigger loyalty discounts after X cycles
- Calculate when pricing changes take effect
- Implement “First 3 months 50% off” promotions
3. Customer Retention:
- Identify subscriptions at risky cycle counts
- Send retention campaigns at specific milestones
- Track average cycles before churn
- Celebrate subscription anniversaries
4. Analytics & Reporting:
- Calculate customer lifetime value (cycle × price)
- Analyze subscription duration distribution
- Track retention curves by cohort
- Measure success of lifecycle campaigns
5. Customer Portal Display:
- Show “Order #X of Y” progress indicators
- Display remaining cycles until cancellation allowed
- Show subscription tenure/loyalty status
- Calculate and display subscription value earned
Response Format:
Returns a single integer representing the current cycle number:
3
Response Examples:
New subscription (just created):
1
After 5 successful billing attempts:
6
Note: Initial order (1) + 5 successful renewals = Cycle 6
Important Considerations:
Cycle vs. Billing Attempts:
- Failed billing attempts don’t increment cycle
- Skipped orders don’t increment cycle
- Manual order creation may not increment cycle
- Cycle represents successful billing events only
Paused Subscriptions:
- Cycle number remains frozen while paused
- Resumes at same cycle when un-paused
- Pause duration doesn’t affect cycle count
Minimum/Maximum Cycles:
minCycles: Minimum cycles before cancellation allowedmaxCycles: Subscription auto-expires after this many cycles- Use this endpoint to check progress towards these limits
Data Source:
- Queries Appstle database (not Shopify API)
- Counts records in subscription_billing_attempt table
- Filters by SUCCESS status only
- Fast response time (< 100ms typically)
Integration Example:
Check if customer can cancel:
// Get subscription details
const contract = await getSubscriptionContract(contractId);
const currentCycle = await fetch(
`/api/external/v2/subscription-contract-details/current-cycle/${contractId}`,
{ headers: { 'X-API-Key': 'your-key' } }
).then(r => r.json());
const minCycles = contract.minCycles || 0;
if (currentCycle >= minCycles) {
console.log('Customer can cancel now');
showCancelButton();
} else {
const cyclesRemaining = minCycles - currentCycle;
console.log(`Must complete ${cyclesRemaining} more orders before canceling`);
showMinimumCommitmentMessage(cyclesRemaining);
}
Display progress to max cycles:
const currentCycle = await getCycleNumber(contractId);
const maxCycles = contract.maxCycles;
if (maxCycles) {
const progress = (currentCycle / maxCycles) * 100;
console.log(`Subscription ${progress.toFixed(0)}% complete (${currentCycle}/${maxCycles})`);
if (currentCycle === maxCycles) {
console.log('This is the final cycle - subscription will expire after this order');
}
}
Performance Characteristics:
Fast Query:
- Simple database count query
- Indexed by shop and contractId
- Typical response time: 50-150ms
- Suitable for real-time UI updates
Best Practices:
- Cache Results: Cache cycle number briefly (few minutes) to reduce API calls
- Combine with Contract Data: Fetch contract details simultaneously for min/max cycles
- Handle Edge Cases: Account for subscriptions with no successful billings yet
- Display Progress: Show cycle number in customer-friendly format (“Order 3 of 12”)
- Sync with Billing: Update cycle number after each billing attempt completes
Common Misunderstandings:
Myth: Cycle = Months Subscribed
- Reality: Cycle = Successful billing attempts, not time elapsed
- A paused subscription stays at same cycle for months
- Failed payments don’t advance the cycle
Myth: First order is Cycle 0
- Reality: Cycles start at 1, not 0
- Initial/origin order is Cycle 1
Related Fields:
minCycles: Minimum cycles before cancellation (from contract)maxCycles: Maximum cycles before auto-expiry (from contract)billingInterval: Frequency between cycles (from contract)
Authentication: Requires valid X-API-Key header
curl --request GET \
--url https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId} \
--header 'X-API-Key: <x-api-key>'import requests
url = "https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId}"
headers = {"X-API-Key": "<x-api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<x-api-key>'}};
fetch('https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));const url = 'https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId}';
const options = {method: 'GET', headers: {'X-API-Key': '<x-api-key>'}};
fetch(url, options)
.then(res => res.json())
.then(json => console.log(json))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-Key: <x-api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<x-api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}require 'uri'
require 'net/http'
url = URI("https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<x-api-key>'
response = http.request(request)
puts response.read_body6curl --request GET \
--url https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId} \
--header 'X-API-Key: <x-api-key>'import requests
url = "https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId}"
headers = {"X-API-Key": "<x-api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<x-api-key>'}};
fetch('https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));const url = 'https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId}';
const options = {method: 'GET', headers: {'X-API-Key': '<x-api-key>'}};
fetch(url, options)
.then(res => res.json())
.then(json => console.log(json))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-Key: <x-api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<x-api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}require 'uri'
require 'net/http'
url = URI("https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details/current-cycle/{contractId}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<x-api-key>'
response = http.request(request)
puts response.read_body6