GUIDES
Custom data
Send your own customer attributes and usage events, so the churn score and the churn drivers know what your billing cannot tell.
Your billing knows what customers pay. Your app knows how they got there and whether they use it. Send that to NoChurn as attributes (facts about a customer) and usage events (what they do). They feed the churn score, the churn drivers of Data & AI, segments and offers.
Attributes
POST /v1/customers/{id}/attributes merges attributes into a customer ({id} is any of its ids). You can also pass attributes when you create or update a customer.
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,"seats":3,"activated_at":"2026-03-16"}}'// user: { id, email, name }; attributes: e.g. { signup_source: "ads", onboarding_done: true }.
// Creates the customer (409 if it already exists: then updates it), then sends the attributes.
const NOCHURN_API_URL = process.env.NOCHURN_API_URL || "https://api.nochurn.sh";
async function nochurn(method, path, body) {
const response = await fetch(`${NOCHURN_API_URL}${path}`, {
method,
headers: { Authorization: `Bearer ${process.env.NOCHURN_API_KEY}`, "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const data = await response.json().catch(() => null);
return { status: response.status, ok: response.ok, data };
}
export async function identifyCustomer(user, attributes) {
const id = encodeURIComponent(String(user.id));
const created = await nochurn("POST", "/v1/customers", { external_id: String(user.id), email: user.email, name: user.name });
if (created.status === 409) {
await nochurn("PATCH", `/v1/customers/${id}`, { email: user.email, name: user.name });
} else if (!created.ok) {
throw new Error(`NoChurn: ${created.data?.error?.message} ${created.data?.error?.fix ?? ""}`);
}
const result = await nochurn("POST", `/v1/customers/${id}/attributes`, { attributes });
if (!result.ok) throw new Error(`NoChurn: ${result.data?.error?.message} ${result.data?.error?.fix ?? ""}`);
return result.data;
}import os
from urllib.parse import quote
import requests
NOCHURN_API_URL = os.environ.get("NOCHURN_API_URL", "https://api.nochurn.sh")
def _headers():
return {"Authorization": f"Bearer {os.environ['NOCHURN_API_KEY']}", "Content-Type": "application/json"}
def _raise_for(response):
if response.status_code >= 300:
error = (response.json() or {}).get("error", {})
raise RuntimeError(f"NoChurn: {error.get('message')} {error.get('fix', '')}")
def identify_customer(user, attributes):
customer_id = quote(str(user["id"]), safe="")
created = requests.post(f"{NOCHURN_API_URL}/v1/customers", headers=_headers(), timeout=10,
json={"external_id": str(user["id"]), "email": user.get("email"), "name": user.get("name")})
if created.status_code == 409:
requests.patch(f"{NOCHURN_API_URL}/v1/customers/{customer_id}", headers=_headers(), timeout=10,
json={"email": user.get("email"), "name": user.get("name")})
else:
_raise_for(created)
response = requests.post(f"{NOCHURN_API_URL}/v1/customers/{customer_id}/attributes", headers=_headers(), timeout=10,
json={"attributes": attributes})
_raise_for(response)
return response.json()require "net/http"
require "json"
require "uri"
require "erb"
def nochurn_request(method, path, body = nil)
uri = URI("#{ENV.fetch('NOCHURN_API_URL', 'https://api.nochurn.sh')}#{path}")
request = Net::HTTP.const_get(method.capitalize).new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NOCHURN_API_KEY')}"
request["Content-Type"] = "application/json"
request.body = JSON.generate(body) if body
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
[ response.code.to_i, (JSON.parse(response.body) rescue nil) ]
end
def identify_customer(user, attributes)
id = ERB::Util.url_encode(user[:id].to_s)
status, data = nochurn_request(:post, "/v1/customers", { external_id: user[:id].to_s, email: user[:email], name: user[:name] })
if status == 409
nochurn_request(:patch, "/v1/customers/#{id}", { email: user[:email], name: user[:name] })
elsif status >= 300
raise "NoChurn: #{data&.dig('error', 'message')} #{data&.dig('error', 'fix')}"
end
status, data = nochurn_request(:post, "/v1/customers/#{id}/attributes", { attributes: attributes })
raise "NoChurn: #{data&.dig('error', 'message')} #{data&.dig('error', 'fix')}" if status >= 300
data
endRules and limits
- Keys: snake_case, starting with a letter, 40 characters at most (
signup_source, notSignup Source). - Values: text (255 characters at most), numbers, booleans or dates (
2026-03-16or ISO 8601). Flatten objects and lists.nullremoves the key from the customer. - Types are locked: the type of a key is set by its first value; another type later answers
422attribute_type_mismatchand writes nothing.POST /v1/attribute_definitionsdeclares a key and its type before the first send. - 100 keys per account and mode. One more answers
attribute_limit_reached. - Sensitive attributes: mark a key sensitive (
PATCH /v1/attribute_definitions/{key}) and it is never sent to the AI. - Test and live mode keep separate attributes.
What to send first
signup_source and onboarding_done are the two the integration checklist looks for. Then, as you have them: signup_date, plan and billing_interval, company_size or seats, activated_at, last_active_at, the usage of your key feature, support_tickets or nps, country and industry, coupon_used, trial_days. The Custom data & AI page says which question each one answers.
Send them at signup and whenever they change (a signup handler, an after-save hook, a background job).
Usage events
POST /v1/usage_events records what a customer does: { customer, name, timestamp?, properties?, idempotency_key? }. POST /v1/usage_events/batch takes up to 1,000 events. Events are deduplicated on idempotency_key, rolled up each night into the attributes last_active_at, events_30d and <event>_30d, and never sent to the AI.
curl -X POST https://api.nochurn.sh/v1/usage_events -H "Authorization: Bearer $NOCHURN_API_KEY" -H "Content-Type: application/json" -d '{"customer":"user_123","name":"project_created","properties":{"template":"blank"},"idempotency_key":"evt_42"}'From the browser, NoChurn’s script sends page_view and your own events to POST /v1/widget/events with the publishable key and the signed customer token: no secret key in the front end.
CSV import
POST /v1/imports takes a CSV of customers and attributes (multipart file, or the text as csv). Columns are mapped by name: customer, external_id, email, stripe_customer, name, country, locale; any other column becomes an attribute. dry_run returns the mapping and the first rows without writing. GET /v1/imports/{id} gives the error report.
What comes out
Data & AI ranks the attributes most linked to churn (GET /v1/insights), with the sample size of each group: correlation, not cause. The churn score of each customer (GET /v1/customers/{id}/score) uses them too.