API REFERENCE v1

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

RETURNS

POST/v1/widget/ping
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)
RESPONSE 200
{
  "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

BODY PARAMETERS

  • problems array of enums REQUIRED

    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

ERRORS

POST/v1/widget/guard_report
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)
RESPONSE 202
{
  "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

BODY PARAMETERS

  • key string

    Your publishable key, when the Authorization header cannot be sent (sendBeacon).

  • token string REQUIRED

    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.

  • events array of objects 50 items at most REQUIRED
    Show child parametersHide child parameters
    • name string REQUIRED
    • properties object

      Flat: strings (256), numbers, booleans or null; 20 keys, 4 KB.

    • timestamp string date-time

      Clamped to [now - 24 h, now].

    • idempotency_key string 64 characters at most REQUIRED

RETURNS

  • 202 a beacon: text/plain body, key in the body, no Authorization header — a WidgetEventBatch

ERRORS

POST/v1/widget/events
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)
RESPONSE 202
{
  "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)."
    }
  ]
}