Get customer retention activities
Retrieves a paginated, filterable list of customer retention activities for the authenticated shop. A retention activity is recorded whenever a customer goes through the cancellation or pause flow and either accepts a retention offer (discount, pause, frequency change, product swap, etc.) or proceeds to cancel/pause, along with the reason they selected.
What is a Retention Activity? Every time a subscriber interacts with the cancellation/pause save-flow, an activity record is created capturing the contract, the action taken (e.g. offer shown, offer accepted, cancelled, paused), the event source (CANCELLATION or PAUSE), the selected reason, any discount applied, and the resulting status.
First offer vs fallback offer:
When the selected cancellation reason has a fallback offer, an accepted offer’s additionalInfo JSON carries offerTier: PRIMARY if the customer accepted the first offer, FALLBACK if they declined it and accepted the fallback. It is absent for reasons without a fallback offer, for declines, and for activities recorded before this field was introduced.
Filtering:
contractId- restrict results to a single subscription contracteventSource-CANCELLATIONorPAUSEstatus- status of the retention activity (e.g. PENDING, COMPLETED)retentionAction- the action recorded (e.g. offer type accepted or DECLINED)retentionReason- the cancellation/pause reason the customer selectedfromDay/toDay- ISO-8601 date-time bounds onactivityOn
Pagination:
Standard Spring pagination parameters are supported: page (0-indexed), size, and sort (e.g. sort=id,desc). Pagination metadata (total count, total pages, link headers) is returned in the response headers.
Use Cases:
- Build custom churn/retention dashboards and reports
- Sync retention activity into an external CRM or data warehouse
- Audit which retention offers are converting for a given contract or reason
- Feed win-back campaign automation with recent cancellation reasons
Authentication: Requires a valid API key, passed via the X-API-Key header (preferred) or the api_key query parameter (deprecated).
curl --request GET \
--url https://subscription-admin.appstle.com/api/external/v2/customer-retention-activitiesimport requests
url = "https://subscription-admin.appstle.com/api/external/v2/customer-retention-activities"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://subscription-admin.appstle.com/api/external/v2/customer-retention-activities', 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/customer-retention-activities';
const options = {method: 'GET'};
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/customer-retention-activities",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);
$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/customer-retention-activities"
req, _ := http.NewRequest("GET", url, nil)
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/customer-retention-activities")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
response = http.request(request)
puts response.read_body[
{
"activityOn": "2026-08-01T14:22:10Z",
"additionalInfo": "{\"offerId\":\"disc_20pct\"}",
"cancellationReason": "Too expensive",
"contractId": 991823,
"customerEmail": "jane.doe@example.com",
"customerName": "Jane Doe",
"discountApplied": "20% off for 3 months",
"eventSource": "CANCELLATION",
"id": 4821,
"retentionAction": "DISCOUNT_ACCEPTED",
"shop": "example-shop.myshopify.com",
"status": "COMPLETED"
}
]{
"detail": "Valid X-API-Key header is required",
"status": 401,
"title": "Authentication required",
"type": "https://example.com/errors/unauthorized"
}{
"detail": "API key does not have permission to view customer retention activities",
"status": 403,
"title": "Insufficient permissions",
"type": "https://example.com/errors/forbidden"
}Headers
Query Parameters
API Key (Deprecated - Use Header X-API-Key instead)
PAUSE, CANCELLATION Pagination parameters (page number and size)
Show child attributes
Show child attributes
Response
Customer retention activities successfully retrieved
curl --request GET \
--url https://subscription-admin.appstle.com/api/external/v2/customer-retention-activitiesimport requests
url = "https://subscription-admin.appstle.com/api/external/v2/customer-retention-activities"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://subscription-admin.appstle.com/api/external/v2/customer-retention-activities', 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/customer-retention-activities';
const options = {method: 'GET'};
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/customer-retention-activities",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);
$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/customer-retention-activities"
req, _ := http.NewRequest("GET", url, nil)
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/customer-retention-activities")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
response = http.request(request)
puts response.read_body[
{
"activityOn": "2026-08-01T14:22:10Z",
"additionalInfo": "{\"offerId\":\"disc_20pct\"}",
"cancellationReason": "Too expensive",
"contractId": 991823,
"customerEmail": "jane.doe@example.com",
"customerName": "Jane Doe",
"discountApplied": "20% off for 3 months",
"eventSource": "CANCELLATION",
"id": 4821,
"retentionAction": "DISCOUNT_ACCEPTED",
"shop": "example-shop.myshopify.com",
"status": "COMPLETED"
}
]{
"detail": "Valid X-API-Key header is required",
"status": 401,
"title": "Authentication required",
"type": "https://example.com/errors/unauthorized"
}{
"detail": "API key does not have permission to view customer retention activities",
"status": 403,
"title": "Insufficient permissions",
"type": "https://example.com/errors/forbidden"
}