GET STARTED
Quickstart
A test key, a first API call, your billing (Stripe or your own code), webhooks and the cancel flow, all in test mode.
Everything on this page runs in test mode: no real payment is retried and no email leaves NoChurn. A human switches to live mode in the dashboard once the integration works.
1. Get a test key
Create a free account at app.nochurn.sh, then open Developers and create a secret key. Test keys start with nc_test_. Keep the key on your server, for example in NOCHURN_API_KEY: never in front-end code, a mobile app or a committed file.
| Secret | Where it lives | Used for |
|---|---|---|
nc_test_… / nc_live_… secret key |
Your backend (NOCHURN_API_KEY) |
Every server-side call: Authorization: Bearer <key> |
pk_test_… / pk_live_… publishable key |
Your front end or app (public) | The cancel flow and the in-app screens, with a signed customer token |
ncws_… widget signing secret |
Your backend (NOCHURN_WIDGET_SECRET) |
Signing customer tokens |
whsec_… webhook secret |
Your backend (NOCHURN_WEBHOOK_SECRET) |
Verifying NoChurn-Signature on webhooks |
2. Make a first call
GET /v1/account returns the account of the key. A test key only ever sees test data.
curl https://api.nochurn.sh/v1/account \
-H "Authorization: Bearer $NOCHURN_API_KEY"const response = await fetch("https://api.nochurn.sh/v1/account", {
headers: { Authorization: `Bearer ${process.env.NOCHURN_API_KEY}` },
});
console.log(await response.json());import os
import requests
response = requests.get("https://api.nochurn.sh/v1/account",
headers={"Authorization": f"Bearer {os.environ['NOCHURN_API_KEY']}"}, timeout=10)
print(response.json())require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/account")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NOCHURN_API_KEY')}"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts JSON.parse(response.body)A wrong or missing key answers 401 with an error body: { "error": { "code", "message", "fix", "docs_url" } }. Every error has this shape, see Errors.
3. Connect your billing
With Stripe. In the dashboard, open Integrations and click Connect Stripe (Stripe Connect, test mode first). NoChurn imports customers, subscriptions, invoices and payment methods, then follows Stripe’s events. It never sees card numbers. Details: Stripe.
With another billing tool. Chargebee, Recurly, Maxio, Zuora, Paddle, Lemon Squeezy, Polar, Braintree, Mollie, GoCardless, Razorpay, the App Store and Google Play connect from Integrations with their API credentials: see Integrations.
With your own billing code. Send subscriptions and every payment attempt to the API. A failed payment opens the recovery flow; NoChurn then asks your backend to retry through the billing.retry_requested webhook. Amounts are in cents.
curl -X POST https://api.nochurn.sh/v1/subscriptions -H "Authorization: Bearer $NOCHURN_API_KEY" -H "Content-Type: application/json" -d '{"external_id":"sub_123","customer":{"external_id":"user_123","email":"jane@example.com"},"plan":"Pro","amount":4900,"currency":"usd","interval":"month","status":"active","current_period_end":"2026-12-05T00:00:00Z"}'
curl -X POST https://api.nochurn.sh/v1/payments -H "Authorization: Bearer $NOCHURN_API_KEY" -H "Content-Type: application/json" -d '{"external_id":"pay_123","customer":"user_123","subscription":"sub_123","invoice":"inv_123","amount":4900,"currency":"usd","status":"failed","decline_code":"insufficient_funds","invoice_url":"https://app.example.com/billing"}'const NOCHURN_API_URL = process.env.NOCHURN_API_URL || "https://api.nochurn.sh";
async function post(path, body) {
const response = await fetch(`${NOCHURN_API_URL}${path}`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.NOCHURN_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await response.json().catch(() => null);
if (!response.ok) throw new Error(`NoChurn: ${data?.error?.message} ${data?.error?.fix ?? ""}`);
return data;
}
await post("/v1/subscriptions", {
external_id: "sub_123", customer: { external_id: "user_123", email: "jane@example.com" },
plan: "Pro", amount: 4900, currency: "usd", interval: "month", status: "active",
current_period_end: "2026-12-05T00:00:00Z",
});
await post("/v1/payments", {
external_id: "pay_123", customer: "user_123", subscription: "sub_123", invoice: "inv_123",
amount: 4900, currency: "usd", status: "failed", decline_code: "insufficient_funds",
invoice_url: "https://app.example.com/billing",
});import os
import requests
NOCHURN_API_URL = os.environ.get("NOCHURN_API_URL", "https://api.nochurn.sh")
def post(path, body):
response = requests.post(f"{NOCHURN_API_URL}{path}", json=body, timeout=10,
headers={"Authorization": f"Bearer {os.environ['NOCHURN_API_KEY']}"})
if response.status_code >= 300:
error = response.json().get("error", {})
raise RuntimeError(f"NoChurn: {error.get('message')} {error.get('fix', '')}")
return response.json()
post("/v1/subscriptions", {"external_id": "sub_123", "customer": {"external_id": "user_123", "email": "jane@example.com"},
"plan": "Pro", "amount": 4900, "currency": "usd", "interval": "month", "status": "active",
"current_period_end": "2026-12-05T00:00:00Z"})
post("/v1/payments", {"external_id": "pay_123", "customer": "user_123", "subscription": "sub_123", "invoice": "inv_123",
"amount": 4900, "currency": "usd", "status": "failed", "decline_code": "insufficient_funds",
"invoice_url": "https://app.example.com/billing"})require "net/http"
require "json"
def nochurn_post(path, body)
uri = URI("#{ENV.fetch('NOCHURN_API_URL', 'https://api.nochurn.sh')}#{path}")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NOCHURN_API_KEY')}"
request["Content-Type"] = "application/json"
request.body = JSON.generate(body)
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
data = JSON.parse(response.body) rescue nil
raise "NoChurn: #{data&.dig('error', 'message')} #{data&.dig('error', 'fix')}" if response.code.to_i >= 300
data
end
nochurn_post("/v1/subscriptions", { external_id: "sub_123", customer: { external_id: "user_123", email: "jane@example.com" },
plan: "Pro", amount: 4900, currency: "usd", interval: "month", status: "active",
current_period_end: "2026-12-05T00:00:00Z" })
nochurn_post("/v1/payments", { external_id: "pay_123", customer: "user_123", subscription: "sub_123", invoice: "inv_123",
amount: 4900, currency: "usd", status: "failed", decline_code: "insufficient_funds",
invoice_url: "https://app.example.com/billing" })invoice_url is the page of your app where the customer updates their card: the recovery emails link to it. The full guide: Your own billing (API).
4. Identify your customers
Whatever the billing, send your own user id (external_id) and a few attributes from your backend, at signup and whenever they change. POST /v1/customers answers 409 when the customer already exists: then update it. The two base attributes the integration checklist looks for are signup_source and onboarding_done.
curl -X POST https://api.nochurn.sh/v1/customers -H "Authorization: Bearer $NOCHURN_API_KEY" -H "Content-Type: application/json" -d '{"external_id":"user_123","email":"jane@example.com","name":"Jane Doe"}'
curl -X POST https://api.nochurn.sh/v1/customers/user_123/attributes -H "Authorization: Bearer $NOCHURN_API_KEY" -H "Content-Type: application/json" -d '{"attributes":{"signup_source":"ads","onboarding_done":true}}'With Stripe, also put that id in the Stripe customer’s metadata.user_id (the key is configurable): NoChurn then links the Stripe customer to the one your app identified instead of creating a duplicate. More in Custom data and Concepts.
5. Receive webhooks
Add a route to your backend, register it (POST /v1/webhook_endpoints, or Developers › Webhooks in the dashboard), store the whsec_… secret it returns once, and verify NoChurn-Signature on every delivery. The code, in Node, Python and Ruby: Webhooks.
6. Open the cancel flow
Publish the cancel flow in test mode from the dashboard (Cancel flow), then open it from your cancel button. Your server signs a short customer token; your front end opens a cancel session with the publishable key and that token, shows each step, and keeps a Cancel now button visible at every step. The steps, the token code and the headless calls: Cancel modal.
7. Check the integration
GET /v1/integration/verify runs a 7-point checklist (billing connected, webhook verified, identify received, cancel flow opened, click to cancel, failed-payment email, no secret in code). Each point is pass, fail with its fix, or unknown. The sandbox scenarios play a failed payment, a removed card or a cancel flow for you. When everything passes, a human goes live in the dashboard.
Prefer to let your coding agent do it? Connect the NoChurn MCP server: it works in test mode and uses the same tested code.