API REFERENCE v1

CORE RESOURCES

Customers

The Customer object

The people and companies who pay you: their three ids (cst_, cus_, your external_id), churn score, access and custom attributes.

ATTRIBUTES

  • object enum
    • customer
  • id string

    NoChurn id, always present.

  • stripe_customer_id string | null

    Stripe customer id, when billed by Stripe.

  • external_id string | null

    The id of this customer in your app.

  • stripe_account string | null
  • livemode boolean
  • email string | null
  • company_domain string | null

    Domain of the email when it is a company's (not a free, disposable or placeholder address): the customer's logo is GET /v1/logos/{company_domain}.

Show 24 more attributesHide 24 attributes
  • name string | null
  • locale string | null
  • country string | null
  • region string | null

    State or province of the address.

  • timezone string | null

    IANA time zone used to reach the customer.

  • timezone_source enum | null

    api (sent by you, never overwritten), attribute, address, country_guess (main zone of a multi-zone country), ip_country, card_country, account (fallback).

    • api
    • attribute
    • address
    • country_guess
    • ip_country
    • card_country
    • account
  • attributes object

    Custom attributes (M4/M6).

  • churn_score integer | null
  • churn_level enum | null
    • low
    • medium
    • high
  • flag object | null

    FLAGGED: a prevention signal before the renewal. Cleared when a new card arrives.

    Show child attributesHide child attributes
  • state enum

    From billing: in_recovery = an open invoice with a failed attempt; churned = no live subscription.

    • none
    • trialing
    • active
    • in_recovery
    • paused
    • churned
  • plan string | null

    Plan of the main live subscription.

  • mrr_cents integer

    Monthly recurring revenue, in the minor unit of currency.

  • Paid invoices seen by NoChurn (12 months of Stripe history, then all).

  • currency string | null

    Billing currency. Amounts are never converted.

  • payment_method object | null

    Default payment method (null when none).

    Show child attributesHide child attributes
  • last_seen_at string | null date-time

    From the last_active_at attribute your app sends.

  • churned_at string | null date-time
  • flows_paused_at string | null date-time

    Set by Pause all flows: no flow acts on this customer.

  • signup_at string | null date-time
  • created_at string date-time
  • updated_at string date-time
  • identifiers array of objects

    Every identifier a source gave for this customer, oldest first: how Stripe, your other tools, your app and support all reach this one customer (docs: customer matching).

    Show child attributesHide child attributes
    • kind enum

      stripe (cus_), provider (a billing, store or support tool's id), external (your id, current or previous), email, nochurn (the cst_ id of a customer merged into this one, still accepted).

      • stripe
      • external
      • email
      • provider
      • nochurn
    • provider string | null

      The tool for stripe and provider ids (stripe, chargebee, app_store, intercom…).

    • connection string | null

      The billing connection (scn_ / bcn_) or support integration (itg_) the id belongs to.

    • value string
    • source enum

      Who gave it: api, import, stripe, stripe_metadata, provider, support, manual, merge (moved by a merge).

      • api
      • stripe_metadata
      • stripe
      • manual
      • provider
      • support
      • import
      • merge
    • created_at string date-time
  • risk object

    With include[]=risk (list only): when the customer renews and why the score is what it is. Early signals (card removed…) are in flag.

    Show child attributesHide child attributes
    • next_renewal_at string | null date-time

      Soonest end of the current period of a paying subscription (active or past_due) still to come.

    • cancel_at_period_end boolean | null

      That subscription stops then instead of renewing. null without a renewal.

    • score_factors array of objects 5 items at most

      Factors that raised the last churn score, largest first (starting point and lowering factors left out).

      Show child attributesHide child attributes
    • score_computed_at string | null date-time

Schema: Customer

THE CUSTOMER OBJECT
{
  "object": "customer",
  "id": "cst_8VVgYZN5XyepV47K",
  "stripe_customer_id": "cus_lena",
  "external_id": "u_lena",
  "stripe_account": "acct_A",
  "livemode": false,
  "email": "customer1@acme-corp.dev",
  "company_domain": "acme-corp.dev",
  "name": null,
  "locale": null,
  "country": null,
  "region": null,
  "timezone": "Europe/Paris",
  "timezone_source": "account",
  "attributes": {
    "plan": "pro",
    "exports_30d": 46
  },
  "churn_score": 82,
  "churn_level": "high",
  "flag": null,
  "state": "in_recovery",
  "plan": "Pro",
  "mrr_cents": 4900,
  "lifetime_value_cents": 4900,
  "currency": "eur",
  "payment_method": {
    "id": "npm_2HFV26kN2xmX77qn",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "exp_month": null,
    "exp_year": null,
    "funding": "prepaid",
    "wallet": "apple_pay",
    "risk_category": "prepaid",
    "label": "Apple Pay · Visa •••• 4242"
  },
  "last_seen_at": null,
  "churned_at": null,
  "flows_paused_at": "2026-11-04T10:00:00.000Z",
  "signup_at": null,
  "created_at": "2026-11-04T10:00:00.000Z",
  "updated_at": "2026-11-04T10:00:00.000Z",
  "identifiers": []
}

Create a customer (billing without Stripe, or before Stripe knows it)

Matched first (customer matching): when exactly one customer of this mode has this email and no other id of yours, it is linked to your external_id and returned with 200 instead of a duplicate. Several customers with the email: a new one is created and a possible duplicate To do is raised.

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): sandbox scope, test mode only.

