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
- reason enum
- card_removed
- card_expiring
-
- 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.
- lifetime_value_cents integer
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).
-
From the last_active_at attribute your app sends.
-
Set by Pause all flows: no flow acts on this customer.
- 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
- 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
-
- 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
-
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.
-
Factors that raised the last churn score, largest first (starting point and lowering factors left out).
-
Schema: Customer
{
"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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth):
sandboxscope, 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
-
201created — the Customer object -
200the only customer with this email, linked to your external_id — the Customer object
ERRORS
-
400invalid_request no identifier -
409conflict external_id already used
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) {
"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"
}
]
}{
"object": "customer",
"id": "cst_pzfcpuSfUFQSGkNf",
"stripe_customer_id": "cus_123",
"external_id": "u_ana",
"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_ana",
"source": "api",
"created_at": "2026-11-04T10:00:00.000Z"
}
]
} Retrieve a customer
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
-
cst_…, cus_… or your own id.
QUERY PARAMETERS
- stripe_account string
RETURNS
-
200customer — the Customer object
ERRORS
-
404customer_not_found not found
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) {
"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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth):
sandboxscope, test mode only.
PATH PARAMETERS
-
cst_…, cus_… or your own id.
QUERY PARAMETERS
- stripe_account string
BODY PARAMETERS
- external_id string
- 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
-
200time zone set by your app, then back to the guess — the Customer object
ERRORS
-
409conflict external_id already used by another customer -
422invalid_request unknown time zone
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) {
"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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
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.
-
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.
-
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
- starting_after string
RETURNS
-
200at risk and renewing within 7 days, with why and when — a list of Customer object (CustomerPage)
ERRORS
-
400invalid_request renews_within_days out of range
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) {
"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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
-
cst_…, cus_… or your id.
QUERY PARAMETERS
- stripe_account string
acct_… when the same cus_ exists on two connected Stripe accounts.
RETURNS
-
200paid: full, nothing to pay — a CustomerAccess
ERRORS
-
404customer_not_found customer of another account
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) {
"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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
RETURNS
-
200summary — a CustomerSummary
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) {
"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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
-
cst_…, cus_… or your id.
RETURNS
-
200profile — a CustomerProfile
ERRORS
-
404customer_not_found customer of another account
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) {
"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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
QUERY PARAMETERS
- starting_after string
RETURNS
-
200second page — a TimelinePage
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) {
"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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
RETURNS
-
200paused — the Customer object
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) {
"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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
RETURNS
-
200resumed — the Customer object
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) {
"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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
RETURNS
-
200summary of the current mode — a CustomerDuplicateSummary
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) {
"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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
RETURNS
-
202scan queued — a CustomerDuplicateRun
ERRORS
-
429rate_limited scanned less than a minute ago
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) {
"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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
RETURNS
-
200merged now — a CustomerDuplicateRun -
202many pairs: in the background — a CustomerDuplicateRun
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) {
"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"
}{
"object": "customer_duplicate_run",
"id": "cdr_JM2D5yebmkpCcrDU",
"livemode": false,
"kind": "merge_all",
"status": "queued",
"total": 12,
"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"
} 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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
QUERY PARAMETERS
- customer string
- stripe_customer string
- external_id string
- stripe_account string
acct_… — needed when the same cus_ exists on two connected Stripe accounts.
RETURNS
-
200the customer and its three ids — the Customer object
ERRORS
-
404customer_not_found no match -
409ambiguous_customer ambiguous
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) {
"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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
QUERY PARAMETERS
-
- open
- dismissed
- merged
- starting_after string
RETURNS
-
200open proposals of the current mode — a list of CustomerDuplicate (CustomerDuplicateList)
ERRORS
-
400invalid_request unknown status
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) {
"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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
RETURNS
-
200dismissed — a CustomerDuplicate
ERRORS
-
404not_found unknown or in another account
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) {
"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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
BODY PARAMETERS
-
The customer to keep: one of the two (suggested_keep is the default to offer).
RETURNS
-
200the same merge again: the same answer — a CustomerDuplicate
ERRORS
-
400invalid_request keep is not one of the two -
404not_found in another account or the other mode -
409conflict already merged into the other customer
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) {
"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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
RETURNS
-
200the analysis — a CustomerInsight
ERRORS
-
422insufficient_data not enough activity for an analysis (insufficient_data, no AI call)
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) {
"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"
} A customer's company logo
The favicon of a company domain, for the customer's avatar: an image (PNG most often; never SVG), Cache-Control: private, max-age=86400. Only a domain that is the company_domain of a customer of the account answers (404 otherwise: never an open proxy). NoChurn fetches it from Google's favicon service and caches it 30 days: Google never sees who looks at it. 404 with an empty body when the domain has no logo: show your default avatar. A session matches customers of both modes; an API key, those of its mode. Resource customers (read).
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
-
A customer's company_domain.
QUERY PARAMETERS
- account string
Dashboard session only: the account, in place of the NoChurn-Account header that an <img> cannot send.
RETURNS
-
200the logo
ERRORS
-
404no logo (empty body), or no customer of the account has this company domain (JSON error)
curl https://api.nochurn.sh/v1/logos/{domain} \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/logos/{domain}", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const data = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/logos/{domain}",
headers={
"Authorization": "Bearer nc_test_…",
},
)
data = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/logos/{domain}")
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) }
data = JSON.parse(res.body) // No example 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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
-
cst_…, cus_… or your own id.
RETURNS
-
200the score — a ChurnScore
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) {
"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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth):
sandboxscope, test mode only.
PATH PARAMETERS
BODY PARAMETERS
RETURNS
-
200merged — the Customer object
ERRORS
-
422attribute_type_mismatch wrong type for a locked attribute: nothing is saved
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) {
"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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
QUERY PARAMETERS
- flow enum
Apply this flow's own send window.
- prevention
- recovery
- cancel
- winback
RETURNS
-
200the next slot in the customer's time zone, with the reasons of a delay — a SendSlot
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) {
"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"
}