API REFERENCE v1

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
Show 9 more attributesHide 9 attributes
  • live_version object | null
    Show child attributesHide child attributes
  • 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.

    • published_at string | null date-time
    • unpublished_changes array of strings

      Draft sections not yet published in this mode (a pending draft).

  • draft_updated_at string | null date-time
  • paused_at string | null date-time
  • created_at string date-time
  • updated_at string date-time
  • 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
    • object enum
      • cancel_flow_logo
    • id string
    • flow string
    • url string

      Public and stable: put it in draft.design.logo_url. No account id in it; served without auth.

      • image/png
      • image/jpeg
      • image/webp
    • byte_size integer
    • width integer

      px

    • height integer

      px

    • created_at string date-time

Schema: FlowDetail

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

PATH PARAMETERS

  • id string REQUIRED

    flw_… or the kind of the default flow: prevention, recovery, cancel, winback.

RETURNS

ERRORS

GET/v1/flows/{id}
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): configure scope, test mode only.

PATH PARAMETERS

  • id string REQUIRED

    flw_… or the kind of the default flow: prevention, recovery, cancel, winback.

BODY PARAMETERS

RETURNS

ERRORS

PATCH/v1/flows/{id}
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

RETURNS

GET/v1/flows
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)
RESPONSE 200
{
  "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
}

KPIs, funnel and holdout of a flow

  • Secret key (nc_test_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

PATH PARAMETERS

  • id string REQUIRED

    flw_… or the kind (prevention, recovery, cancel, winback).

QUERY PARAMETERS

  • period enum default "month"
    • month
    • 30d
    • 90d
    • 12m

RETURNS

ERRORS

GET/v1/flows/{id}/stats
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

PATH PARAMETERS

  • id string REQUIRED

    flw_… or the kind of the default flow

BODY PARAMETERS

  • step string REQUIRED

    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.

    Show child parametersHide child parameters
  • 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

ERRORS

POST/v1/flows/{id}/email_preview
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)
RESPONSE 200
{
  "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&#39;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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

RETURNS

GET/v1/flow_notifications
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • kind string REQUIRED

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

ERRORS

PATCH/v1/flow_notifications/{kind}
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • kind string REQUIRED

RETURNS

POST/v1/flow_notifications/{kind}/test
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)
RESPONSE 201
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): configure scope, test mode only.

PATH PARAMETERS

  • id string REQUIRED

BODY PARAMETERS

  • to enum REQUIRED
    • test
    • live

RETURNS

POST/v1/flows/{id}/publish
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): configure scope, test mode only.

PATH PARAMETERS

  • id string REQUIRED

RETURNS

POST/v1/flows/{id}/discard
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

PATH PARAMETERS

  • id string REQUIRED

RETURNS

GET/v1/flows/{id}/versions
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): configure scope, test mode only.

PATH PARAMETERS

  • id string REQUIRED
  • number integer REQUIRED

RETURNS

POST/v1/flows/{id}/versions/{number}/restore
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • id string REQUIRED

RETURNS

ERRORS

  • 403 forbidden restricted key with flows:read only
POST/v1/flows/{id}/pause
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • id string REQUIRED

RETURNS

POST/v1/flows/{id}/resume
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

PATH PARAMETERS

  • id string REQUIRED

QUERY PARAMETERS

RETURNS

GET/v1/flows/{id}/runs
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • id string REQUIRED

    flw_… or recovery

HEADER PARAMETERS

BODY PARAMETERS

  • step string | integer REQUIRED

    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

ERRORS

POST/v1/flows/{id}/send_test
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)
RESPONSE 201
{
  "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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

RETURNS

GET/v1/segment_fields
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

QUERY PARAMETERS

  • flow string REQUIRED

    flw_… or a kind.

RETURNS

GET/v1/segments
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

QUERY PARAMETERS

  • flow string REQUIRED

    flw_… or a kind.

BODY PARAMETERS

  • name string REQUIRED
  • 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

  • 201 created before Everyone else — a Segment

ERRORS

POST/v1/segments
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)
RESPONSE 201
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • id string REQUIRED

BODY PARAMETERS

RETURNS

  • 200 a null in send_window follows the flow: not stored — a Segment

ERRORS

PATCH/v1/segments/{id}
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • id string REQUIRED

RETURNS

  • 200 deleted

ERRORS

DELETE/v1/segments/{id}
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

BODY PARAMETERS

RETURNS

POST/v1/segments/preview
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)
RESPONSE 200
{
  "object": "segment_preview",
  "customers_count": 1,
  "mrr_cents": 4900,
  "counted_at": "2026-11-04T10:00:00Z"
}

A/B tests

  • Secret key (nc_test_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

QUERY PARAMETERS

RETURNS

GET/v1/experiments
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

BODY PARAMETERS

RETURNS

ERRORS

  • 400 invalid_request invalid variants (duplicate labels, no single control), unknown override section, or another flow's segment
POST/v1/experiments
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)
RESPONSE 201
{
  "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_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • id string REQUIRED
  • action enum REQUIRED
    • start
    • stop
    • pick

BODY PARAMETERS

  • variant string

    pick: abv_… or label.

RETURNS

ERRORS

  • 409 conflict only a running or stopped test gets a winner
POST/v1/experiments/{id}/{action}
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)
RESPONSE 200
{
  "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
    }
  ]
}