BODY PARAMETERS

  • external_id string

    Recommended: the id in your app.

  • email string
  • name string
  • locale enum
    • en
    • de
    • fr
    • it
    • pt
  • country string
  • region string

    State or province (US: TX).

  • timezone string | null

    IANA zone. Wins over any guess; null goes back to the guess.

RETURNS

ERRORS

POST/v1/customers
curl https://api.nochurn.sh/v1/customers \
  -H "Authorization: Bearer nc_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "u_456",
    "email": "new@example.test"
  }'
const res = await fetch("https://api.nochurn.sh/v1/customers", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "external_id": "u_456",
    "email": "new@example.test"
  }),
});
const customer = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/customers",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
    json={
      "external_id": "u_456",
      "email": "new@example.test"
    },
)
customer = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
  "external_id" => "u_456",
  "email" => "new@example.test"
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
customer = JSON.parse(res.body)
RESPONSE
{
  "object": "customer",
  "id": "cst_ATdWVXLNrWdPxkSw",
  "stripe_customer_id": null,
  "external_id": "u_456",
  "stripe_account": null,
  "livemode": false,
  "email": "new@example.test",
  "company_domain": null,
  "name": null,
  "locale": null,
  "country": null,
  "region": null,
  "timezone": "Europe/Paris",
  "timezone_source": "account",
  "attributes": {},
  "churn_score": null,
  "churn_level": null,
  "flag": null,
  "state": "none",
  "plan": null,
  "mrr_cents": 0,
  "lifetime_value_cents": 0,
  "currency": null,
  "payment_method": null,
  "last_seen_at": null,
  "churned_at": null,
  "flows_paused_at": null,
  "signup_at": "2026-11-04T10:00:00.000Z",
  "created_at": "2026-11-04T10:00:00.000Z",
  "updated_at": "2026-11-04T10:00:00.000Z",
  "identifiers": [
    {
      "kind": "external",
      "provider": null,
      "connection": null,
      "value": "u_456",
      "source": "api",
      "created_at": "2026-11-04T10:00:00.000Z"
    },
    {
      "kind": "email",
      "provider": null,
      "connection": null,
      "value": "new@example.test",
      "source": "api",
      "created_at": "2026-11-04T10:00:00.000Z"
    }
  ]
}

Retrieve a customer

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

PATH PARAMETERS

  • id string REQUIRED

    cst_…, cus_… or your own id.

QUERY PARAMETERS

RETURNS

ERRORS

GET/v1/customers/{id}
curl https://api.nochurn.sh/v1/customers/cst_3WcGvYMRCF8rixYt \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customers/cst_3WcGvYMRCF8rixYt", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const customer = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/customers/cst_3WcGvYMRCF8rixYt",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
customer = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers/cst_3WcGvYMRCF8rixYt")
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) }
customer = JSON.parse(res.body)
RESPONSE 200
{
  "object": "customer",
  "id": "cst_3WcGvYMRCF8rixYt",
  "stripe_customer_id": "cus_123",
  "external_id": null,
  "stripe_account": "acct_A",
  "livemode": false,
  "email": "ana@example.test",
  "company_domain": null,
  "name": null,
  "locale": null,
  "country": null,
  "region": null,
  "timezone": "Europe/Paris",
  "timezone_source": "account",
  "attributes": {},
  "churn_score": null,
  "churn_level": null,
  "flag": null,
  "state": "none",
  "plan": null,
  "mrr_cents": 0,
  "lifetime_value_cents": 0,
  "currency": null,
  "payment_method": null,
  "last_seen_at": null,
  "churned_at": null,
  "flows_paused_at": null,
  "signup_at": null,
  "created_at": "2026-11-04T10:00:00.000Z",
  "updated_at": "2026-11-04T10:00:00.000Z",
  "identifiers": []
}

Update a customer — e.g. link your own id to a Stripe customer

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): sandbox scope, test mode only.

PATH PARAMETERS

  • id string REQUIRED

    cst_…, cus_… or your own id.

QUERY PARAMETERS

BODY PARAMETERS

RETURNS

ERRORS

PATCH/v1/customers/{id}
curl https://api.nochurn.sh/v1/customers/cst_QwoaoX5sv4eucKBL \
  -X PATCH \
  -H "Authorization: Bearer nc_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "u_789"
  }'
const res = await fetch("https://api.nochurn.sh/v1/customers/cst_QwoaoX5sv4eucKBL", {
  method: "PATCH",
  headers: {
    Authorization: "Bearer nc_test_…",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "external_id": "u_789"
  }),
});
const customer = await res.json();
import requests

