RETENTION
Flows
The Flow object
Recovery, prevention, cancel and win-back flows: drafts, publishing, emails and runs.
ATTRIBUTES
- object enum
- flow
- id string
- kind enum
- prevention
- recovery
- cancel
- winback
- key string
- name string
- status enum
draft: never published; test: runs in test mode only; live: runs in both modes; paused.
- draft
- test
- live
- paused
- holdout_percent integer
Share of customers NoChurn leaves alone, to measure its effect.
- test_version object | null
Show child attributesHide child attributes
- id string
- number integer
- published_to enum
- test
- live
-
Show 9 more attributesHide 9 attributes
- live_version object | null
Show child attributesHide child attributes
- id string
- number integer
- published_to enum
- test
- live
-
- unpublished_changes array of strings
Draft sections that differ from the last version.
- mode_status object
Where the flow stands in the mode of the request (NoChurn-Mode or the key's mode).
Show child attributesHide child attributes
- livemode boolean
- state enum
published: a version runs in this mode; draft: nothing published in this mode yet; paused: paused (both modes).
- published
- draft
- paused
- version integer | null
Number of the version that runs in this mode.
- unpublished_changes array of strings
Draft sections not yet published in this mode (a pending draft).
-
- draft object
Configuration being edited, by section.
- logos array of objects
Logos uploaded for the cancel modal and not removed (POST /v1/flows/cancel/logos); [] for the other flows.
Show child attributesHide child attributes
Schema: FlowDetail
{
"object": "flow",
"id": "flw_wrgKan8xHjAdgH4k",
"kind": "cancel",
"key": "default",
"name": "Cancel flow",
"status": "live",
"holdout_percent": 5,
"test_version": {
"id": "flv_hPABNrYmFDsYS7G8",
"number": 1,
"published_at": "2026-11-04T10:00:00.000Z",
"published_to": "live"
},
"live_version": {
"id": "flv_hPABNrYmFDsYS7G8",
"number": 1,
"published_at": "2026-11-04T10:00:00.000Z",
"published_to": "live"
},
"unpublished_changes": [],
"mode_status": {
"livemode": false,
"state": "published",
"version": 1,
"published_at": "2026-11-04T10:00:00.000Z",
"unpublished_changes": []
},
"draft_updated_at": "2026-11-04T10:00:00.000Z",
"paused_at": null,
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z",
"draft": {
"pause": {
"inapp": "none"
},
"steps": ["reason", "offer", "feedback", "confirmation"],
"texts": {},
"design": {
"format": "modal",
"preset": "light",
"tokens": {},
"custom_css": ""
},
"reasons": [
{
"key": "too_expensive",
"labels": {
"de": "Es ist zu teuer",
"en": "It's too expensive",
"fr": "C'est trop cher",
"it": "È troppo caro",
"pt": "É demasiado caro"
},
"offer_id": null
},
{
"key": "missing_feature",
"labels": {
"de": "Eine Funktion fehlt",
"en": "A feature is missing",
"fr": "Il manque une fonctionnalité",
"it": "Manca una funzionalità",
"pt": "Falta uma funcionalidade"
},
"offer_id": null
},
{
"key": "not_using",
"labels": {
"de": "Ich nutze es zu wenig",
"en": "I don't use it enough",
"fr": "Je ne l'utilise pas assez",
"it": "Non lo uso abbastanza",
"pt": "Não o uso o suficiente"
},
"offer_id": null
},
{
"key": "switching",
"labels": {
"de": "Ich wechsle zu einem anderen Tool",
"en": "I'm switching to another tool",
"fr": "Je passe à un autre outil",
"it": "Passo a un altro strumento",
"pt": "Vou mudar para outra ferramenta"
},
"offer_id": null
},
{
"key": "temporary",
"labels": {
"de": "Ich brauche nur eine Pause",
"en": "I only need a break",
"fr": "J'ai juste besoin d'une pause",
"it": "Ho solo bisogno di una pausa",
"pt": "Só preciso de uma pausa"
},
"offer_id": null
},
{
"key": "project_finished",
"labels": {
"de": "Mein Projekt ist abgeschlossen",
"en": "My project is finished",
"fr": "Mon projet est terminé",
"it": "Il mio progetto è finito",
"pt": "O meu projeto terminou"
},
"offer_id": null
},
{
"key": "other",
"labels": {
"de": "Etwas anderes",
"en": "Something else",
"fr": "Autre chose",
"it": "Altro",
"pt": "Outra coisa"
},
"offer_id": null,
"free_text": true
}
],
"feedback": {
"enabled": true,
"required": false
},
"cancel_mode": "at_period_end",
"review_requests": {
"enabled": false,
"happy_reasons": ["project_finished", "too_expensive", "temporary"],
"follow_up_email_days": 2
},
"offers_once_per_customer_per_year": true
},
"logos": []
} Retrieve a flow with its draft
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
-
flw_… or the kind of the default flow: prevention, recovery, cancel, winback.
RETURNS
-
200the flow — the Flow object
ERRORS
-
404not_found unknown flow
curl https://api.nochurn.sh/v1/flows/flw_ANSL3Lgcmq6LE2og \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/flows/flw_ANSL3Lgcmq6LE2og", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const flow = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/flows/flw_ANSL3Lgcmq6LE2og",
headers={
"Authorization": "Bearer nc_test_…",
},
)
flow = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_ANSL3Lgcmq6LE2og")
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) }
flow = JSON.parse(res.body) {
"object": "flow",
"id": "flw_ANSL3Lgcmq6LE2og",
"kind": "recovery",
"key": "default",
"name": "Recovery flow",
"status": "draft",
"holdout_percent": 0,
"test_version": null,
"live_version": null,
"unpublished_changes": [],
"mode_status": {
"livemode": false,
"state": "draft",
"version": null,
"published_at": null,
"unpublished_changes": []
},
"draft_updated_at": "2026-11-04T10:00:00.000Z",
"paused_at": null,
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z",
"draft": {
"inapp": {
"wall": {
"enabled": true,
"after_days": 7
},
"banner": {
"enabled": true,
"audience": "admins",
"dismiss_hours": 24
},
"annual_grace_days": 14
},
"emails": [
{
"day": 0
},
{
"day": 4
},
{
"day": 10
}
],
"retries": {
"then": "cancel_at_period_end_and_winback",
"rules": [
{
"timing": "after_payday",
"attempts": 4,
"decline_code": "insufficient_funds"
},
{
"attempts": 0,
"email_now": true,
"decline_code": "expired_card"
},
{
"days": [3, 5, 7],
"attempts": 3,
"decline_code": "do_not_honor"
},
{
"stop": true,
"email": false,
"attempts": 0,
"decline_code": "fraudulent"
}
],
"max_days": 14
},
"send_window": {
"audience": null,
"quiet_hours": null,
"respect_customer_timezone": null
},
"email_settings": {
"tone": "friendly",
"review": "auto_check",
"ai_writes": true,
"languages": ["en", "fr", "de", "it", "pt"]
},
"card_update_page": {
"methods": ["card", "apple_pay", "google_pay", "sepa_debit"],
"link_valid_days": 14,
"pay_open_invoice": true
}
},
"logos": []
} Edit the draft
A section sent replaces that section of the draft. Changes run only once published.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth):
configurescope, test mode only.
PATH PARAMETERS
-
flw_… or the kind of the default flow: prevention, recovery, cancel, winback.
BODY PARAMETERS
RETURNS
-
200draft updated, unpublished changes listed — the Flow object
ERRORS
-
400invalid_request unknown section
curl https://api.nochurn.sh/v1/flows/flw_YJEtbe7bMLQa255h \
-X PATCH \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"draft": {
"retries": {
"max_days": 10
}
},
"holdout_percent": 5
}'const res = await fetch("https://api.nochurn.sh/v1/flows/flw_YJEtbe7bMLQa255h", {
method: "PATCH",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"draft": {
"retries": {
"max_days": 10
}
},
"holdout_percent": 5
}),
});
const flow = await res.json();import requests
res = requests.patch(
"https://api.nochurn.sh/v1/flows/flw_YJEtbe7bMLQa255h",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"draft": {
"retries": {
"max_days": 10
}
},
"holdout_percent": 5
},
)
flow = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_YJEtbe7bMLQa255h")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"draft" => {
"retries" => {
"max_days" => 10
}
},
"holdout_percent" => 5
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
flow = JSON.parse(res.body) {
"object": "flow",
"id": "flw_YJEtbe7bMLQa255h",
"kind": "recovery",
"key": "default",
"name": "Recovery flow",
"status": "draft",
"holdout_percent": 5,
"test_version": null,
"live_version": null,
"unpublished_changes": ["retries"],
"mode_status": {
"livemode": false,
"state": "draft",
"version": null,
"published_at": null,
"unpublished_changes": ["retries"]
},
"draft_updated_at": "2026-11-04T10:00:00.000Z",
"paused_at": null,
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z",
"draft": {
"inapp": {
"wall": {
"enabled": true,
"after_days": 7
},
"banner": {
"enabled": true,
"audience": "admins",
"dismiss_hours": 24
},
"annual_grace_days": 14
},
"emails": [
{
"day": 0
},
{
"day": 4
},
{
"day": 10
}
],
"retries": {
"max_days": 10
},
"send_window": {
"audience": null,
"quiet_hours": null,
"respect_customer_timezone": null
},
"email_settings": {
"tone": "friendly",
"review": "auto_check",
"ai_writes": true,
"languages": ["en", "fr", "de", "it", "pt"]
},
"card_update_page": {
"methods": ["card", "apple_pay", "google_pay", "sepa_debit"],
"link_valid_days": 14,
"pay_open_invoice": true
}
},
"logos": []
} List the 4 flows
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
RETURNS
-
200prevention, recovery, cancel and win-back — a list of Flow (FlowList)
curl https://api.nochurn.sh/v1/flows \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/flows", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const page = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/flows",
headers={
"Authorization": "Bearer nc_test_…",
},
)
page = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows")
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) {
"object": "list",
"data": [
{
"object": "flow",
"id": "flw_1K3ms53XtrQohYHb",
"kind": "prevention",
"key": "default",
"name": "Prevention flow",
"status": "draft",
"holdout_percent": 5,
"test_version": null,
"live_version": null,
"unpublished_changes": [],
"mode_status": {
"livemode": false,
"state": "draft",
"version": null,
"published_at": null,
"unpublished_changes": []
},
"draft_updated_at": "2026-11-04T10:00:00.000Z",
"paused_at": null,
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z"
},
{
"object": "flow",
"id": "flw_pqMp6g74MZoresPY",
"kind": "recovery",
"key": "default",
"name": "Recovery flow",
"status": "draft",
"holdout_percent": 0,
"test_version": null,
"live_version": null,
"unpublished_changes": [],
"mode_status": {
"livemode": false,
"state": "draft",
"version": null,
"published_at": null,
"unpublished_changes": []
},
"draft_updated_at": "2026-11-04T10:00:00.000Z",
"paused_at": null,
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z"
},
{
"object": "flow",
"id": "flw_NZ3Jng8vP1kfQmtf",
"kind": "cancel",
"key": "default",
"name": "Cancel flow",
"status": "draft",
"holdout_percent": 5,
"test_version": null,
"live_version": null,
"unpublished_changes": [],
"mode_status": {
"livemode": false,
"state": "draft",
"version": null,
"published_at": null,
"unpublished_changes": []
},
"draft_updated_at": "2026-11-04T10:00:00.000Z",
"paused_at": null,
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z"
},
{
"object": "flow",
"id": "flw_rVqAhXgThA4dJQdJ",
"kind": "winback",
"key": "default",
"name": "Win-back flow",
"status": "draft",
"holdout_percent": 5,
"test_version": null,
"live_version": null,
"unpublished_changes": [],
"mode_status": {
"livemode": false,
"state": "draft",
"version": null,
"published_at": null,
"unpublished_changes": []
},
"draft_updated_at": "2026-11-04T10:00:00.000Z",
"paused_at": null,
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z"
}
],
"has_more": false,
"next_cursor": null
} Upload a logo for the cancel modal (admin+)
PNG, JPEG or WebP, 1 MB and 4000 px per side at most; the type is read from the file's bytes, not its name (SVG is refused: it can carry script). Stored by NoChurn (S3 in production). Returns the logo and its public url, served without auth at an unguessable path: put it in draft.design.logo_url (PATCH /v1/flows/cancel) and save the draft — the upload alone does not change the draft. Resource flows (write).
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
-
cancel, or the cancel flow's flw_…
BODY PARAMETERSmultipart/form-data
-
PNG, JPEG or WebP, at most 1 MB.
RETURNS
-
201stored, with cancel_flow.logo_uploaded — a CancelFlowLogo
ERRORS
-
400invalid_request another flow than the cancel flow -
413logo_too_large over 1 MB -
415logo_unsupported not a PNG, JPEG or WebP (read from the bytes): an SVG named logo.png
curl https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/logos \
-H "Authorization: Bearer nc_test_…" \
-F file=@logo.pngimport { openAsBlob } from "node:fs";
const form = new FormData();
form.append("file", await openAsBlob("logo.png"), "logo.png");
const res = await fetch("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/logos", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
},
body: form,
});
const cancelFlowLogo = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/logos",
headers={
"Authorization": "Bearer nc_test_…",
},
files={"file": open("logo.png", "rb")},
)
cancel_flow_logo = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/logos")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req.set_form([["file", File.open("logo.png")]], "multipart/form-data")
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
cancel_flow_logo = JSON.parse(res.body) {
"object": "cancel_flow_logo",
"id": "clg_a64GDD84khnhiTj2",
"flow": "flw_xYvYtMQxrYYHDSbn",
"url": "https://api.nochurn.sh/assets/logos/2yfX8g5SnJZbznKJCmpExdYFiSEyrQ6t.png",
"content_type": "image/png",
"byte_size": 45,
"width": 240,
"height": 80,
"created_at": "2026-11-04T10:00:00.000Z"
} Remove a logo (admin+)
Takes it out of the draft at once (design.logo_url becomes null when it was this logo). The file is deleted once no published version, segment or A/B variant uses it, so a running modal never loses its logo.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
-
clg_…
RETURNS
-
200removed, with cancel_flow.logo_removed; the flow with its draft — the Flow object
ERRORS
-
404not_found another account's logo
curl https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/logos/{logo_id} \
-X DELETE \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/logos/{logo_id}", {
method: "DELETE",
headers: {
Authorization: "Bearer nc_test_…",
},
});
const flow = await res.json();import requests
res = requests.delete(
"https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/logos/{logo_id}",
headers={
"Authorization": "Bearer nc_test_…",
},
)
flow = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/logos/{logo_id}")
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) }
flow = JSON.parse(res.body) {
"object": "flow",
"id": "flw_bv4fLvDSqHLBcmKP",
"kind": "cancel",
"key": "default",
"name": "Cancel flow",
"status": "draft",
"holdout_percent": 5,
"test_version": null,
"live_version": null,
"unpublished_changes": ["design"],
"mode_status": {
"livemode": false,
"state": "draft",
"version": null,
"published_at": null,
"unpublished_changes": ["design"]
},
"draft_updated_at": "2026-11-04T10:00:00.000Z",
"paused_at": null,
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z",
"draft": {
"pause": {
"inapp": "none"
},
"steps": ["reason", "offer", "feedback", "confirmation"],
"texts": {},
"design": {
"preset": "light",
"logo_url": null,
"css_report": {
"errors": [],
"cancel_button_check": "ok"
},
"custom_css": ""
},
"reasons": [
{
"key": "too_expensive",
"labels": {
"de": "Es ist zu teuer",
"en": "It's too expensive",
"fr": "C'est trop cher",
"it": "È troppo caro",
"pt": "É demasiado caro"
},
"offer_id": null
},
{
"key": "missing_feature",
"labels": {
"de": "Eine Funktion fehlt",
"en": "A feature is missing",
"fr": "Il manque une fonctionnalité",
"it": "Manca una funzionalità",
"pt": "Falta uma funcionalidade"
},
"offer_id": null
},
{
"key": "not_using",
"labels": {
"de": "Ich nutze es zu wenig",
"en": "I don't use it enough",
"fr": "Je ne l'utilise pas assez",
"it": "Non lo uso abbastanza",
"pt": "Não o uso o suficiente"
},
"offer_id": null
},
{
"key": "switching",
"labels": {
"de": "Ich wechsle zu einem anderen Tool",
"en": "I'm switching to another tool",
"fr": "Je passe à un autre outil",
"it": "Passo a un altro strumento",
"pt": "Vou mudar para outra ferramenta"
},
"offer_id": null
},
{
"key": "temporary",
"labels": {
"de": "Ich brauche nur eine Pause",
"en": "I only need a break",
"fr": "J'ai juste besoin d'une pause",
"it": "Ho solo bisogno di una pausa",
"pt": "Só preciso de uma pausa"
},
"offer_id": null
},
{
"key": "project_finished",
"labels": {
"de": "Mein Projekt ist abgeschlossen",
"en": "My project is finished",
"fr": "Mon projet est terminé",
"it": "Il mio progetto è finito",
"pt": "O meu projeto terminou"
},
"offer_id": null
},
{
"key": "other",
"labels": {
"de": "Etwas anderes",
"en": "Something else",
"fr": "Autre chose",
"it": "Altro",
"pt": "Outra coisa"
},
"offer_id": null,
"free_text": true
}
],
"feedback": {
"enabled": true,
"required": false
},
"cancel_mode": "at_period_end",
"review_requests": {
"enabled": false,
"happy_reasons": ["project_finished", "too_expensive", "temporary"],
"follow_up_email_days": 2
},
"offers_once_per_customer_per_year": true
},
"logos": []
} KPIs, funnel and holdout of a flow
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
-
flw_… or the kind (prevention, recovery, cancel, winback).
QUERY PARAMETERS
-
- month
- 30d
- 90d
- 12m
RETURNS
-
200the recovery flow this month — a FlowStats
ERRORS
-
400invalid_request unknown period
curl https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/stats \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/stats", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const flowStats = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/stats",
headers={
"Authorization": "Bearer nc_test_…",
},
)
flow_stats = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/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) }
flow_stats = JSON.parse(res.body) {
"object": "flow_stats",
"flow": "flw_JgjjWhZqyvzdjbiw",
"kind": "recovery",
"livemode": false,
"period": {
"key": "month",
"from": "2026-11-01T00:00:00+01:00",
"to": "2026-11-04T11:00:00+01:00"
},
"kpis": [
{
"key": "revenue_recovered",
"label": "Revenue recovered",
"value": 4900,
"unit": "money",
"sample_size": 1,
"pending": 0,
"currency": "eur",
"breakdown": [
{
"currency": "eur",
"amount_cents": 4900,
"converted_cents": 4900,
"unconverted_cents": 0
}
]
},
{
"key": "recovery_rate",
"label": "Recovery rate",
"value": 1,
"unit": "rate",
"sample_size": 1
},
{
"key": "in_progress",
"label": "In progress",
"value": 1,
"unit": "count",
"sample_size": 1
},
{
"key": "median_days_to_recover",
"label": "Median time to recover",
"value": 0,
"unit": "days",
"sample_size": 1
}
],
"funnel": [
{
"key": "payment_failed",
"label": "Payment failed",
"count": 2,
"share": 1
},
{
"key": "smart_retry",
"label": "Smart retry",
"count": 1,
"share": 0.5
},
{
"key": "ai_email",
"label": "AI email",
"count": 0,
"share": 0
},
{
"key": "card_page",
"label": "Card update page",
"count": 0,
"share": 0
},
{
"key": "recovered",
"label": "Recovered",
"count": 1,
"share": 0.5
}
],
"holdout": {
"percent": 0,
"runs": 0,
"natural_rate": null,
"treated_rate": 0,
"lift": null,
"sample_size": 2
}
} Preview an email of a flow
Renders one email of the flow's draft for one language, from your text (saved, or draft_content being typed), the AI or NoChurn's template, with an example customer (fictional data) or one of your customers (customer_id, in the key's mode). Sends nothing, stores nothing. source: ai shows the last email the AI wrote for this step, or the template; generate: true asks the AI for a sample (20 a day). A restricted key needs flows:read.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
-
flw_… or the kind of the default flow
BODY PARAMETERS
-
email_1…, card_removed, card_expiring, renewal_reminder, d30, d60, d90, review_follow_up
- language string
Default: the flow's first language.
- source enum
Default: what the customer would get.
- custom
- ai
- template
- draft_content object
Your text being typed, not saved yet.
- customer_id string
cst_…, cus_… or your id: fill the placeholders with this customer's data.
- generate boolean
With source ai: ask the AI for a sample now.
RETURNS
-
200the email, rendered — a EmailPreview
ERRORS
-
400invalid_request unknown step
curl https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/email_preview \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"step": "email_1",
"language": "fr",
"source": "template"
}'const res = await fetch("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/email_preview", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"step": "email_1",
"language": "fr",
"source": "template"
}),
});
const emailPreview = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/email_preview",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"step": "email_1",
"language": "fr",
"source": "template"
},
)
email_preview = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/email_preview")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"step" => "email_1",
"language" => "fr",
"source" => "template"
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
email_preview = JSON.parse(res.body) {
"object": "email_preview",
"step": "email_1",
"label": "Email 1 · day 0",
"language": "fr",
"source": "template",
"subject": "Votre paiement n'est pas passé",
"text": "Bonjour Alex,\n\nVotre dernier paiement de €49.00 pour Acme n'est pas passé. Cela arrive souvent quand une carte a expiré ou a été remplacée.\n\nVous pouvez mettre à jour votre moyen de paiement en une minute : https://pay.nochurn.sh/example\n\nMerci,\nAcme",
"html": "<p>Bonjour Alex,</p><p>Votre dernier paiement de €49.00 pour Acme n'est pas passé. Cela arrive souvent quand une carte a expiré ou a été remplacée.</p><p>Vous pouvez mettre à jour votre moyen de paiement en une minute : <a href=\"https://pay.nochurn.sh/example\">https://pay.nochurn.sh/example</a></p><p>Merci,<br>Acme</p>",
"ai_generated": false,
"review": {
"state": "auto_approved",
"issues": []
},
"warnings": ["Links are examples: each customer gets their own link"],
"customer": {
"id": null,
"name": "Example customer",
"example": true
},
"sample": true,
"template": {
"subject": "Votre paiement n'est pas passé",
"body": "Bonjour {{first_name}},\n\nVotre dernier paiement de {{amount}} pour {{company}} n'est pas passé. Cela arrive souvent quand une carte a expiré ou a été remplacée.\n\nVous pouvez mettre à jour votre moyen de paiement en une minute : {{card_link}}\n\nMerci,\n{{sender_name}}"
},
"custom": null,
"placeholders": [
"first_name",
"amount",
"company",
"card_link",
"sender_name"
],
"required_placeholders": ["card_link"],
"languages": ["en", "fr", "de", "it", "pt"],
"sources": ["custom", "ai", "template"]
} Slack messages and Intercom notes sent when a flow acts
In the current mode. Slack: a message in a channel. Intercom: an internal note on the customer's contact (found by email), never a message to the customer. Each event is sent once per destination, retried with backoff and logged (Logs › webhook).
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
RETURNS
-
200the settings — a FlowNotifications
curl https://api.nochurn.sh/v1/flow_notifications \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/flow_notifications", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const flowNotifications = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/flow_notifications",
headers={
"Authorization": "Bearer nc_test_…",
},
)
flow_notifications = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flow_notifications")
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) }
flow_notifications = JSON.parse(res.body) {
"object": "flow_notifications",
"livemode": false,
"events": [
{
"type": "recovery.opened",
"flow": "recovery"
},
{
"type": "recovery.recovered",
"flow": "recovery"
},
{
"type": "recovery.lost",
"flow": "recovery"
},
{
"type": "cancel_session.offer_accepted",
"flow": "cancel"
},
{
"type": "cancel_session.completed",
"flow": "cancel"
},
{
"type": "customer.flagged",
"flow": "prevention"
},
{
"type": "winback.reactivated",
"flow": "winback"
}
],
"slack": {
"kind": "slack",
"connected": true,
"integration_status": "active",
"workspace": "Globex",
"events": [],
"channel": "#alerts",
"notes_access": null,
"last_delivery_at": null,
"last_error": null
},
"intercom": {
"kind": "intercom_note",
"connected": false,
"integration_status": null,
"workspace": null,
"events": [],
"channel": null,
"notes_access": null,
"last_delivery_at": null,
"last_error": null
}
} Choose the events sent to Slack (and its channel) or added as Intercom notes
Admins only. events: [] turns the destination off. The integration must be connected to pick events.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
BODY PARAMETERS
- events array of enums
- recovery.opened
- recovery.recovered
- recovery.lost
- cancel_session.offer_accepted
- cancel_session.completed
- customer.flagged
- winback.reactivated
- channel string
Slack only: #channel or a channel id. A private channel needs the NoChurn app invited.
RETURNS
-
200saved — a FlowNotifications
ERRORS
-
409conflict not connected -
422invalid_request an event that is not notified
curl https://api.nochurn.sh/v1/flow_notifications/{kind} \
-X PATCH \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"events": ["cancel_session.completed", "recovery.recovered"],
"channel": "#retention"
}'const res = await fetch("https://api.nochurn.sh/v1/flow_notifications/{kind}", {
method: "PATCH",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"events": ["cancel_session.completed", "recovery.recovered"],
"channel": "#retention"
}),
});
const flowNotifications = await res.json();import requests
res = requests.patch(
"https://api.nochurn.sh/v1/flow_notifications/{kind}",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"events": ["cancel_session.completed", "recovery.recovered"],
"channel": "#retention"
},
)
flow_notifications = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flow_notifications/{kind}")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"events" => ["cancel_session.completed", "recovery.recovered"],
"channel" => "#retention"
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
flow_notifications = JSON.parse(res.body) {
"object": "flow_notifications",
"livemode": false,
"events": [
{
"type": "recovery.opened",
"flow": "recovery"
},
{
"type": "recovery.recovered",
"flow": "recovery"
},
{
"type": "recovery.lost",
"flow": "recovery"
},
{
"type": "cancel_session.offer_accepted",
"flow": "cancel"
},
{
"type": "cancel_session.completed",
"flow": "cancel"
},
{
"type": "customer.flagged",
"flow": "prevention"
},
{
"type": "winback.reactivated",
"flow": "winback"
}
],
"slack": {
"kind": "slack",
"connected": true,
"integration_status": "active",
"workspace": "Globex",
"events": ["cancel_session.completed", "recovery.recovered"],
"channel": "#retention",
"notes_access": null,
"last_delivery_at": null,
"last_error": null
},
"intercom": {
"kind": "intercom_note",
"connected": false,
"integration_status": null,
"workspace": null,
"events": [],
"channel": null,
"notes_access": null,
"last_delivery_at": null,
"last_error": null
}
} Send a test message to the Slack channel now
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
RETURNS
-
201posted — a WebhookDeliveryDetail
curl https://api.nochurn.sh/v1/flow_notifications/{kind}/test \
-X POST \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/flow_notifications/{kind}/test", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
},
});
const webhookDelivery = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/flow_notifications/{kind}/test",
headers={
"Authorization": "Bearer nc_test_…",
},
)
webhook_delivery = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flow_notifications/{kind}/test")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
webhook_delivery = JSON.parse(res.body) {
"object": "webhook_delivery",
"id": "whd_4z6TAYS8bp3otSoZ",
"livemode": false,
"endpoint": "whk_Px7rFYqisAcTaDdj",
"destination": "slack",
"event": null,
"type": "test",
"status": "delivered",
"attempts": 1,
"next_attempt_at": null,
"response_status": 200,
"response_ms": null,
"error": null,
"delivered_at": "2026-11-04T10:00:00Z",
"failed_at": null,
"replay_of": null,
"created_at": "2026-11-04T10:00:00Z",
"payload": {
"id": "nevt_test_f6a8e4b5b4ce",
"data": {},
"type": "test",
"object": "event",
"created": 1793786400,
"livemode": false
},
"response_excerpt": "1.2"
} Publish the draft to test or live mode
to: test is "Try in test mode"; to: live is "Publish" (both modes then run this version).
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth):
configurescope, test mode only.
PATH PARAMETERS
BODY PARAMETERS
-
- test
- live
RETURNS
-
200published — the Flow object
curl https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/publish \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"to": "live"
}'const res = await fetch("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/publish", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"to": "live"
}),
});
const flow = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/publish",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"to": "live"
},
)
flow = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/publish")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"to" => "live"
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
flow = JSON.parse(res.body) {
"object": "flow",
"id": "flw_wrgKan8xHjAdgH4k",
"kind": "cancel",
"key": "default",
"name": "Cancel flow",
"status": "live",
"holdout_percent": 5,
"test_version": {
"id": "flv_hPABNrYmFDsYS7G8",
"number": 1,
"published_at": "2026-11-04T10:00:00.000Z",
"published_to": "live"
},
"live_version": {
"id": "flv_hPABNrYmFDsYS7G8",
"number": 1,
"published_at": "2026-11-04T10:00:00.000Z",
"published_to": "live"
},
"unpublished_changes": [],
"mode_status": {
"livemode": false,
"state": "published",
"version": 1,
"published_at": "2026-11-04T10:00:00.000Z",
"unpublished_changes": []
},
"draft_updated_at": "2026-11-04T10:00:00.000Z",
"paused_at": null,
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z",
"draft": {
"pause": {
"inapp": "none"
},
"steps": ["reason", "offer", "feedback", "confirmation"],
"texts": {},
"design": {
"format": "modal",
"preset": "light",
"tokens": {},
"custom_css": ""
},
"reasons": [
{
"key": "too_expensive",
"labels": {
"de": "Es ist zu teuer",
"en": "It's too expensive",
"fr": "C'est trop cher",
"it": "È troppo caro",
"pt": "É demasiado caro"
},
"offer_id": null
},
{
"key": "missing_feature",
"labels": {
"de": "Eine Funktion fehlt",
"en": "A feature is missing",
"fr": "Il manque une fonctionnalité",
"it": "Manca una funzionalità",
"pt": "Falta uma funcionalidade"
},
"offer_id": null
},
{
"key": "not_using",
"labels": {
"de": "Ich nutze es zu wenig",
"en": "I don't use it enough",
"fr": "Je ne l'utilise pas assez",
"it": "Non lo uso abbastanza",
"pt": "Não o uso o suficiente"
},
"offer_id": null
},
{
"key": "switching",
"labels": {
"de": "Ich wechsle zu einem anderen Tool",
"en": "I'm switching to another tool",
"fr": "Je passe à un autre outil",
"it": "Passo a un altro strumento",
"pt": "Vou mudar para outra ferramenta"
},
"offer_id": null
},
{
"key": "temporary",
"labels": {
"de": "Ich brauche nur eine Pause",
"en": "I only need a break",
"fr": "J'ai juste besoin d'une pause",
"it": "Ho solo bisogno di una pausa",
"pt": "Só preciso de uma pausa"
},
"offer_id": null
},
{
"key": "project_finished",
"labels": {
"de": "Mein Projekt ist abgeschlossen",
"en": "My project is finished",
"fr": "Mon projet est terminé",
"it": "Il mio progetto è finito",
"pt": "O meu projeto terminou"
},
"offer_id": null
},
{
"key": "other",
"labels": {
"de": "Etwas anderes",
"en": "Something else",
"fr": "Autre chose",
"it": "Altro",
"pt": "Outra coisa"
},
"offer_id": null,
"free_text": true
}
],
"feedback": {
"enabled": true,
"required": false
},
"cancel_mode": "at_period_end",
"review_requests": {
"enabled": false,
"happy_reasons": ["project_finished", "too_expensive", "temporary"],
"follow_up_email_days": 2
},
"offers_once_per_customer_per_year": true
},
"logos": []
} Discard the unpublished changes
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth):
configurescope, test mode only.
PATH PARAMETERS
RETURNS
-
200draft back to the last version — the Flow object
curl https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/discard \
-X POST \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/discard", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
},
});
const flow = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/discard",
headers={
"Authorization": "Bearer nc_test_…",
},
)
flow = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/discard")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
flow = JSON.parse(res.body) {
"object": "flow",
"id": "flw_qUMZ9KGuvjKnxmww",
"kind": "recovery",
"key": "default",
"name": "Recovery flow",
"status": "draft",
"holdout_percent": 0,
"test_version": null,
"live_version": null,
"unpublished_changes": [],
"mode_status": {
"livemode": false,
"state": "draft",
"version": null,
"published_at": null,
"unpublished_changes": []
},
"draft_updated_at": "2026-11-04T10:00:00.000Z",
"paused_at": null,
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z",
"draft": {
"retries": {
"rules": [
{
"decline_code": "insufficient_funds",
"attempts": 4,
"timing": "after_payday"
},
{
"decline_code": "expired_card",
"attempts": 0,
"email_now": true
},
{
"decline_code": "do_not_honor",
"attempts": 3,
"days": [3, 5, 7]
},
{
"decline_code": "fraudulent",
"attempts": 0,
"stop": true,
"email": false
}
],
"max_days": 14,
"then": "cancel_at_period_end_and_winback"
},
"emails": [
{
"day": 0
},
{
"day": 4
},
{
"day": 10
}
],
"email_settings": {
"ai_writes": true,
"tone": "friendly",
"review": "auto_check",
"languages": ["en", "fr", "de", "it", "pt"]
},
"card_update_page": {
"link_valid_days": 14,
"pay_open_invoice": true,
"methods": ["card", "apple_pay", "google_pay", "sepa_debit"]
},
"inapp": {
"banner": {
"enabled": true,
"audience": "admins",
"dismiss_hours": 24
},
"wall": {
"enabled": true,
"after_days": 7
},
"annual_grace_days": 14
},
"send_window": {
"audience": null,
"respect_customer_timezone": null,
"quiet_hours": null
}
},
"logos": []
} List published versions, newest first
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
RETURNS
-
200versions — a list of FlowVersion (FlowVersionList)
curl https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/versions \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/versions", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const page = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/versions",
headers={
"Authorization": "Bearer nc_test_…",
},
)
page = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/versions")
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) {
"object": "list",
"data": [
{
"object": "flow_version",
"id": "flv_uLhBgP6hNvvgKQbm",
"flow": "flw_Wwa1oeVCBHng5M6m",
"number": 2,
"config": {
"inapp": {
"wall": {
"enabled": true,
"after_days": 7
},
"banner": {
"enabled": true,
"audience": "admins",
"dismiss_hours": 24
},
"annual_grace_days": 14
},
"emails": [
{
"day": 0
},
{
"day": 4
},
{
"day": 10
}
],
"retries": {
"then": "cancel_at_period_end_and_winback",
"rules": [
{
"timing": "after_payday",
"attempts": 4,
"decline_code": "insufficient_funds"
},
{
"attempts": 0,
"email_now": true,
"decline_code": "expired_card"
},
{
"days": [3, 5, 7],
"attempts": 3,
"decline_code": "do_not_honor"
},
{
"stop": true,
"email": false,
"attempts": 0,
"decline_code": "fraudulent"
}
],
"max_days": 14
},
"send_window": {
"audience": null,
"quiet_hours": null,
"respect_customer_timezone": null
},
"email_settings": {
"tone": "friendly",
"review": "auto_check",
"ai_writes": true,
"languages": ["en", "fr", "de", "it", "pt"]
},
"card_update_page": {
"methods": ["card", "apple_pay", "google_pay", "sepa_debit"],
"link_valid_days": 14,
"pay_open_invoice": true
}
},
"holdout_percent": 0,
"published_to": "test",
"published_by": "system",
"published_at": "2026-11-04T10:00:00.000Z"
},
{
"object": "flow_version",
"id": "flv_VGm8keAZNkfA8TPw",
"flow": "flw_Wwa1oeVCBHng5M6m",
"number": 1,
"config": {
"inapp": {
"wall": {
"enabled": true,
"after_days": 7
},
"banner": {
"enabled": true,
"audience": "admins",
"dismiss_hours": 24
},
"annual_grace_days": 14
},
"emails": [
{
"day": 0
},
{
"day": 4
},
{
"day": 10
}
],
"retries": {
"then": "cancel_at_period_end_and_winback",
"rules": [
{
"timing": "after_payday",
"attempts": 4,
"decline_code": "insufficient_funds"
},
{
"attempts": 0,
"email_now": true,
"decline_code": "expired_card"
},
{
"days": [3, 5, 7],
"attempts": 3,
"decline_code": "do_not_honor"
},
{
"stop": true,
"email": false,
"attempts": 0,
"decline_code": "fraudulent"
}
],
"max_days": 14
},
"send_window": {
"audience": null,
"quiet_hours": null,
"respect_customer_timezone": null
},
"email_settings": {
"tone": "friendly",
"review": "auto_check",
"ai_writes": true,
"languages": ["en", "fr", "de", "it", "pt"]
},
"card_update_page": {
"methods": ["card", "apple_pay", "google_pay", "sepa_debit"],
"link_valid_days": 14,
"pay_open_invoice": true
}
},
"holdout_percent": 0,
"published_to": "test",
"published_by": "system",
"published_at": "2026-11-04T10:00:00.000Z"
}
],
"has_more": false,
"next_cursor": null
} Put an older version back in the draft
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth):
configurescope, test mode only.
PATH PARAMETERS
RETURNS
-
200restored in the draft (publish to apply) — the Flow object
curl https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/versions/{number}/restore \
-X POST \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/versions/{number}/restore", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
},
});
const flow = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/versions/{number}/restore",
headers={
"Authorization": "Bearer nc_test_…",
},
)
flow = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/versions/{number}/restore")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
flow = JSON.parse(res.body) {
"object": "flow",
"id": "flw_bHzwVnLVo1KxDxu6",
"kind": "recovery",
"key": "default",
"name": "Recovery flow",
"status": "test",
"holdout_percent": 0,
"test_version": {
"id": "flv_ymeANEcX3A1TunN7",
"number": 2,
"published_at": "2026-11-04T10:00:00.000Z",
"published_to": "test"
},
"live_version": null,
"unpublished_changes": ["retries"],
"mode_status": {
"livemode": false,
"state": "published",
"version": 2,
"published_at": "2026-11-04T10:00:00.000Z",
"unpublished_changes": ["retries"]
},
"draft_updated_at": "2026-11-04T10:00:00.000Z",
"paused_at": null,
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z",
"draft": {
"inapp": {
"wall": {
"enabled": true,
"after_days": 7
},
"banner": {
"enabled": true,
"audience": "admins",
"dismiss_hours": 24
},
"annual_grace_days": 14
},
"emails": [
{
"day": 0
},
{
"day": 4
},
{
"day": 10
}
],
"retries": {
"then": "cancel_at_period_end_and_winback",
"rules": [
{
"timing": "after_payday",
"attempts": 4,
"decline_code": "insufficient_funds"
},
{
"attempts": 0,
"email_now": true,
"decline_code": "expired_card"
},
{
"days": [3, 5, 7],
"attempts": 3,
"decline_code": "do_not_honor"
},
{
"stop": true,
"email": false,
"attempts": 0,
"decline_code": "fraudulent"
}
],
"max_days": 14
},
"send_window": {
"audience": null,
"quiet_hours": null,
"respect_customer_timezone": null
},
"email_settings": {
"tone": "friendly",
"review": "auto_check",
"ai_writes": true,
"languages": ["en", "fr", "de", "it", "pt"]
},
"card_update_page": {
"methods": ["card", "apple_pay", "google_pay", "sepa_debit"],
"link_valid_days": 14,
"pay_open_invoice": true
}
},
"logos": []
} Pause a flow (no new run)
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
RETURNS
-
200paused — the Flow object
ERRORS
-
403forbidden restricted key with flows:read only
curl https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/pause \
-X POST \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/pause", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
},
});
const flow = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/pause",
headers={
"Authorization": "Bearer nc_test_…",
},
)
flow = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/pause")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
flow = JSON.parse(res.body) {
"object": "flow",
"id": "flw_owZ77WgHmyWTS5hn",
"kind": "winback",
"key": "default",
"name": "Win-back flow",
"status": "paused",
"holdout_percent": 5,
"test_version": null,
"live_version": null,
"unpublished_changes": [],
"mode_status": {
"livemode": false,
"state": "paused",
"version": null,
"published_at": null,
"unpublished_changes": []
},
"draft_updated_at": "2026-11-04T10:00:00.000Z",
"paused_at": "2026-11-04T10:00:00.000Z",
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z",
"draft": {
"offer": {
"label": "1 month free",
"promo_code": null
},
"stop_on": ["reply", "resubscribe", "unsubscribe"],
"sequence": [
{
"day": 30,
"kind": "d30"
},
{
"day": 60,
"kind": "d60"
},
{
"day": 90,
"kind": "d90"
}
],
"never_after": ["refund", "fraud", "gdpr_erasure"],
"send_window": {
"audience": null,
"quiet_hours": null,
"respect_customer_timezone": null
},
"resubscribe_url": null,
"max_churn_age_days": 7
},
"logos": []
} Resume a paused flow
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
RETURNS
-
200resumed — the Flow object
curl https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/resume \
-X POST \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/resume", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
},
});
const flow = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/resume",
headers={
"Authorization": "Bearer nc_test_…",
},
)
flow = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/resume")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
flow = JSON.parse(res.body) {
"object": "flow",
"id": "flw_RnhboVHZYcVRnCsL",
"kind": "winback",
"key": "default",
"name": "Win-back flow",
"status": "draft",
"holdout_percent": 5,
"test_version": null,
"live_version": null,
"unpublished_changes": [],
"mode_status": {
"livemode": false,
"state": "draft",
"version": null,
"published_at": null,
"unpublished_changes": []
},
"draft_updated_at": "2026-11-04T10:00:00.000Z",
"paused_at": null,
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z",
"draft": {
"offer": {
"label": "1 month free",
"promo_code": null
},
"stop_on": ["reply", "resubscribe", "unsubscribe"],
"sequence": [
{
"day": 30,
"kind": "d30"
},
{
"day": 60,
"kind": "d60"
},
{
"day": 90,
"kind": "d90"
}
],
"never_after": ["refund", "fraud", "gdpr_erasure"],
"send_window": {
"audience": null,
"quiet_hours": null,
"respect_customer_timezone": null
},
"resubscribe_url": null,
"max_churn_age_days": 7
},
"logos": []
} Customers in this flow (current mode)
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
QUERY PARAMETERS
- state enum
- active
- succeeded
- failed
- stopped
- starting_after string
RETURNS
-
200runs of the mode, with their customer — a list of FlowRun (FlowRunList)
curl https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/runs \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/runs", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const page = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/runs",
headers={
"Authorization": "Bearer nc_test_…",
},
)
page = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/runs")
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) {
"object": "list",
"data": [
{
"object": "flow_run",
"id": "run_DDiKSSzFHtYhREuC",
"livemode": false,
"flow": "flw_V7HicHjaunR9yXum",
"version": 1,
"customer": {
"id": "cst_77ZMsUw9nrd8gjCi",
"name": "Lena",
"email": "customer1@acme-corp.dev",
"mrr_cents": 0,
"currency": null
},
"subject": null,
"holdout": false,
"state": "active",
"current_step": "entered",
"outcome": null,
"outcome_at": null,
"outcome_mrr_cents": null,
"outcome_cents": null,
"currency": null,
"confirmation": null,
"stop_reason": null,
"started_at": "2026-11-04T10:00:00.000Z",
"ended_at": null
}
],
"has_more": false,
"next_cursor": null
} Send a test email to yourself
Dashboard only: emails the signed-in user, marked [TEST], with sample data, one email of the saved draft as a customer would get it (your text for the language, else the AI, else NoChurn's template). 20 a day.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
-
flw_… or recovery
HEADER PARAMETERS
- X-Requested-With string
BODY PARAMETERS
-
The email (as in the email preview: email_1…, card_removed, d30, review_follow_up…); a recovery email also by its index (0, 1, 2).
- language string
Default: the account's language when the flow writes in it.
RETURNS
-
201sent to the signed-in user — a FlowTestEmail
ERRORS
-
403forbidden with an API key
curl https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/send_test \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"step": 1
}'const res = await fetch("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/send_test", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"step": 1
}),
});
const flowTestEmail = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/send_test",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"step": 1
},
)
flow_test_email = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/flows/flw_1K3ms53XtrQohYHb/send_test")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"step" => 1
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
flow_test_email = JSON.parse(res.body) {
"object": "flow_test_email",
"to": "user1@example.test",
"subject": "[TEST] Quick reminder about your payment",
"body": "Hi User,\n\nThe last payment of €49.00 for Acme didn't go through. You can update your card in one minute here: https://pay.nochurn.sh/example\n\nThanks,\nAcme",
"ai_generated": true,
"source": "ai",
"review": {
"state": "auto_approved",
"issues": []
}
} Fields a segment can use
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
RETURNS
-
200by source (stripe, nochurn, custom) — a list of SegmentField (SegmentFieldList)
curl https://api.nochurn.sh/v1/segment_fields \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/segment_fields", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const page = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/segment_fields",
headers={
"Authorization": "Bearer nc_test_…",
},
)
page = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/segment_fields")
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) {
"object": "list",
"has_more": false,
"next_cursor": null,
"data": [
{
"source": "nochurn",
"field": "churn_score",
"type": "number",
"label": "Churn score (0–100)",
"ops": ["eq", "neq", "gt", "gte", "lt", "lte", "exists", "not_exists"]
},
{
"source": "nochurn",
"field": "churn_level",
"type": "string",
"label": "Churn level",
"ops": ["eq", "neq", "in", "not_in", "contains", "exists", "not_exists"]
},
{
"source": "nochurn",
"field": "state",
"type": "string",
"label": "Billing state",
"ops": ["eq", "neq", "in", "not_in", "contains", "exists", "not_exists"]
},
{
"source": "nochurn",
"field": "country",
"type": "string",
"label": "Country",
"ops": ["eq", "neq", "in", "not_in", "contains", "exists", "not_exists"]
},
{
"source": "nochurn",
"field": "locale",
"type": "string",
"label": "Language",
"ops": ["eq", "neq", "in", "not_in", "contains", "exists", "not_exists"]
},
{
"source": "nochurn",
"field": "tenure_days",
"type": "number",
"label": "Days as a customer",
"ops": ["eq", "neq", "gt", "gte", "lt", "lte", "exists", "not_exists"]
},
{
"source": "nochurn",
"field": "flagged",
"type": "boolean",
"label": "Early signal (FLAGGED)",
"ops": ["eq", "exists", "not_exists"]
},
{
"source": "nochurn",
"field": "mrr_cents",
"type": "number",
"label": "MRR (minor unit)",
"ops": ["eq", "neq", "gt", "gte", "lt", "lte", "exists", "not_exists"]
},
{
"source": "nochurn",
"field": "support_contacts",
"type": "string",
"label": "Support conversations, last 30 days (0, 1–2, 3+)",
"ops": ["eq", "neq", "in", "not_in", "contains", "exists", "not_exists"]
},
{
"source": "stripe",
"field": "plan",
"type": "string",
"label": "Plan",
"ops": ["eq", "neq", "in", "not_in", "contains", "exists", "not_exists"]
},
{
"source": "stripe",
"field": "interval",
"type": "string",
"label": "Billing interval",
"ops": ["eq", "neq", "in", "not_in", "contains", "exists", "not_exists"]
},
{
"source": "stripe",
"field": "currency",
"type": "string",
"label": "Currency",
"ops": ["eq", "neq", "in", "not_in", "contains", "exists", "not_exists"]
},
{
"source": "stripe",
"field": "payment_method",
"type": "string",
"label": "Payment method category",
"ops": ["eq", "neq", "in", "not_in", "contains", "exists", "not_exists"]
},
{
"source": "stripe",
"field": "card_country",
"type": "string",
"label": "Card country",
"ops": ["eq", "neq", "in", "not_in", "contains", "exists", "not_exists"]
},
{
"source": "stripe",
"field": "card_funding",
"type": "string",
"label": "Card funding",
"ops": ["eq", "neq", "in", "not_in", "contains", "exists", "not_exists"]
}
]
} Segments of a flow
In match order: the first segment whose conditions match wins; Everyone else is always last.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
QUERY PARAMETERS
-
flw_… or a kind.
RETURNS
-
200with Everyone else — a list of Segment (SegmentList)
curl "https://api.nochurn.sh/v1/segments?flow=…" \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/segments?flow=…", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const page = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/segments?flow=…",
headers={
"Authorization": "Bearer nc_test_…",
},
)
page = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/segments?flow=…")
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) {
"object": "list",
"has_more": false,
"next_cursor": null,
"data": [
{
"object": "segment",
"id": "seg_H45K9royPTDptPfa",
"flow": "flw_adUax5LQBJ5bnTPF",
"name": "Everyone else",
"position": 1000000,
"is_default": true,
"conditions": {
"any": []
},
"overrides": {},
"customers_count": null,
"mrr_cents": null,
"counted_at": null,
"created_at": "2026-11-04T10:00:00Z"
}
]
} Create a segment
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
QUERY PARAMETERS
-
flw_… or a kind.
BODY PARAMETERS
- conditions object
{ any: [ { all: [ { source, field, op, value } ] } ] }
- overrides object
THEN: patch of the flow config for this segment. A null in send_window means follow the flow (not the account): it is not stored.
RETURNS
-
201created before Everyone else — a Segment
ERRORS
-
422invalid_request unknown field
curl "https://api.nochurn.sh/v1/segments?flow=…" \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"name": "High risk",
"conditions": {
"any": [
{
"all": [
{
"source": "nochurn",
"field": "churn_score",
"op": "gte",
"value": 70
}
]
}
]
},
"overrides": {
"cancel_mode": "immediately"
}
}'const res = await fetch("https://api.nochurn.sh/v1/segments?flow=…", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "High risk",
"conditions": {
"any": [
{
"all": [
{
"source": "nochurn",
"field": "churn_score",
"op": "gte",
"value": 70
}
]
}
]
},
"overrides": {
"cancel_mode": "immediately"
}
}),
});
const segment = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/segments?flow=…",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"name": "High risk",
"conditions": {
"any": [
{
"all": [
{
"source": "nochurn",
"field": "churn_score",
"op": "gte",
"value": 70
}
]
}
]
},
"overrides": {
"cancel_mode": "immediately"
}
},
)
segment = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/segments?flow=…")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"name" => "High risk",
"conditions" => {
"any" => [
{
"all" => [
{
"source" => "nochurn",
"field" => "churn_score",
"op" => "gte",
"value" => 70
}
]
}
]
},
"overrides" => {
"cancel_mode" => "immediately"
}
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
segment = JSON.parse(res.body) {
"object": "segment",
"id": "seg_oAiWGSuQoPvjdK6X",
"flow": "flw_CE1Qp1CyPvsjbJXE",
"name": "High risk",
"position": 0,
"is_default": false,
"conditions": {
"any": [
{
"all": [
{
"source": "nochurn",
"field": "churn_score",
"op": "gte",
"value": 70
}
]
}
]
},
"overrides": {
"cancel_mode": "immediately"
},
"customers_count": null,
"mrr_cents": null,
"counted_at": null,
"created_at": "2026-11-04T10:00:00Z"
} Edit a segment
Overrides follow the flow draft's rules: known sections only; the cancel flow's texts and CSS are normalized. A null in send_window means follow the flow (not the account): it is not stored.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
BODY PARAMETERS
- name string
- conditions object
- overrides object
RETURNS
-
200a null in send_window follows the flow: not stored — a Segment
ERRORS
-
400invalid_request unknown override section
curl https://api.nochurn.sh/v1/segments/seg_xZtZrJpMirXY6Z62 \
-X PATCH \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"overrides": {
"cancel_mode": "immediately"
}
}'const res = await fetch("https://api.nochurn.sh/v1/segments/seg_xZtZrJpMirXY6Z62", {
method: "PATCH",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"overrides": {
"cancel_mode": "immediately"
}
}),
});
const segment = await res.json();import requests
res = requests.patch(
"https://api.nochurn.sh/v1/segments/seg_xZtZrJpMirXY6Z62",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"overrides": {
"cancel_mode": "immediately"
}
},
)
segment = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/segments/seg_xZtZrJpMirXY6Z62")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"overrides" => {
"cancel_mode" => "immediately"
}
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
segment = JSON.parse(res.body) {
"object": "segment",
"id": "seg_xZtZrJpMirXY6Z62",
"flow": "flw_yJRnuy8MJBfpxCpi",
"name": "VIP",
"position": 0,
"is_default": false,
"conditions": {
"any": [
{
"all": [
{
"op": "gte",
"field": "churn_score",
"value": 70,
"source": "nochurn"
}
]
}
]
},
"overrides": {
"cancel_mode": "immediately"
},
"customers_count": null,
"mrr_cents": null,
"counted_at": null,
"created_at": "2026-11-04T10:00:00Z"
} Delete a segment
Its draft A/B tests go with it. A segment whose tests ran keeps their results: it cannot be deleted (409).
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
RETURNS
-
200deleted
ERRORS
-
409conflict it has A/B test results
curl https://api.nochurn.sh/v1/segments/seg_MRdMzGuyEFdBKekA \
-X DELETE \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/segments/seg_MRdMzGuyEFdBKekA", {
method: "DELETE",
headers: {
Authorization: "Bearer nc_test_…",
},
});
const segment = await res.json();import requests
res = requests.delete(
"https://api.nochurn.sh/v1/segments/seg_MRdMzGuyEFdBKekA",
headers={
"Authorization": "Bearer nc_test_…",
},
)
segment = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/segments/seg_MRdMzGuyEFdBKekA")
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) }
segment = JSON.parse(res.body) {
"object": "segment",
"id": "seg_MRdMzGuyEFdBKekA",
"flow": "flw_1BGQe19r5oviKRhH",
"name": "VIP",
"position": 0,
"is_default": false,
"conditions": {
"any": [
{
"all": [
{
"op": "gte",
"field": "churn_score",
"value": 70,
"source": "nochurn"
}
]
}
]
},
"overrides": {},
"customers_count": null,
"mrr_cents": null,
"counted_at": null,
"created_at": "2026-11-04T10:00:00Z",
"deleted": true
} Count the customers of conditions
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
BODY PARAMETERS
RETURNS
-
200live count and MRR — a SegmentPreview
curl https://api.nochurn.sh/v1/segments/preview \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"conditions": {
"any": [
{
"all": [
{
"source": "nochurn",
"field": "churn_score",
"op": "gte",
"value": 70
}
]
}
]
}
}'const res = await fetch("https://api.nochurn.sh/v1/segments/preview", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"conditions": {
"any": [
{
"all": [
{
"source": "nochurn",
"field": "churn_score",
"op": "gte",
"value": 70
}
]
}
]
}
}),
});
const segmentPreview = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/segments/preview",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"conditions": {
"any": [
{
"all": [
{
"source": "nochurn",
"field": "churn_score",
"op": "gte",
"value": 70
}
]
}
]
}
},
)
segment_preview = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/segments/preview")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"conditions" => {
"any" => [
{
"all" => [
{
"source" => "nochurn",
"field" => "churn_score",
"op" => "gte",
"value" => 70
}
]
}
]
}
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
segment_preview = JSON.parse(res.body) {
"object": "segment_preview",
"customers_count": 1,
"mrr_cents": 4900,
"counted_at": "2026-11-04T10:00:00Z"
} A/B tests
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
QUERY PARAMETERS
- flow string
RETURNS
-
200newest first — a list of Experiment (ExperimentList)
curl https://api.nochurn.sh/v1/experiments \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/experiments", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const page = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/experiments",
headers={
"Authorization": "Bearer nc_test_…",
},
)
page = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/experiments")
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) {
"object": "list",
"data": [],
"has_more": false,
"next_cursor": null
} Draft an A/B test
2 to 4 variants (A is the control). The winner is picked automatically when a variant is the best with probability ≥ threshold and every variant has min_sample runs.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
BODY PARAMETERS
-
- offer
- text
- timing
- design
- channel
-
- saved
- card_updated_before_renewal
- recovered_within_7d
- won_back
- segment string
- auto_pick boolean
- threshold number
- min_sample integer
-
Show child parametersHide child parameters
- label string
- weight integer
- overrides object
A null in send_window means follow the flow (not the account): it is not stored.
- is_control boolean
-
RETURNS
-
201a draft — a Experiment
ERRORS
-
400invalid_request invalid variants (duplicate labels, no single control), unknown override section, or another flow's segment
curl https://api.nochurn.sh/v1/experiments \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"flow": "cancel",
"name": "Discount vs pause",
"target": "offer",
"goal_metric": "saved",
"variants": [
{
"weight": 50
},
{
"weight": 50,
"overrides": {
"cancel_mode": "immediately"
}
}
]
}'const res = await fetch("https://api.nochurn.sh/v1/experiments", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"flow": "cancel",
"name": "Discount vs pause",
"target": "offer",
"goal_metric": "saved",
"variants": [
{
"weight": 50
},
{
"weight": 50,
"overrides": {
"cancel_mode": "immediately"
}
}
]
}),
});
const experiment = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/experiments",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"flow": "cancel",
"name": "Discount vs pause",
"target": "offer",
"goal_metric": "saved",
"variants": [
{
"weight": 50
},
{
"weight": 50,
"overrides": {
"cancel_mode": "immediately"
}
}
]
},
)
experiment = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/experiments")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"flow" => "cancel",
"name" => "Discount vs pause",
"target" => "offer",
"goal_metric" => "saved",
"variants" => [
{
"weight" => 50
},
{
"weight" => 50,
"overrides" => {
"cancel_mode" => "immediately"
}
}
]
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
experiment = JSON.parse(res.body) {
"object": "experiment",
"id": "abt_kpD34ZFDVT9aMwBJ",
"flow": "flw_uBtoswcyqSYy5vqi",
"segment": null,
"name": "Discount vs pause",
"target": "offer",
"goal_metric": "saved",
"status": "draft",
"auto_pick": true,
"threshold": 0.95,
"min_sample": 100,
"started_at": null,
"stopped_at": null,
"winner": null,
"stats_at": null,
"created_at": "2026-11-04T10:00:00Z",
"variants": [
{
"id": "abv_EwrAK2JyfLNW7vMd",
"label": "A",
"is_control": true,
"weight": 50,
"overrides": {},
"exposures": 0,
"conversions": 0,
"conversion_rate": null,
"probability_best": null
},
{
"id": "abv_uBa3iRjgQ3iMXEQj",
"label": "B",
"is_control": false,
"weight": 50,
"overrides": {
"cancel_mode": "immediately"
},
"exposures": 0,
"conversions": 0,
"conversion_rate": null,
"probability_best": null
}
]
} Start, stop, or pick a winner
- Secret key (
nc_test_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
-
- start
- stop
- pick
BODY PARAMETERS
- variant string
pick: abv_… or label.
RETURNS
-
200started — a Experiment
ERRORS
-
409conflict only a running or stopped test gets a winner
curl https://api.nochurn.sh/v1/experiments/abt_kpD34ZFDVT9aMwBJ/start \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{}'const res = await fetch("https://api.nochurn.sh/v1/experiments/abt_kpD34ZFDVT9aMwBJ/start", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
const experiment = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/experiments/abt_kpD34ZFDVT9aMwBJ/start",
headers={
"Authorization": "Bearer nc_test_…",
},
json={},
)
experiment = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/experiments/abt_kpD34ZFDVT9aMwBJ/start")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
experiment = JSON.parse(res.body) {
"object": "experiment",
"id": "abt_11NKySG6YQfrocfH",
"flow": "flw_PnbiXGGd1R1skZwB",
"segment": null,
"name": "T",
"target": "offer",
"goal_metric": "saved",
"status": "running",
"auto_pick": true,
"threshold": 0.95,
"min_sample": 100,
"started_at": "2026-11-04T10:00:00Z",
"stopped_at": null,
"winner": null,
"stats_at": null,
"created_at": "2026-11-04T10:00:00Z",
"variants": [
{
"id": "abv_8bRciRLXrwnVhqgf",
"label": "A",
"is_control": false,
"weight": 50,
"overrides": {},
"exposures": 0,
"conversions": 0,
"conversion_rate": null,
"probability_best": null
},
{
"id": "abv_qgyEn9yU48DBcVcW",
"label": "B",
"is_control": false,
"weight": 50,
"overrides": {},
"exposures": 0,
"conversions": 0,
"conversion_rate": null,
"probability_best": null
}
]
}