API REFERENCE v1

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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): configure scope, test mode only.

BODY PARAMETERS

  • kind enum REQUIRED
    • discount
    • pause
    • downgrade
    • talk
  • name string REQUIRED
  • params object REQUIRED

    discount { percent, months, coupon_code? (Maxio coupon) } · pause { months } · downgrade { target_price_id, plan_name } · talk { url }

RETURNS

ERRORS

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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): configure scope, test mode only.

PATH PARAMETERS

  • id string REQUIRED

BODY PARAMETERS

RETURNS

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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • id string REQUIRED

RETURNS

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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

QUERY PARAMETERS

RETURNS

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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

QUERY PARAMETERS

RETURNS

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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

PATH PARAMETERS

  • id string REQUIRED

RETURNS

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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • id string REQUIRED

BODY PARAMETERS

RETURNS

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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • id string REQUIRED

RETURNS

  • 204 removed
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); // 204
import requests

res = requests.delete(
    "https://api.nochurn.sh/v1/review_platforms/rvp_WdXYG22G1uHGFB2V",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
print(res.status_code)  # 204
require "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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

RETURNS

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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

BODY PARAMETERS

  • kind enum REQUIRED
    • g2
    • capterra
    • trustpilot
    • google
    • app_store
    • product_hunt
  • url string REQUIRED
  • review_count integer
  • rating number
  • rules object
    Show child parametersHide child parameters

RETURNS

ERRORS

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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

RETURNS

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": {}
}