res = requests.patch(
    "https://api.nochurn.sh/v1/customers/cst_QwoaoX5sv4eucKBL",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
    json={
      "external_id": "u_789"
    },
)
customer = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers/cst_QwoaoX5sv4eucKBL")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
  "external_id" => "u_789"
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
customer = JSON.parse(res.body)
RESPONSE 200
{
  "object": "customer",
  "id": "cst_QwoaoX5sv4eucKBL",
  "stripe_customer_id": "cus_123",
  "external_id": "u_789",
  "stripe_account": "acct_A",
  "livemode": false,
  "email": "ana@example.test",
  "company_domain": null,
  "name": null,
  "locale": null,
  "country": null,
  "region": null,
  "timezone": "Europe/Paris",
  "timezone_source": "account",
  "attributes": {},
  "churn_score": null,
  "churn_level": null,
  "flag": null,
  "state": "none",
  "plan": null,
  "mrr_cents": 0,
  "lifetime_value_cents": 0,
  "currency": null,
  "payment_method": null,
  "last_seen_at": null,
  "churned_at": null,
  "flows_paused_at": null,
  "signup_at": null,
  "created_at": "2026-11-04T10:00:00.000Z",
  "updated_at": "2026-11-04T10:00:00.000Z",
  "identifiers": [
    {
      "kind": "email",
      "provider": null,
      "connection": null,
      "value": "ana@example.test",
      "source": "api",
      "created_at": "2026-11-04T10:00:00.000Z"
    },
    {
      "kind": "external",
      "provider": null,
      "connection": null,
      "value": "u_789",
      "source": "api",
      "created_at": "2026-11-04T10:00:00.000Z"
    }
  ]
}

List customers

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

QUERY PARAMETERS

  • q string

    Search email, name or any id, including every identifier a source gave (a tool's id, a previous email or id).

  • email string

    Current or previous email.

  • level string

    Churn level, comma-separated: low, medium, high.

  • state string

    Comma-separated: none, trialing, active, in_recovery, paused, churned.

  • sort enum default "-created_at"

    Descending. Customers without a score (or last_seen_at) come last.

    • -created_at
    • -churn_score
    • -mrr
    • -last_seen_at
  • at_risk boolean

    true: a churn score of 70 or more, or an early signal (flag: card removed…), the rule of Home's at-risk renewals; false: the others.

  • renews_within_days integer 1 to 90

    A paying subscription (active or past_due) whose current period ends within this many days (set to cancel at the period end included: include[]=risk says so). With at_risk=true and 7: Home's at_risk_7d.

  • include[] array of enums

    total_count: count of the filtered list. risk: each customer's next renewal and score factors (risk).

    • total_count
    • risk
  • limit integer default 20 · 1 to 100

RETURNS

ERRORS

GET/v1/customers
curl https://api.nochurn.sh/v1/customers \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customers", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const page = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/customers",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
page = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers")
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": "customer",
      "id": "cst_bTjTg5h1W9jXomCB",
      "stripe_customer_id": "cus_123",
      "external_id": null,
      "stripe_account": "acct_A",
      "livemode": false,
      "email": "ana@example.test",
      "company_domain": null,
      "name": null,
      "locale": null,
      "country": null,
      "region": null,
      "timezone": "Europe/Paris",
      "timezone_source": "account",
      "attributes": {},
      "churn_score": null,
      "churn_level": null,
      "flag": null,
      "state": "none",
      "plan": null,
      "mrr_cents": 0,
      "lifetime_value_cents": 0,
      "currency": null,
      "payment_method": null,
      "last_seen_at": null,
      "churned_at": null,
      "flows_paused_at": null,
      "signup_at": null,
      "created_at": "2026-11-04T10:00:00.000Z",
      "updated_at": "2026-11-04T10:00:00.000Z",
      "identifiers": []
    },
    {
      "object": "customer",
      "id": "cst_BMdHx29aVWcEdQLp",
      "stripe_customer_id": null,
      "external_id": "u_123",
      "stripe_account": null,
      "livemode": false,
      "email": "bob@example.test",
      "company_domain": null,
      "name": null,
      "locale": null,
      "country": null,
      "region": null,
      "timezone": "Europe/Paris",
      "timezone_source": "account",
      "attributes": {},
      "churn_score": null,
      "churn_level": null,
      "flag": null,
      "state": "none",
      "plan": null,
      "mrr_cents": 0,
      "lifetime_value_cents": 0,
      "currency": null,
      "payment_method": null,
      "last_seen_at": null,
      "churned_at": null,
      "flows_paused_at": null,
      "signup_at": null,
      "created_at": "2026-11-04T10:00:00.000Z",
      "updated_at": "2026-11-04T10:00:00.000Z",
      "identifiers": []
    }
  ],
  "has_more": false,
  "next_cursor": null
}

May this customer use your app? (server-side wall)

The truth behind NoChurn's in-app wall, for your backend: the widget's wall alone can be bypassed. Same calculation as GET /v1/inapp/state (wall.after_days, annual_grace_days; 0 = as soon as the payment fails). Secret key (customers: read). Webhooks customer.access_restricted / customer.access_restored tell you when it changes.

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

PATH PARAMETERS

  • id string REQUIRED

    cst_…, cus_… or your id.

QUERY PARAMETERS

  • acct_… when the same cus_ exists on two connected Stripe accounts.

RETURNS

ERRORS

