RETENTION
Cancel flow
The Offer object
The cancel flow: its configuration, design, sessions and the customer's answers.
ATTRIBUTES
Schema: Offer
THE OFFER OBJECT
{
"object": "offer",
"id": "off_ZUAX1WJmcBxXHowk",
"kind": "discount",
"name": "30% for 3 months",
"params": {
"months": 3,
"percent": 30
},
"archived_at": "2026-11-04T10:00:00Z",
"created_at": "2026-11-04T10:00:00Z"
} Add an offer to the cancel flow's library
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth):
configurescope, test mode only.
BODY PARAMETERS
-
- discount
- pause
- downgrade
- talk
-
discount { percent, months, coupon_code? (Maxio coupon) } · pause { months } · downgrade { target_price_id, plan_name } · talk { url }
RETURNS
-
201a discount that names a Maxio coupon — the Offer object
ERRORS
-
422invalid_request a talk offer without an https link
POST/v1/offers
curl https://api.nochurn.sh/v1/offers \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"kind": "discount",
"name": "30% for 3 months",
"params": {
"percent": 30,
"months": 3
}
}'const res = await fetch("https://api.nochurn.sh/v1/offers", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"kind": "discount",
"name": "30% for 3 months",
"params": {
"percent": 30,
"months": 3
}
}),
});
const offer = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/offers",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"kind": "discount",
"name": "30% for 3 months",
"params": {
"percent": 30,
"months": 3
}
},
)
offer = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/offers")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"kind" => "discount",
"name" => "30% for 3 months",
"params" => {
"percent" => 30,
"months" => 3
}
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
offer = JSON.parse(res.body) RESPONSE 201
{
"object": "offer",
"id": "off_9QeL5KwSdZsZnnwJ",
"kind": "discount",
"name": "30% for 3 months",
"params": {
"percent": 30,
"months": 3
},
"archived_at": null,
"created_at": "2026-11-04T10:00:00Z"
} Edit an offer
Published flow versions keep their frozen copy of the offer.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth):
configurescope, test mode only.
PATH PARAMETERS
BODY PARAMETERS
RETURNS
-
200edited — the Offer object
PATCH/v1/offers/{id}
curl https://api.nochurn.sh/v1/offers/off_vQW5kVg4HApSZfQX \
-X PATCH \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"name": "25% for 3 months",
"params": {
"percent": 25,
"months": 3
}
}'const res = await fetch("https://api.nochurn.sh/v1/offers/off_vQW5kVg4HApSZfQX", {
method: "PATCH",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "25% for 3 months",
"params": {
"percent": 25,
"months": 3
}
}),
});
const offer = await res.json();import requests
res = requests.patch(
"https://api.nochurn.sh/v1/offers/off_vQW5kVg4HApSZfQX",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"name": "25% for 3 months",
"params": {
"percent": 25,
"months": 3
}
},
)
offer = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/offers/off_vQW5kVg4HApSZfQX")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"name" => "25% for 3 months",
"params" => {
"percent" => 25,
"months" => 3
}
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
offer = JSON.parse(res.body) RESPONSE 200
{
"object": "offer",
"id": "off_vQW5kVg4HApSZfQX",
"kind": "discount",
"name": "25% for 3 months",
"params": {
"percent": 25,
"months": 3
},
"archived_at": null,
"created_at": "2026-11-04T10:00:00Z"
} Archive an offer
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
RETURNS
-
200archived (published flows keep their copy) — the Offer object
DELETE/v1/offers/{id}
curl https://api.nochurn.sh/v1/offers/off_ZUAX1WJmcBxXHowk \
-X DELETE \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/offers/off_ZUAX1WJmcBxXHowk", {
method: "DELETE",
headers: {
Authorization: "Bearer nc_test_…",
},
});
const offer = await res.json();import requests
res = requests.delete(
"https://api.nochurn.sh/v1/offers/off_ZUAX1WJmcBxXHowk",
headers={
"Authorization": "Bearer nc_test_…",
},
)
offer = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/offers/off_ZUAX1WJmcBxXHowk")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer nc_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
offer = JSON.parse(res.body) RESPONSE 200
{
"object": "offer",
"id": "off_ZUAX1WJmcBxXHowk",
"kind": "discount",
"name": "30% for 3 months",
"params": {
"months": 3,
"percent": 30
},
"archived_at": "2026-11-04T10:00:00Z",
"created_at": "2026-11-04T10:00:00Z"
} Offer library
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
QUERY PARAMETERS
- include_archived boolean
RETURNS
-
200active offers — a list of Offer object (OfferList)
GET/v1/offers
curl https://api.nochurn.sh/v1/offers \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/offers", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const page = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/offers",
headers={
"Authorization": "Bearer nc_test_…",
},
)
page = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/offers")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer nc_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
page = JSON.parse(res.body) RESPONSE 200
{
"object": "list",
"data": [
{
"object": "offer",
"id": "off_KmCAXbU2dJBckeAo",
"kind": "discount",
"name": "30% for 3 months",
"params": {
"months": 3,
"percent": 30
},
"archived_at": null,
"created_at": "2026-11-04T10:00:00Z"
}
],
"has_more": false,
"next_cursor": null
} Cancel sessions
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
QUERY PARAMETERS
RETURNS
-
200newest first — a list of CancelSessionReport (CancelSessionReportList)
GET/v1/cancel_sessions
curl https://api.nochurn.sh/v1/cancel_sessions \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/cancel_sessions", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const page = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/cancel_sessions",
headers={
"Authorization": "Bearer nc_test_…",
},
)
page = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/cancel_sessions")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer nc_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
page = JSON.parse(res.body) RESPONSE 200
{
"object": "list",
"data": [
{
"object": "cancel_session",
"id": "ses_XsgUyosnL9TPcZiz",
"livemode": false,
"step": "reason",
"outcome": null,
"locale": "en",
"holdout": false,
"published": false,
"customer": {
"id": "cst_H5m8w5V7ovfoLPQE",
"stripe_customer_id": null,
"external_id": null
},
"subscription": null,
"subscriptions": [],
"reasons": [
{
"key": "too_expensive",
"label": "It's too expensive",
"free_text": false
},
{
"key": "missing_feature",
"label": "A feature is missing",
"free_text": false
},
{
"key": "not_using",
"label": "I don't use it enough",
"free_text": false
},
{
"key": "switching",
"label": "I'm switching to another tool",
"free_text": false
},
{
"key": "temporary",
"label": "I only need a break",
"free_text": false
},
{
"key": "project_finished",
"label": "My project is finished",
"free_text": false
},
{
"key": "other",
"label": "Something else",
"free_text": true
}
],
"offer": null,
"talk_url": null,
"texts": {
"review_ask": "Would you leave a short review on {{platform}}? No reward, just your honest opinion.",
"review_cta": "Leave a review",
"title": "Before you go",
"reason_prompt": "Why are you cancelling?",
"other_placeholder": "Tell us more (optional)",
"offer_title": "One last thing",
"accept": "Accept the offer",
"decline": "No thanks, continue",
"feedback_prompt": "Anything we could have done better?",
"feedback_submit": "Continue",
"confirm_title": "Cancel your subscription?",
"confirm_body": "You keep access until the end of the paid period.",
"cancel_now": "Cancel my subscription",
"saved_title": "Thanks for staying!",
"cancelled_title": "Your subscription is cancelled",
"cancelled_body": "You keep access until the end of the paid period.",
"offer_discount": "{{percent}}% off for {{months}} months",
"offer_pause": "Pause for {{months}} months, free",
"offer_downgrade": "Switch to {{plan}}",
"offer_talk": "Talk to the team",
"choose_subscription_title": "Which subscription?",
"choose_subscription_body": "You have several subscriptions. Choose the one you want to cancel.",
"choose_subscription_select": "Continue with this one",
"cancel_this_one": "Cancel this one",
"renews_on": "Renews on {{date}}"
},
"feedback_enabled": true,
"feedback": {
"enabled": true,
"required": false
},
"theme": {
"object": "cancel_flow_theme",
"preset": "light",
"format": "modal",
"tokens": {
"bg": "#f5f6fa",
"surface": "#ffffff",
"text": "#1c1e4a",
"muted": "#686b85",
"accent": "#4637d9",
"accent_text": "#ffffff",
"border": "#e3e5ec",
"danger": "#c2410c",
"font": "system-ui, sans-serif",
"font_size": 15,
"radius": 12,
"radius_control": 8,
"overlay": 0.5,
"max_width": 480,
"shadow": "md",
"spacing": "regular",
"border_width": 1,
"border_style": "solid",
"font_scale": 1.25,
"font_weight_title": 600
},
"font_url": null,
"brand": "logo",
"logo_url": null,
"app_name": "Account 1",
"end_screen": {
"style": "celebrate",
"auto_close_seconds": 6
},
"custom_css": "",
"cancel_button_check": "ok",
"classes": [
"nc-modal",
"nc-title",
"nc-reason",
"nc-offer",
"nc-btn-primary",
"nc-btn-cancel",
"nc-footer"
]
},
"cancel_url_now": null,
"cancel_status": null,
"cancel_effective_at": null,
"result": null,
"access_until": null,
"confirmation_email_sent": false,
"review": null,
"expires_at": "2026-11-05T10:00:00Z",
"created_at": "2026-11-04T10:00:00Z",
"reason": "too_expensive",
"comment": "Pricey"
}
],
"has_more": false,
"next_cursor": null
} Replay a cancel session
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
RETURNS
-
200the timestamped steps — a CancelSessionReplay
GET/v1/cancel_sessions/{id}/replay
curl https://api.nochurn.sh/v1/cancel_sessions/ses_XsgUyosnL9TPcZiz/replay \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/cancel_sessions/ses_XsgUyosnL9TPcZiz/replay", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const cancelSessionReplay = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/cancel_sessions/ses_XsgUyosnL9TPcZiz/replay",
headers={
"Authorization": "Bearer nc_test_…",
},
)
cancel_session_replay = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/cancel_sessions/ses_XsgUyosnL9TPcZiz/replay")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer nc_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
cancel_session_replay = JSON.parse(res.body) RESPONSE 200
{
"object": "cancel_session_replay",
"id": "ses_AMW6ttCwGtJUgAYx",
"steps": [
{
"at": "2026-11-04T10:00:00Z",
"step": "reason"
}
],
"reason": null,
"comment": null,
"feedback": null,
"outcome": null,
"cancel_status": null
} Edit or disable a review page
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
BODY PARAMETERS
- url string
- review_count integer
- rating number
- status enum
- active
- disabled
- rules object
RETURNS
-
200edited — a ReviewPlatform
PATCH/v1/review_platforms/{id}
curl https://api.nochurn.sh/v1/review_platforms/rvp_WdXYG22G1uHGFB2V \
-X PATCH \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"review_count": 42,
"status": "disabled"
}'const res = await fetch("https://api.nochurn.sh/v1/review_platforms/rvp_WdXYG22G1uHGFB2V", {
method: "PATCH",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"review_count": 42,
"status": "disabled"
}),
});
const reviewPlatform = await res.json();import requests
res = requests.patch(
"https://api.nochurn.sh/v1/review_platforms/rvp_WdXYG22G1uHGFB2V",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"review_count": 42,
"status": "disabled"
},
)
review_platform = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/review_platforms/rvp_WdXYG22G1uHGFB2V")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"review_count" => 42,
"status" => "disabled"
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
review_platform = JSON.parse(res.body) RESPONSE 200
{
"object": "review_platform",
"id": "rvp_WdXYG22G1uHGFB2V",
"kind": "g2",
"url": "https://g2.com/acme",
"status": "disabled",
"review_count": 42,
"rating": null,
"rules": {},
"created_at": "2026-11-04T10:00:00Z"
} Remove a review page
Deleted, or disabled when reviews were already requested on it.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
RETURNS
-
204removed
DELETE/v1/review_platforms/{id}
curl https://api.nochurn.sh/v1/review_platforms/rvp_WdXYG22G1uHGFB2V \
-X DELETE \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/review_platforms/rvp_WdXYG22G1uHGFB2V", {
method: "DELETE",
headers: {
Authorization: "Bearer nc_test_…",
},
});
console.log(res.status); // 204import requests
res = requests.delete(
"https://api.nochurn.sh/v1/review_platforms/rvp_WdXYG22G1uHGFB2V",
headers={
"Authorization": "Bearer nc_test_…",
},
)
print(res.status_code) # 204require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/review_platforms/rvp_WdXYG22G1uHGFB2V")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer nc_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code # 204 RESPONSE 204
// No content Your review pages
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
RETURNS
-
200the list — a list of ReviewPlatform (ReviewPlatformList)
GET/v1/review_platforms
curl https://api.nochurn.sh/v1/review_platforms \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/review_platforms", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const page = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/review_platforms",
headers={
"Authorization": "Bearer nc_test_…",
},
)
page = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/review_platforms")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer nc_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
page = JSON.parse(res.body) RESPONSE 200
{
"object": "list",
"data": [
{
"object": "review_platform",
"id": "rvp_DLEKX7d6fQenDkjn",
"kind": "g2",
"url": "https://g2.com/acme",
"status": "active",
"review_count": null,
"rating": null,
"rules": {},
"created_at": "2026-11-04T10:00:00Z"
}
],
"has_more": false,
"next_cursor": null
} Add a review page
Happy leavers are asked once, ever, on the platform with the fewest NoChurn requests this quarter. No reward is ever offered.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
BODY PARAMETERS
-
- g2
- capterra
- trustpilot
- app_store
- product_hunt
- review_count integer
- rating number
- rules object
RETURNS
-
201added — a ReviewPlatform
ERRORS
-
422invalid_request not an https link
POST/v1/review_platforms
curl https://api.nochurn.sh/v1/review_platforms \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"kind": "trustpilot",
"url": "https://trustpilot.com/review/acme.com",
"rules": {
"countries": ["FR"]
}
}'const res = await fetch("https://api.nochurn.sh/v1/review_platforms", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"kind": "trustpilot",
"url": "https://trustpilot.com/review/acme.com",
"rules": {
"countries": ["FR"]
}
}),
});
const reviewPlatform = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/review_platforms",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"kind": "trustpilot",
"url": "https://trustpilot.com/review/acme.com",
"rules": {
"countries": ["FR"]
}
},
)
review_platform = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/review_platforms")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"kind" => "trustpilot",
"url" => "https://trustpilot.com/review/acme.com",
"rules" => {
"countries" => ["FR"]
}
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
review_platform = JSON.parse(res.body) RESPONSE 201
{
"object": "review_platform",
"id": "rvp_se7mSPq3Ww8gp51o",
"kind": "trustpilot",
"url": "https://trustpilot.com/review/acme.com",
"status": "active",
"review_count": null,
"rating": null,
"rules": {
"countries": ["FR"]
},
"created_at": "2026-11-04T10:00:00Z"
} Review requests this month
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
RETURNS
-
200counts from what NoChurn did — a ReviewRequestStats
GET/v1/review_requests/stats
curl https://api.nochurn.sh/v1/review_requests/stats \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/review_requests/stats", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const reviewRequestStats = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/review_requests/stats",
headers={
"Authorization": "Bearer nc_test_…",
},
)
review_request_stats = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/review_requests/stats")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer nc_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
review_request_stats = JSON.parse(res.body) RESPONSE 200
{
"object": "review_request_stats",
"period_start": "2026-11-01T00:00:00Z",
"cancellations": 0,
"requested": 0,
"clicked": 0,
"follow_ups_sent": 0,
"by_platform": {}
}