API REFERENCE v1

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

BASE URL
https://api.nochurn.sh
YOUR FIRST CALL
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.

AUTHENTICATED REQUEST
# 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…"
401RESPONSE
{
  "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"
  }
}
409ERROR RESPONSE
{
  "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 in duplicates, not stored again.
  • A requested cancellation is cancelled once: a retry never cancels twice.
  • On its side, NoChurn sends an Idempotency-Key with 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.

POST/v1/usage_events
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.

GET/v1/customers
curl "https://api.nochurn.sh/v1/customers?limit=20&starting_after=cst_bTjTg5h1W9jXomCB" \
  -H "Authorization: Bearer nc_test_…"
200RESPONSE
{
  "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.

POST/v1/sandbox/customers
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.

NOCHURN-CLIENT
# 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"