GET/v1/customers/{id}/access
curl https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/access \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/access", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const customerAccess = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/access",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
customer_access = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/access")
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) }
customer_access = JSON.parse(res.body)
RESPONSE 200
{
  "object": "customer_access",
  "customer": "cst_4w9Nj8EJEvWj7MDT",
  "livemode": false,
  "access": "full",
  "reason": null,
  "since": null,
  "failing_since": "2026-11-04T10:00:00Z",
  "restricts_at": "2026-11-11T10:00:00Z",
  "invoice": "ninv_1dMMAnaZyLmBNtDB",
  "pay_url": "http://localhost:3000/c/Iyp0n60Dcd6R6xAK3UJ1MszddKUzHkeP",
  "subscription": null,
  "paused_at": null,
  "resumes_at": null,
  "cancel_url": "http://localhost:3000/a/eyJfcmFpbHMiOnsiZGF0YSI6ImNzdF80dzlOajhFSkV2V2o3TURUIiwiZXhwIjoiMjAyNi0xMi0wNFQxMDowMDowMC4wMDBaIiwicHVyIjoiY3VzdG9tZXIvYWNjZXNzX2NhbmNlbCJ9fQ--46a89957572650047e29db93b65f0dc84d316720d722daa2cb2615d5fafc99f9"
}

The 4 figures of the Customers page

This month starts on the 1st in the account's time zone. Amounts are grouped by currency.

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

RETURNS

GET/v1/customers/summary
curl https://api.nochurn.sh/v1/customers/summary \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customers/summary", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const customerSummary = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/customers/summary",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
customer_summary = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers/summary")
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) }
customer_summary = JSON.parse(res.body)
RESPONSE 200
{
  "object": "customer_summary",
  "livemode": false,
  "timezone": "Europe/Paris",
  "month_start": "2026-11-01T00:00:00+01:00",
  "active": {
    "count": 1,
    "new_this_month": 1
  },
  "high_risk": {
    "threshold": 70,
    "count": 1,
    "mrr": [
      {
        "currency": "eur",
        "cents": 4900
      }
    ],
    "scored_customers": 1
  },
  "in_recovery": {
    "count": 1,
    "mrr": [
      {
        "currency": "eur",
        "cents": 4900
      }
    ]
  },
  "saved_this_month": {
    "count": null,
    "unavailable_reason": "Counted from the recovery and cancel flows, not live yet."
  }
}

Customer profile

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

PATH PARAMETERS

  • id string REQUIRED

    cst_…, cus_… or your id.

RETURNS

ERRORS

GET/v1/customers/{id}/profile
curl https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/profile \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/profile", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const customerProfile = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/profile",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
customer_profile = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/profile")
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) }
customer_profile = JSON.parse(res.body)
RESPONSE 200
{
  "object": "customer_profile",
  "customer": {
    "object": "customer",
    "id": "cst_6KWhRc2vsEuErrbU",
    "stripe_customer_id": "cus_lena",
    "external_id": "u_lena",
    "stripe_account": "acct_A",
    "livemode": false,
    "email": "customer1@acme-corp.dev",
    "company_domain": "acme-corp.dev",
    "name": null,
    "locale": null,
    "country": null,
    "region": null,
    "timezone": "Europe/Paris",
    "timezone_source": "account",
    "attributes": {
      "plan": "pro",
      "exports_30d": 46
    },
    "churn_score": 82,
    "churn_level": "high",
    "flag": null,
    "state": "in_recovery",
    "plan": "Pro",
    "mrr_cents": 4900,
    "lifetime_value_cents": 4900,
    "currency": "eur",
    "payment_method": {
      "id": "npm_5g6sqZdgrBnJo1rw",
      "type": "card",
      "brand": "visa",
      "last4": "4242",
      "exp_month": null,
      "exp_year": null,
      "funding": "prepaid",
      "wallet": "apple_pay",
      "risk_category": "prepaid",
      "label": "Apple Pay · Visa •••• 4242"
    },
    "last_seen_at": null,
    "churned_at": null,
    "flows_paused_at": null,
    "signup_at": null,
    "created_at": "2026-11-04T10:00:00.000Z",
    "updated_at": "2026-11-04T10:00:00.000Z",
    "identifiers": []
  },
  "billing": {
    "mrr_cents": 4900,
    "currency": "eur",
    "lifetime_value_cents": 4900,
    "lifetime_value_since": "2025-11-04T10:00:00.000Z",
    "customer_since": "2026-11-04T10:00:00.000Z",
    "payment_method": {
      "id": "npm_5g6sqZdgrBnJo1rw",
      "type": "card",
      "brand": "visa",
      "last4": "4242",
      "exp_month": null,
      "exp_year": null,
      "funding": "prepaid",
      "wallet": "apple_pay",
      "risk_category": "prepaid",
      "label": "Apple Pay · Visa •••• 4242"
    },
    "next_renewal": {
      "at": "2026-11-14T10:00:00.000Z",
      "amount_cents": 4900,
      "currency": "eur",
      "subscription": "nsub_9mqhquow11EnC1Px"
    },
    "subscriptions": [
      {
        "id": "nsub_9mqhquow11EnC1Px",
        "plan": "Pro",
        "status": "active",
        "mrr_cents": 4900,
        "currency": "eur",
        "interval": "month",
        "interval_count": 1,
        "current_period_end": "2026-11-14T10:00:00.000Z",
        "cancel_at_period_end": false
      }
    ]
  },
  "score": {
    "score": 82,
    "level": "high",
    "factors": [],
    "computed_at": null
  },
  "ai_analysis": null,
  "ai_analysis_status": "missing",
  "attributes": {
    "plan": "pro",
    "exports_30d": 46
  },
  "links": {
    "stripe_dashboard": "https://dashboard.stripe.com/acct_A/test/customers/cus_lena"
  }
}

