BROWSER · PUBLISHABLE KEY
Widget
Called from your customer's browser, by the script (js.nochurn.sh) or your own code, with a publishable key (pk_test_…)
and a customer token signed by your server. See Authentication.
Calls made by the browser script (js.nochurn.sh) with a publishable key.
The widget loaded (onboarding: detected)
Publishable key + allowed Origin, no customer token. The first ping marks the widget as detected (Event widget.detected). Returns the key's fallback cancel link (DECISIONS 71).
- Publishable key (
pk_test_…). A secret key works too.
HEADER PARAMETERS
- Origin string
RETURNS
-
200detected — a WidgetPing
curl https://api.nochurn.sh/v1/widget/ping \
-X POST \
-H "Authorization: Bearer pk_test_…"const res = await fetch("https://api.nochurn.sh/v1/widget/ping", {
method: "POST",
headers: {
Authorization: "Bearer pk_test_…",
},
});
const widgetPing = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/widget/ping",
headers={
"Authorization": "Bearer pk_test_…",
},
)
widget_ping = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/widget/ping")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer pk_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
widget_ping = JSON.parse(res.body) {
"object": "widget_ping",
"detected_at": "2026-11-04T10:00:00Z",
"cancel_fallback_url": "http://localhost:3000/k/pk_test_cuzA5wx4ZZPE357jiSJReKBsj1iaeavi"
} The widget had to restore the hidden cancel button
Sent by the modal when its render-time check (click to cancel) found the cancel button hidden and repaired it. Publishable key only + customer token + allowed Origin. Same alert as the server-side CSS check: Event cancel_flow.css_fallback_applied (data.source widget), at most one per flow and mode per hour, and a To do item until the next publish. Limits: 10 reports a minute per key and customer, 60 per IP (429).
- Publishable key only (
pk_test_…), customer token (X-NoChurn-Customer-Token) when there is one. - A secret key is refused (
403 publishable_key_required).
HEADER PARAMETERS
- X-NoChurn-Customer-Token string
- Origin string
BODY PARAMETERS
-
What the guard repaired.
- display
- visibility
- opacity
- pointer-events
- contrast
- size
- font-size
- covered
- session string
The cancel session shown (ses_…), if any.
- css_from_code boolean
A theme or css was passed in code (NoChurn.cancel({ css })).
RETURNS
-
202recorded once an hour — a WidgetGuardReport
ERRORS
-
400invalid_request unknown problems -
401unauthorized no customer token -
403publishable_key_required a secret key
curl https://api.nochurn.sh/v1/widget/guard_report \
-H "Authorization: Bearer pk_test_…" \
-H "Content-Type: application/json" \
-d '{
"problems": ["display", "contrast"],
"session": "ses_jiTb8PYHFcUhWCni",
"css_from_code": true
}'const res = await fetch("https://api.nochurn.sh/v1/widget/guard_report", {
method: "POST",
headers: {
Authorization: "Bearer pk_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"problems": ["display", "contrast"],
"session": "ses_jiTb8PYHFcUhWCni",
"css_from_code": true
}),
});
const widgetGuardReport = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/widget/guard_report",
headers={
"Authorization": "Bearer pk_test_…",
},
json={
"problems": ["display", "contrast"],
"session": "ses_jiTb8PYHFcUhWCni",
"css_from_code": True
},
)
widget_guard_report = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/widget/guard_report")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer pk_test_…"
req["Content-Type"] = "application/json"
req.body = {
"problems" => ["display", "contrast"],
"session" => "ses_jiTb8PYHFcUhWCni",
"css_from_code" => true
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
widget_guard_report = JSON.parse(res.body) {
"object": "widget_guard_report",
"recorded": true,
"event": "nevt_aMee2SMCB7o7TYT8"
} Track usage events from the browser (page_view, NoChurn.track)
Called by js.nochurn.sh/v1 for your logged-in end user: page_view (path only, never the query string) and NoChurn.track(name, props). Publishable key only, in the Authorization header or as key in the body (navigator.sendBeacon cannot set headers); a secret key is refused (403 publishable_key_required). The customer is read from the signed widget token (token, HS256 { kind, value, exp }), never from the events. The body is JSON, as application/json or text/plain (a beacon, no preflight), 256 KB at most. 50 events per call. name: 1 to 64 of a-z 0-9 _ . : -; properties: flat, 20 keys, strings of 256 characters, numbers, booleans or null, 4 KB; timestamp: ISO 8601, clamped to the last 24 hours (the browser clock is not trusted); idempotency_key required (64 characters), deduplicated per mode. Errors are per event (rejected); an unknown customer rejects every event with customer_not_found. Limits: 120 events a minute per key and customer, 300 requests a minute per IP (429 rate_limited, retry_after). Neither the IP nor the user agent is stored.
- Publishable key (
pk_test_…) + customer token (X-NoChurn-Customer-Token), from an allowed Origin. - A secret key is refused (
403 publishable_key_required).
HEADER PARAMETERS
- Origin string
BODY PARAMETERS
- key string
Your publishable key, when the Authorization header cannot be sent (sendBeacon).
-
The widget token signed by your server (HS256 { kind, value, exp }), as for NoChurn.init.
- client string
The script's version, when the NoChurn-Client header cannot be sent (sendBeacon): GET /v1/account/clients.
-
Show child parametersHide child parameters
- properties object
Flat: strings (256), numbers, booleans or null; 20 keys, 4 KB.
-
Clamped to [now - 24 h, now].
RETURNS
-
202a beacon: text/plain body, key in the body, no Authorization header — a WidgetEventBatch
ERRORS
-
400invalid_request a body that is not JSON -
401unauthorized no widget token -
403publishable_key_required a secret key (never in a web page) -
429rate_limited over 120 events a minute for this key and customer
curl https://api.nochurn.sh/v1/widget/events \
-H "Authorization: Bearer pk_test_…" \
-H "Content-Type: application/json" \
-d '{
"token": "EXAMPLE00000000000000000",
"events": [
{
"name": "page_view",
"properties": {
"path": "/billing"
},
"timestamp": "2026-11-01T10:00:00Z",
"idempotency_key": "e1"
},
{
"name": "Export Used",
"idempotency_key": "e2"
},
{
"name": "export:csv",
"properties": {
"rows": 12,
"nested": {
"a": 1
}
},
"idempotency_key": "e3"
},
{
"name": "login"
}
]
}'const res = await fetch("https://api.nochurn.sh/v1/widget/events", {
method: "POST",
headers: {
Authorization: "Bearer pk_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"token": "EXAMPLE00000000000000000",
"events": [
{
"name": "page_view",
"properties": {
"path": "/billing"
},
"timestamp": "2026-11-01T10:00:00Z",
"idempotency_key": "e1"
},
{
"name": "Export Used",
"idempotency_key": "e2"
},
{
"name": "export:csv",
"properties": {
"rows": 12,
"nested": {
"a": 1
}
},
"idempotency_key": "e3"
},
{
"name": "login"
}
]
}),
});
const widgetEventBatch = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/widget/events",
headers={
"Authorization": "Bearer pk_test_…",
},
json={
"token": "EXAMPLE00000000000000000",
"events": [
{
"name": "page_view",
"properties": {
"path": "/billing"
},
"timestamp": "2026-11-01T10:00:00Z",
"idempotency_key": "e1"
},
{
"name": "Export Used",
"idempotency_key": "e2"
},
{
"name": "export:csv",
"properties": {
"rows": 12,
"nested": {
"a": 1
}
},
"idempotency_key": "e3"
},
{
"name": "login"
}
]
},
)
widget_event_batch = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/widget/events")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer pk_test_…"
req["Content-Type"] = "application/json"
req.body = {
"token" => "EXAMPLE00000000000000000",
"events" => [
{
"name" => "page_view",
"properties" => {
"path" => "/billing"
},
"timestamp" => "2026-11-01T10:00:00Z",
"idempotency_key" => "e1"
},
{
"name" => "Export Used",
"idempotency_key" => "e2"
},
{
"name" => "export:csv",
"properties" => {
"rows" => 12,
"nested" => {
"a" => 1
}
},
"idempotency_key" => "e3"
},
{
"name" => "login"
}
]
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
widget_event_batch = JSON.parse(res.body) {
"object": "widget_event_batch",
"accepted": 1,
"duplicates": 0,
"rejected": [
{
"index": 1,
"code": "invalid_request",
"message": "name \"Export Used\" is not snake_case."
},
{
"index": 2,
"code": "invalid_request",
"message": "properties: \"nested\" is not a string, number, boolean or null."
},
{
"index": 3,
"code": "invalid_request",
"message": "idempotency_key is required (64 characters at most)."
}
]
}