API REFERENCE · 206 ENDPOINTS
Introduction
The NoChurn API is a REST API over HTTPS. Requests and responses are JSON. The base URL is https://api.nochurn.sh. Data is hosted in the EU.
Every id is prefixed by its object (cst_ customers, nsub_ subscriptions, ninv_ invoices, nevt_ events…), so it never collides with a Stripe id (cus_, sub_, in_). A customer can be referenced by its NoChurn id (cst_…), its Stripe id (cus_…) or your own id (external_id), see GET /v1/customers/resolve.
This reference covers the endpoints you call with a secret key from your server, and, last, the browser endpoints the script calls with a publishable key. It is generated from openapi.yaml (also as JSON).
CORE RESOURCES
RETENTION
DATA
DEVELOPERS
BROWSER · PUBLISHABLE KEY
https://api.nochurn.sh curl https://api.nochurn.sh/v1/customers \
-H "Authorization: Bearer nc_test_…" Authentication
Send your secret key in the Authorization header: Authorization: Bearer nc_test_… (or nc_live_…). The key sets the account and the mode: a nc_test_ key only sees test data. Keep secret keys on your server.
Restricted keys have a level per resource: none, read or write. A GET needs read, other methods need write, except a few POST that only read (an email preview). A key never creates or edits a key with more rights than its own.
Publishable keys (pk_test_…, pk_live_…) only work on the browser endpoints; anywhere else they get 403 forbidden. A browser request about one customer also sends a customer token in X-NoChurn-Customer-Token: a JWT your server signs (HS256) with your widget signing secret (GET /v1/account/widget_secret), with the claims kind (customer, stripe or external), value and exp. The request's Origin must be one of the key's allowed origins.
AI apps connected over MCP use an OAuth access token (ncat_…) with the scopes read, configure or sandbox. They work in test mode; live reads need the human's permission (403 live_read_not_granted), and a live write is never made directly (403 live_approval_required): the app asks a human with POST /v1/live_requests. Each operation says which scope it needs.
A missing, unknown or revoked key returns 401 unauthorized.
# Secret key, from your server
curl https://api.nochurn.sh/v1/customers \
-H "Authorization: Bearer nc_test_…"
# Browser endpoints: publishable key + customer token
curl https://api.nochurn.sh/v1/inapp/state \
-H "Authorization: Bearer pk_test_…" \
-H "X-NoChurn-Customer-Token: eyJhbGciOiJIUzI1NiJ9…" {
"error": {
"code": "unauthorized",
"message": "Missing, unknown or revoked API key.",
"fix": "Send a valid key in the Authorization header (\"Authorization: Bearer nc_live_…\"). Create one in the dashboard, under Developers › API keys.",
"docs_url": "https://nochurn.sh/docs/errors#unauthorized"
}
} Errors
Errors use the HTTP status and always the same body: { error: { code, message, fix, docs_url } }. code is stable, for your code; message says what happened and fix what to do; docs_url points to the code's section in Errors.
Branch on code, not on the message. 500 internal_error is safe to retry; 429 rate_limited gives retry_after in seconds.
{
"error": {
"code": "conflict",
"message": "The request conflicts with the current state of the resource.",
"fix": "Fetch the resource to see its current state before retrying.",
"docs_url": "https://nochurn.sh/docs/errors#conflict"
}
} Idempotency
Retrying a request never does the work twice where it matters:
- Subscriptions and payments sent by your billing are idempotent on your
external_id: posting the same one again updates it (POST /v1/subscriptions, POST /v1/payments). - Usage events are deduplicated on their
idempotency_key, per mode (POST /v1/usage_events, batch): a duplicate is counted induplicates, not stored again. - A requested cancellation is cancelled once: a retry never cancels twice.
- On its side, NoChurn sends an
Idempotency-Keywith every action it takes in Stripe, and deduplicates the webhooks it receives on their event id. Deduplicate the webhooks you receive on their event id too (Webhooks).
The API does not read an Idempotency-Key header: rely on the keys above.
curl https://api.nochurn.sh/v1/usage_events \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"customer": "u_1",
"name": "export_used",
"idempotency_key": "e1"
}'const res = await fetch("https://api.nochurn.sh/v1/usage_events", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"customer": "u_1",
"name": "export_used",
"idempotency_key": "e1"
}),
});
const usageEventBatch = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/usage_events",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"customer": "u_1",
"name": "export_used",
"idempotency_key": "e1"
},
)
usage_event_batch = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/usage_events")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"customer" => "u_1",
"name" => "export_used",
"idempotency_key" => "e1"
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
usage_event_batch = JSON.parse(res.body) Pagination
List endpoints return { object: "list", data, has_more, next_cursor }, most often newest first. Ask for up to limit items (1 to 100, default 20), then pass next_cursor as starting_after to get the next page while has_more is true.
An unknown cursor returns 400 invalid_request, and so does a limit outside 1 to 100. Where it is offered, include[]=total_count adds the count of the filtered list.
curl "https://api.nochurn.sh/v1/customers?limit=20&starting_after=cst_bTjTg5h1W9jXomCB" \
-H "Authorization: Bearer nc_test_…" {
"object": "list",
"data": [
{
"object": "customer",
"id": "cst_bTjTg5h1W9jXomCB",
"…": "…"
}
],
"has_more": true,
"next_cursor": "cst_bTjTg5h1W9jXomCB"
} Test mode
Every account has a test mode and a live mode, with separate data. A nc_test_ (or pk_test_) key only reads and writes test data, marked livemode: false. In test mode Stripe runs in test mode: no real payment is retried and no email leaves NoChurn.
The sandbox endpoints create test customers and play scenarios (a failed payment, a removed card, a cancel flow run); they refuse live mode with 403 test_mode_only. Every sample in this reference uses a test key.
curl https://api.nochurn.sh/v1/sandbox/customers \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"plan": "Team",
"attributes": {
"signup_source": "ads"
}
}'const res = await fetch("https://api.nochurn.sh/v1/sandbox/customers", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"plan": "Team",
"attributes": {
"signup_source": "ads"
}
}),
});
const sandboxCustomer = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/sandbox/customers",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"plan": "Team",
"attributes": {
"signup_source": "ads"
}
},
)
sandbox_customer = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/sandbox/customers")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"plan" => "Team",
"attributes" => {
"signup_source" => "ads"
}
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
sandbox_customer = JSON.parse(res.body) Versioning
The version is in the path: every endpoint starts with /v1.
NoChurn's own client code (the browser script, the MCP server, the SDKs) sends NoChurn-Client: <name>/<semver>, for example widget/1.0.0 or mcp/0.1.0, so that GET /v1/account/clients can tell you when one of them needs an update. Your own code does not need to send it.
BROWSER ENDPOINTS
The last group of this reference, BROWSER · PUBLISHABLE KEY, lists what the script (js.nochurn.sh) calls from your customer's browser: the cancel flow and its sessions, the in-app banner and wall, usage events. Call them yourself to build a headless cancel flow.
# Only NoChurn's own clients send this header (optional)
curl https://api.nochurn.sh/v1/account/clients \
-H "Authorization: Bearer nc_test_…" \
-H "NoChurn-Client: node/1.2.0"