Customer activity, newest first

NoChurn events, payment attempts, subscription starts and cancellations.

  • 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/customers/{id}/timeline
curl https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/timeline \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/timeline", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const page = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/timeline",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
page = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/timeline")
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": "timeline_item",
      "id": "nevt_TCnRkbneCoz1xsac",
      "occurred_at": "2026-11-04T10:00:00.000Z",
      "category": "billing",
      "type": "payment_method.attached",
      "summary": "Card added",
      "ref": {
        "type": "payment_method",
        "id": "npm_4VZffAUJcm7uFR8g"
      },
      "data": {}
    },
    {
      "object": "timeline_item",
      "id": "npay_EpsfXBzWuJC66pWy",
      "occurred_at": "2026-11-03T10:00:00.000Z",
      "category": "billing",
      "type": "payment.failed",
      "summary": "€49.00 · failed · insufficient_funds",
      "ref": {
        "type": "payment",
        "id": "npay_EpsfXBzWuJC66pWy"
      },
      "data": {
        "amount_cents": 4900,
        "amount_refunded_cents": 0,
        "currency": "eur",
        "decline_code": "insufficient_funds",
        "risk_category": null,
        "invoice": null
      }
    },
    {
      "object": "timeline_item",
      "id": "nsub_wwcVS74Uv3QGxhqn.started",
      "occurred_at": "2026-08-04T10:00:00.000Z",
      "category": "billing",
      "type": "subscription.started",
      "summary": "Pro · active",
      "ref": {
        "type": "subscription",
        "id": "nsub_wwcVS74Uv3QGxhqn"
      },
      "data": {
        "plan": "Pro",
        "status": "active",
        "mrr_cents": 4900,
        "currency": "eur"
      }
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Pause all flows for a customer

Stops the customer's active runs and blocks new ones until resume_flows.

  • 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/customers/{id}/pause_flows
curl https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/pause_flows \
  -X POST \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/pause_flows", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const customer = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/pause_flows",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
customer = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/pause_flows")
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) }
customer = JSON.parse(res.body)
RESPONSE 200
{
  "object": "customer",
  "id": "cst_8VVgYZN5XyepV47K",
  "stripe_customer_id": "cus_lena",
  "external_id": "u_lena",
  "stripe_account": "acct_A",
  "livemode": false,
  "email": "customer1@acme-corp.dev",
  "company_domain": "acme-corp.dev",
  "name": null,
  "locale": null,
  "country": null,
  "region": null,
  "timezone": "Europe/Paris",
  "timezone_source": "account",
  "attributes": {
    "plan": "pro",
    "exports_30d": 46
  },
  "churn_score": 82,
  "churn_level": "high",
  "flag": null,
  "state": "in_recovery",
  "plan": "Pro",
  "mrr_cents": 4900,
  "lifetime_value_cents": 4900,
  "currency": "eur",
  "payment_method": {
    "id": "npm_2HFV26kN2xmX77qn",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "exp_month": null,
    "exp_year": null,
    "funding": "prepaid",
    "wallet": "apple_pay",
    "risk_category": "prepaid",
    "label": "Apple Pay · Visa •••• 4242"
  },
  "last_seen_at": null,
  "churned_at": null,
  "flows_paused_at": "2026-11-04T10:00:00.000Z",
  "signup_at": null,
  "created_at": "2026-11-04T10:00:00.000Z",
  "updated_at": "2026-11-04T10:00:00.000Z",
  "identifiers": []
}

Resume flows for a customer

  • 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/customers/{id}/resume_flows
curl https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/resume_flows \
  -X POST \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/resume_flows", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const customer = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/resume_flows",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
customer = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/resume_flows")
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) }
customer = JSON.parse(res.body)
RESPONSE 200
{
  "object": "customer",
  "id": "cst_yg7PWhJh6a5aucuT",
  "stripe_customer_id": "cus_lena",
  "external_id": "u_lena",
  "stripe_account": "acct_A",
  "livemode": false,
  "email": "customer1@acme-corp.dev",
  "company_domain": "acme-corp.dev",
  "name": null,
  "locale": null,
  "country": null,
  "region": null,
  "timezone": "Europe/Paris",
  "timezone_source": "account",
  "attributes": {
    "plan": "pro",
    "exports_30d": 46
  },
  "churn_score": 82,
  "churn_level": "high",
  "flag": null,
  "state": "in_recovery",
  "plan": "Pro",
  "mrr_cents": 4900,
  "lifetime_value_cents": 4900,
  "currency": "eur",
  "payment_method": {
    "id": "npm_QWF1hjeTz5iFPdTE",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "exp_month": null,
    "exp_year": null,
    "funding": "prepaid",
    "wallet": "apple_pay",
    "risk_category": "prepaid",
    "label": "Apple Pay · Visa •••• 4242"
  },
  "last_seen_at": null,
  "churned_at": null,
  "flows_paused_at": null,
  "signup_at": null,
  "created_at": "2026-11-04T10:00:00.000Z",
  "updated_at": "2026-11-04T10:00:00.000Z",
  "identifiers": []
}

Possible duplicates: count and scans

Open proposals of this mode, the end of the last scan and the latest scan and merge all, with their real counts.

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

RETURNS

GET/v1/customer_duplicates/summary
curl https://api.nochurn.sh/v1/customer_duplicates/summary \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customer_duplicates/summary", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const customerDuplicateSummary = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/customer_duplicates/summary",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
customer_duplicate_summary = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customer_duplicates/summary")
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) }
customer_duplicate_summary = JSON.parse(res.body)
RESPONSE 200
{
  "object": "customer_duplicate_summary",
  "livemode": false,
  "open_count": 1,
  "last_scanned_at": "2026-11-04T10:00:00.000Z",
  "scan": {
    "object": "customer_duplicate_run",
    "id": "cdr_pvVTkKp4mB7Aoxqo",
    "livemode": false,
    "kind": "scan",
    "status": "done",
    "total": 2,
    "processed": 2,
    "found": 1,
    "merged": 0,
    "skipped": 0,
    "failed": 0,
    "failures": [],
    "error": null,
    "started_at": "2026-11-04T10:00:00.000Z",
    "finished_at": "2026-11-04T10:00:00.000Z",
    "created_at": "2026-11-04T10:00:00.000Z"
  },
  "merge_all": null
}

Scan the customers for possible duplicates

Looks for possible duplicates among the customers of this mode already in base, with the same rules as customer matching (a shared current email; an id one customer holds that is another one's alias). In the background: poll GET /v1/customer_duplicates/summary. A pair already proposed, dismissed or merged is never proposed again. One at a time (a running scan is answered), once a minute at most (429). Owner or admin.

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

RETURNS

ERRORS

POST/v1/customer_duplicates/scan
curl https://api.nochurn.sh/v1/customer_duplicates/scan \
  -X POST \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customer_duplicates/scan", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const customerDuplicateRun = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/customer_duplicates/scan",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
customer_duplicate_run = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customer_duplicates/scan")
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) }
customer_duplicate_run = JSON.parse(res.body)
RESPONSE 202
{
  "object": "customer_duplicate_run",
  "id": "cdr_dSZC8EKunhNUWQrf",
  "livemode": false,
  "kind": "scan",
  "status": "queued",
  "total": 0,
  "processed": 0,
  "found": 0,
  "merged": 0,
  "skipped": 0,
  "failed": 0,
  "failures": [],
  "error": null,
  "started_at": null,
  "finished_at": null,
  "created_at": "2026-11-04T10:00:00.000Z"
}

Merge every open possible duplicate

Irreversible. Merges each open proposal of this mode into its suggested_keep customer, one merge per pair (same as POST /v1/customer_duplicates/{id}/merge), oldest first: in a chain (A~B, B~C) the second merge targets the customer kept by the first. A pair that fails stays open with its reason in failures; the others go on. Up to 10 pairs: merged now (200); more: in the background (202, poll the summary). While one runs, it is answered. Owner or admin.

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

RETURNS

POST/v1/customer_duplicates/merge_all
curl https://api.nochurn.sh/v1/customer_duplicates/merge_all \
  -X POST \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customer_duplicates/merge_all", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const customerDuplicateRun = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/customer_duplicates/merge_all",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
customer_duplicate_run = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customer_duplicates/merge_all")
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) }
customer_duplicate_run = JSON.parse(res.body)
RESPONSE
{
  "object": "customer_duplicate_run",
  "id": "cdr_CfHJreAxeF3b8VVy",
  "livemode": false,
  "kind": "merge_all",
  "status": "done",
  "total": 1,
  "processed": 1,
  "found": 0,
  "merged": 1,
  "skipped": 0,
  "failed": 0,
  "failures": [],
  "error": null,
  "started_at": "2026-11-04T10:00:00.000Z",
  "finished_at": "2026-11-04T10:00:00.000Z",
  "created_at": "2026-11-04T10:00:00.000Z"
}

Resolve a customer from any of its ids

Pass exactly one of customer (cst_…, cus_… or your id, resolved in that order), stripe_customer or external_id.

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

QUERY PARAMETERS

RETURNS

ERRORS

GET/v1/customers/resolve
curl https://api.nochurn.sh/v1/customers/resolve \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customers/resolve", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const customer = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/customers/resolve",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
customer = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers/resolve")
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) }
customer = JSON.parse(res.body)
RESPONSE 200
{
  "object": "customer",
  "id": "cst_ssJU64rijfqjh2XS",
  "stripe_customer_id": null,
  "external_id": "u_123",
  "stripe_account": null,
  "livemode": false,
  "email": "bob@example.test",
  "company_domain": null,
  "name": null,
  "locale": null,
  "country": null,
  "region": null,
  "timezone": "Europe/Paris",
  "timezone_source": "account",
  "attributes": {},
  "churn_score": null,
  "churn_level": null,
  "flag": null,
  "state": "none",
  "plan": null,
  "mrr_cents": 0,
  "lifetime_value_cents": 0,
  "currency": null,
  "payment_method": null,
  "last_seen_at": null,
  "churned_at": null,
  "flows_paused_at": null,
  "signup_at": null,
  "created_at": "2026-11-04T10:00:00.000Z",
  "updated_at": "2026-11-04T10:00:00.000Z",
  "identifiers": []
}

List possible duplicate customers

Two customers of this mode that may be the same person: they share an email, or one source named ids held by both. NoChurn never merges customers automatically; each proposal is also a To do (possible_duplicate), which a human dismisses or merges. suggested_keep: the customer to preselect (a live subscription, else billed by a tool, else the oldest).

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

QUERY PARAMETERS

RETURNS

ERRORS

GET/v1/customer_duplicates
curl https://api.nochurn.sh/v1/customer_duplicates \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customer_duplicates", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const page = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/customer_duplicates",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
page = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customer_duplicates")
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": "customer_duplicate",
      "id": "cdp_Mu1X3FMJ79pDfrKj",
      "livemode": false,
      "customer": "cst_jhuithY715hV64ga",
      "other_customer": "cst_y4w5qHA2hRDq9mFK",
      "people": [
        {
          "id": "cst_jhuithY715hV64ga",
          "name": null,
          "email": "ana@example.test"
        },
        {
          "id": "cst_y4w5qHA2hRDq9mFK",
          "name": null,
          "email": "ana@example.test"
        }
      ],
      "identifier": "email",
      "reason": "share an email",
      "status": "open",
      "suggested_keep": "cst_y4w5qHA2hRDq9mFK",
      "kept_customer": null,
      "dismissed_at": null,
      "merged_at": null,
      "created_at": "2026-11-04T10:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Dismiss a possible duplicate (not the same person)

Both customers stay as they are; the pair is never proposed again.

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • id string REQUIRED

RETURNS

ERRORS

  • 404 not_found unknown or in another account
POST/v1/customer_duplicates/{id}/dismiss
curl https://api.nochurn.sh/v1/customer_duplicates/cdp_Mu1X3FMJ79pDfrKj/dismiss \
  -X POST \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customer_duplicates/cdp_Mu1X3FMJ79pDfrKj/dismiss", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const customerDuplicate = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/customer_duplicates/cdp_Mu1X3FMJ79pDfrKj/dismiss",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
customer_duplicate = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customer_duplicates/cdp_Mu1X3FMJ79pDfrKj/dismiss")
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) }
customer_duplicate = JSON.parse(res.body)
RESPONSE 200
{
  "object": "customer_duplicate",
  "id": "cdp_mmgZiHxMMvfqnn8G",
  "livemode": false,
  "customer": "cst_1Xto5g95GnymjNZJ",
  "other_customer": "cst_gD495DbT1fr7KmA1",
  "people": [
    {
      "id": "cst_1Xto5g95GnymjNZJ",
      "name": null,
      "email": "ana@example.test"
    },
    {
      "id": "cst_gD495DbT1fr7KmA1",
      "name": null,
      "email": "bob@example.test"
    }
  ],
  "identifier": "email",
  "reason": "share an email",
  "status": "dismissed",
  "suggested_keep": null,
  "kept_customer": null,
  "dismissed_at": "2026-11-04T10:00:00.000Z",
  "merged_at": null,
  "created_at": "2026-11-04T10:00:00.000Z"
}

Merge a possible duplicate (the same person)

Irreversible. Everything the other customer has (subscriptions, invoices, payments, cards, flow runs, messages, cancel sessions, disputes, timeline, attributes, usage, identifiers) moves to keep, then the other customer is deleted. Its ids stay known: its cst_, Stripe and tool ids, external_id and email are identifiers of the kept customer, so webhooks and API calls that still name it reach the kept one. The kept customer's own values win (the other external_id becomes a previous id). Owner or admin. The same merge again answers the same proposal; another choice once merged, or a dismissed proposal, is a 409.

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • id string REQUIRED

BODY PARAMETERS

  • keep string REQUIRED

    The customer to keep: one of the two (suggested_keep is the default to offer).

RETURNS

ERRORS

POST/v1/customer_duplicates/{id}/merge
curl https://api.nochurn.sh/v1/customer_duplicates/cdp_Mu1X3FMJ79pDfrKj/merge \
  -H "Authorization: Bearer nc_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "keep": "cst_inVEBWu2hf88HdGP"
  }'
const res = await fetch("https://api.nochurn.sh/v1/customer_duplicates/cdp_Mu1X3FMJ79pDfrKj/merge", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "keep": "cst_inVEBWu2hf88HdGP"
  }),
});
const customerDuplicate = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/customer_duplicates/cdp_Mu1X3FMJ79pDfrKj/merge",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
    json={
      "keep": "cst_inVEBWu2hf88HdGP"
    },
)
customer_duplicate = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customer_duplicates/cdp_Mu1X3FMJ79pDfrKj/merge")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
  "keep" => "cst_inVEBWu2hf88HdGP"
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
customer_duplicate = JSON.parse(res.body)
RESPONSE 200
{
  "object": "customer_duplicate",
  "id": "cdp_GtWYvMRMiVrhHU3a",
  "livemode": false,
  "customer": "cst_2qdTqvEmV9hnpr9z",
  "other_customer": "cst_inVEBWu2hf88HdGP",
  "people": [
    null,
    {
      "id": "cst_inVEBWu2hf88HdGP",
      "name": null,
      "email": "ana@example.test"
    }
  ],
  "identifier": "email",
  "reason": "share an email",
  "status": "merged",
  "suggested_keep": "cst_inVEBWu2hf88HdGP",
  "kept_customer": "cst_inVEBWu2hf88HdGP",
  "dismissed_at": null,
  "merged_at": "2026-11-04T10:00:00.000Z",
  "created_at": "2026-11-04T10:00:00.000Z"
}

AI analysis of a customer

Without name, email or sensitive attributes; every number comes from the profile. Fresh for 7 days unless an important event for the customer comes after it (the profile's ai_analysis_status): a fresh analysis is returned without an AI call. One at a time per customer, 200 AI calls a day per account.

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

PATH PARAMETERS

  • id string REQUIRED

RETURNS

ERRORS

  • 422 insufficient_data not enough activity for an analysis (insufficient_data, no AI call)
POST/v1/customers/{id}/insight
curl https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/insight \
  -X POST \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/insight", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const customerInsight = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/insight",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
customer_insight = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/insight")
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) }
customer_insight = JSON.parse(res.body)
RESPONSE 200
{
  "object": "customer_insight",
  "id": "cai_Dbnz8NirwtHM5xPz",
  "summary": "On the Pro plan.",
  "signals": ["Pro"],
  "next_step": "Check in",
  "sources": ["custom_data"],
  "computed_at": "2026-11-04T10:00:00Z"
}

Churn score of a customer

Heuristic v1 (0–100) with the factors that explain it, the prevention flag, and the last 30 computations.

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

PATH PARAMETERS

  • id string REQUIRED

    cst_…, cus_… or your own id.

RETURNS

GET/v1/customers/{id}/score
curl https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/score \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/score", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const churnScore = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/score",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
churn_score = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/score")
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) }
churn_score = JSON.parse(res.body)
RESPONSE 200
{
  "object": "churn_score",
  "customer": "cst_hnAz8xVcGi19CcbK",
  "score": 10,
  "level": "low",
  "model_version": "heuristic-v1",
  "computed_at": "2026-11-04T10:00:00Z",
  "factors": [
    {
      "key": "base",
      "label": "Starting point",
      "points": 10
    }
  ],
  "flag": null,
  "history": [
    {
      "score": 10,
      "level": "low",
      "trigger": "manual",
      "computed_at": "2026-11-04T10:00:00Z"
    }
  ]
}

Merge custom attributes

The type of a key is inferred at its first send and locked. null removes the key. 100 keys per account, snake_case, texts of 255 characters at most.

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): sandbox scope, test mode only.

PATH PARAMETERS

  • id string REQUIRED

BODY PARAMETERS

RETURNS

ERRORS

POST/v1/customers/{id}/attributes
curl https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/attributes \
  -H "Authorization: Bearer nc_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "attributes": {
      "plan": "pro",
      "seats": 12,
      "beta": true
    }
  }'
const res = await fetch("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/attributes", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "attributes": {
      "plan": "pro",
      "seats": 12,
      "beta": true
    }
  }),
});
const customer = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/attributes",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
    json={
      "attributes": {
        "plan": "pro",
        "seats": 12,
        "beta": True
      }
    },
)
customer = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/attributes")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
  "attributes" => {
    "plan" => "pro",
    "seats" => 12,
    "beta" => true
  }
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
customer = JSON.parse(res.body)
RESPONSE 200
{
  "object": "customer",
  "id": "cst_kWvFWUJMzhonPp9G",
  "stripe_customer_id": null,
  "external_id": "u_1",
  "stripe_account": null,
  "livemode": false,
  "email": "ana@acme.com",
  "company_domain": "acme.com",
  "name": null,
  "locale": null,
  "country": null,
  "region": null,
  "timezone": "Europe/Paris",
  "timezone_source": "account",
  "attributes": {
    "plan": "pro",
    "seats": 12,
    "beta": true
  },
  "churn_score": null,
  "churn_level": null,
  "flag": null,
  "state": "none",
  "plan": null,
  "mrr_cents": 0,
  "lifetime_value_cents": 0,
  "currency": null,
  "payment_method": null,
  "last_seen_at": null,
  "churned_at": null,
  "flows_paused_at": null,
  "signup_at": null,
  "created_at": "2026-11-04T10:00:00.000Z",
  "updated_at": "2026-11-04T10:00:00.000Z",
  "identifiers": []
}

When NoChurn would contact this customer now

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

PATH PARAMETERS

  • id string REQUIRED

QUERY PARAMETERS

  • flow enum

    Apply this flow's own send window.

    • prevention
    • recovery
    • cancel
    • winback

RETURNS

  • 200 the next slot in the customer's time zone, with the reasons of a delay — a SendSlot
GET/v1/customers/{id}/next_send_slot
curl https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/next_send_slot \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/next_send_slot", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const sendSlot = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/next_send_slot",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
send_slot = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/customers/cst_bTjTg5h1W9jXomCB/next_send_slot")
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) }
send_slot = JSON.parse(res.body)
RESPONSE 200
{
  "object": "send_slot",
  "customer": "cst_HzkSj31QvesBjiLa",
  "at": "2026-11-04T11:00:00+01:00",
  "timezone": "Europe/Berlin",
  "timezone_source": "address",
  "delayed": false,
  "reasons": [],
  "audience": "b2b"
}