API REFERENCE v1

DEVELOPERS

Account

The Account object

Account settings, setup progress and sending schedule.

ATTRIBUTES

Show 8 more attributesHide 8 attributes

Schema: Account

THE ACCOUNT OBJECT
{
  "object": "account",
  "id": "acc_pApr4brLkk4KGSjf",
  "name": "Account 1",
  "billing_source": "stripe",
  "sandbox": true,
  "default_locale": "en",
  "plan": "free",
  "external_id_metadata_key": "user_id",
  "require_two_factor": false,
  "timezone": "Europe/Paris",
  "audience": null,
  "reporting_currency": null,
  "onboarding": {
    "step": "billing",
    "skipped": [],
    "completed_at": null
  },
  "disputes_settings": {
    "auto_submit": false,
    "auto_submit_hours_before": 24,
    "auto_refund_fraud_warnings": false,
    "auto_refund_max_cents": null,
    "cancel_subscription_on_fraud_refund": true,
    "notify_email": true,
    "notify_slack": false,
    "contest_cost_cents": null,
    "contest_cost_currency": null,
    "auto_submit_min_score": 70
  },
  "created_at": "2026-11-04T10:00:00.000Z",
  "updated_at": "2026-11-04T10:00:00.000Z"
}

The account of the API key (or of the dashboard session)

  • Secret key (nc_test_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

RETURNS

ERRORS

GET/v1/account
curl https://api.nochurn.sh/v1/account \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/account", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const account = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/account",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
account = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/account")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer nc_test_…"

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
account = JSON.parse(res.body)
RESPONSE 200
{
  "object": "account",
  "id": "acc_pApr4brLkk4KGSjf",
  "name": "Account 1",
  "billing_source": "stripe",
  "sandbox": true,
  "default_locale": "en",
  "plan": "free",
  "external_id_metadata_key": "user_id",
  "require_two_factor": false,
  "timezone": "Europe/Paris",
  "audience": null,
  "reporting_currency": null,
  "onboarding": {
    "step": "billing",
    "skipped": [],
    "completed_at": null
  },
  "disputes_settings": {
    "auto_submit": false,
    "auto_submit_hours_before": 24,
    "auto_refund_fraud_warnings": false,
    "auto_refund_max_cents": null,
    "cancel_subscription_on_fraud_refund": true,
    "notify_email": true,
    "notify_slack": false,
    "contest_cost_cents": null,
    "contest_cost_currency": null,
    "auto_submit_min_score": 70
  },
  "created_at": "2026-11-04T10:00:00.000Z",
  "updated_at": "2026-11-04T10:00:00.000Z"
}

Update account settings

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

BODY PARAMETERS

RETURNS

ERRORS

PATCH/v1/account
curl https://api.nochurn.sh/v1/account \
  -X PATCH \
  -H "Authorization: Bearer nc_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Renamed",
    "external_id_metadata_key": "app_user_id"
  }'
const res = await fetch("https://api.nochurn.sh/v1/account", {
  method: "PATCH",
  headers: {
    Authorization: "Bearer nc_test_…",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "Renamed",
    "external_id_metadata_key": "app_user_id"
  }),
});
const account = await res.json();
import requests

res = requests.patch(
    "https://api.nochurn.sh/v1/account",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
    json={
      "name": "Renamed",
      "external_id_metadata_key": "app_user_id"
    },
)
account = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/account")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
  "name" => "Renamed",
  "external_id_metadata_key" => "app_user_id"
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
account = JSON.parse(res.body)
RESPONSE 200
{
  "object": "account",
  "id": "acc_hM7usMgmWPctiKMY",
  "name": "Renamed",
  "billing_source": "stripe",
  "sandbox": true,
  "default_locale": "en",
  "plan": "free",
  "external_id_metadata_key": "app_user_id",
  "require_two_factor": false,
  "timezone": "Europe/Paris",
  "audience": null,
  "reporting_currency": null,
  "onboarding": {
    "step": "billing",
    "skipped": [],
    "completed_at": null
  },
  "disputes_settings": {
    "auto_submit": false,
    "auto_submit_hours_before": 24,
    "auto_refund_fraud_warnings": false,
    "auto_refund_max_cents": null,
    "cancel_subscription_on_fraud_refund": true,
    "notify_email": true,
    "notify_slack": false,
    "contest_cost_cents": null,
    "contest_cost_currency": null,
    "auto_submit_min_score": 70
  },
  "created_at": "2026-11-04T10:00:00.000Z",
  "updated_at": "2026-11-04T10:00:00.000Z"
}

Versions of NoChurn's client code calling the API

Versions of NoChurn's own client code (the widget js.nochurn.sh, the MCP server, the SDKs) seen in the last 90 days in this mode, from the NoChurn-Client header (<name>/<semver>), per client with its latest and minimum supported version. Row status: current, update_available (below latest), unsupported (below minimum), unknown (a client NoChurn does not publish). The widget's origin_host is the host of the page that loaded it; api_key is the key used, oauth_client the AI app (MCP through OAuth). Rows are written at most once every 5 minutes: last_seen_at is within 5 minutes and calls_count is a lower bound. Resource account (read), every role. js.nochurn.sh/v1 updates itself within a major version: update_available for the widget means a pinned /v1.x.y.js URL, a self-hosted copy or an older major.

  • Secret key (nc_test_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

RETURNS

ERRORS

GET/v1/account/clients
curl https://api.nochurn.sh/v1/account/clients \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/account/clients", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const clientVersions = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/account/clients",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
client_versions = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/account/clients")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer nc_test_…"

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
client_versions = JSON.parse(res.body)
RESPONSE 200
{
  "object": "client_versions",
  "livemode": false,
  "window_days": 90,
  "data": [
    {
      "object": "client",
      "client": "widget",
      "name": "Cancel widget (js.nochurn.sh)",
      "latest": "1.0.0",
      "minimum": "1.0.0",
      "status": "unsupported",
      "versions": [
        {
          "id": "clv_fv7a8FvU8CUJZJ8F",
          "version": "1.0.0",
          "status": "current",
          "origin_host": "app.acme.com",
          "api_key": {
            "id": "key_ytLQLbZVZ2GdVyUF",
            "name": "Key 1",
            "kind": "publishable",
            "mode": "test",
            "last4": "ntgP"
          },
          "oauth_client": null,
          "first_seen_at": "2026-11-04T09:00:00.000Z",
          "last_seen_at": "2026-11-04T09:00:00.000Z",
          "calls_count": 3
        },
        {
          "id": "clv_nRfcm9YjNjXUcMqC",
          "version": "0.9.0",
          "status": "unsupported",
          "origin_host": "app.acme.com",
          "api_key": {
            "id": "key_ytLQLbZVZ2GdVyUF",
            "name": "Key 1",
            "kind": "publishable",
            "mode": "test",
            "last4": "ntgP"
          },
          "oauth_client": null,
          "first_seen_at": "2026-11-02T10:00:00.000Z",
          "last_seen_at": "2026-11-02T10:00:00.000Z",
          "calls_count": 3
        }
      ]
    },
    {
      "object": "client",
      "client": "mcp",
      "name": "NoChurn MCP server",
      "latest": "0.1.0",
      "minimum": "0.1.0",
      "status": "current",
      "versions": [
        {
          "id": "clv_E1d1eodQeZi9yoUJ",
          "version": "0.1.0",
          "status": "current",
          "origin_host": null,
          "api_key": null,
          "oauth_client": {
            "id": "mcpc_EioDePP8sJxkMntr",
            "name": "Claude Code"
          },
          "first_seen_at": "2026-11-04T09:00:00.000Z",
          "last_seen_at": "2026-11-04T09:00:00.000Z",
          "calls_count": 3
        }
      ]
    },
    {
      "object": "client",
      "client": "node",
      "name": null,
      "latest": null,
      "minimum": null,
      "status": "unknown",
      "versions": [
        {
          "id": "clv_L5QU6Xo2vi6b7TLV",
          "version": "1.2.0",
          "status": "unknown",
          "origin_host": null,
          "api_key": {
            "id": "key_HCwE3oWH5fUAkZ91",
            "name": "Server",
            "kind": "secret",
            "mode": "test",
            "last4": "51K6"
          },
          "oauth_client": null,
          "first_seen_at": "2026-11-04T09:00:00.000Z",
          "last_seen_at": "2026-11-04T09:00:00.000Z",
          "calls_count": 3
        }
      ]
    }
  ]
}

Email a setup step to a colleague

« Send to my IT team » / « Email it to a developer ». 10 a day.

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

BODY PARAMETERS

  • email string REQUIRED
  • topic enum REQUIRED
    • email_domain
    • api
    • cancel_modal
  • note string

RETURNS

ERRORS

POST/v1/account/setup/share
curl https://api.nochurn.sh/v1/account/setup/share \
  -H "Authorization: Bearer nc_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "it@acme.com",
    "topic": "email_domain"
  }'
const res = await fetch("https://api.nochurn.sh/v1/account/setup/share", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "email": "it@acme.com",
    "topic": "email_domain"
  }),
});
const setupShare = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/account/setup/share",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
    json={
      "email": "it@acme.com",
      "topic": "email_domain"
    },
)
setup_share = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/account/setup/share")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
  "email" => "it@acme.com",
  "topic" => "email_domain"
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
setup_share = JSON.parse(res.body)
RESPONSE 201
{
  "object": "setup_share",
  "topic": "email_domain",
  "email": "it@acme.com",
  "sent_at": "2026-11-04T10:00:00Z"
}

Flows that set their own send window options

In the current mode: the options the published version of each flow sets itself (instead of following Settings › Send schedule), those its draft sets, and the segments (THEN) and A/B variants of the flow that set them (no mode: listed in both; the variants of a running test with applies false). Only flows with at least one.

  • Secret key (nc_test_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

QUERY PARAMETERS

  • keys[] array of enums

    Only these options (all by default).

    • audience
    • respect_customer_timezone
    • skip_weekends
    • skip_public_holidays
    • windows
    • quiet_hours
    • days_off

RETURNS

GET/v1/account/send_schedule/flow_overrides
curl https://api.nochurn.sh/v1/account/send_schedule/flow_overrides \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/account/send_schedule/flow_overrides", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const page = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/account/send_schedule/flow_overrides",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
page = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/account/send_schedule/flow_overrides")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer nc_test_…"

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
page = JSON.parse(res.body)
RESPONSE 200
{
  "object": "list",
  "data": [
    {
      "object": "send_schedule_flow_override",
      "flow": {
        "id": "flw_3yDzGfxYA3qXrkwX",
        "kind": "recovery",
        "name": "Recovery flow"
      },
      "version": {
        "id": "flv_dS81MgAgS7sx1js4",
        "number": 1,
        "published_to": "live"
      },
      "published": {
        "quiet_hours": {
          "to": "08:00",
          "from": "21:00",
          "enabled": true
        }
      },
      "draft": {
        "quiet_hours": {
          "to": "08:00",
          "from": "21:00",
          "enabled": true
        }
      },
      "segments": [],
      "variants": []
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Make flows follow Settings › Send schedule

Sets the options to null (follow the account) in the flows that set them: the published version of the current mode is copied into a new version with only those options changed (other draft changes are never published), and the draft loses them too. The segments (THEN) and A/B variants of those flows lose them in place (removed, so they follow their flow; they have no mode and no versions, so this changes both modes). The variants of a running test are not changed. Owners and admins. Nothing to change: nothing happens.

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

BODY PARAMETERS

  • keys array of enums

    All options by default.

    • audience
    • respect_customer_timezone
    • skip_weekends
    • skip_public_holidays
    • windows
    • quiet_hours
    • days_off
  • flow_ids array of strings

    All flows that override by default. A flow's segments and A/B variants are applied with it.

RETURNS

ERRORS

POST/v1/account/send_schedule/apply_to_flows
curl https://api.nochurn.sh/v1/account/send_schedule/apply_to_flows \
  -H "Authorization: Bearer nc_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "keys": ["quiet_hours", "respect_customer_timezone"],
    "flow_ids": ["flw_cRisxrrykqgKPHdX"]
  }'
const res = await fetch("https://api.nochurn.sh/v1/account/send_schedule/apply_to_flows", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "keys": ["quiet_hours", "respect_customer_timezone"],
    "flow_ids": ["flw_cRisxrrykqgKPHdX"]
  }),
});
const sendScheduleFlowsApplied = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/account/send_schedule/apply_to_flows",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
    json={
      "keys": ["quiet_hours", "respect_customer_timezone"],
      "flow_ids": ["flw_cRisxrrykqgKPHdX"]
    },
)
send_schedule_flows_applied = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/account/send_schedule/apply_to_flows")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
  "keys" => ["quiet_hours", "respect_customer_timezone"],
  "flow_ids" => ["flw_cRisxrrykqgKPHdX"]
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
send_schedule_flows_applied = JSON.parse(res.body)
RESPONSE 200
{
  "object": "send_schedule_flows_applied",
  "livemode": true,
  "applied": 1,
  "counts": {
    "flows": 1,
    "segments": 0,
    "variants": 0
  },
  "data": [
    {
      "object": "send_schedule_flow_applied",
      "flow": {
        "id": "flw_cRisxrrykqgKPHdX",
        "kind": "recovery",
        "name": "Recovery flow"
      },
      "version": {
        "id": "flv_taVfx8DNTK3tKcwE",
        "number": 2,
        "published_to": "live"
      },
      "published": {
        "quiet_hours": {
          "to": "08:00",
          "from": "21:00",
          "enabled": true
        },
        "respect_customer_timezone": true
      },
      "draft": {
        "quiet_hours": {
          "to": "08:00",
          "from": "21:00",
          "enabled": true
        },
        "respect_customer_timezone": true
      },
      "segments": [],
      "variants": []
    }
  ]
}

When NoChurn may contact your customers

  • Secret key (nc_test_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

RETURNS

GET/v1/account/send_schedule
curl https://api.nochurn.sh/v1/account/send_schedule \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/account/send_schedule", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const sendSchedule = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/account/send_schedule",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
send_schedule = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/account/send_schedule")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer nc_test_…"

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
send_schedule = JSON.parse(res.body)
RESPONSE 200
{
  "object": "send_schedule",
  "audience": null,
  "overrides": {},
  "effective": {
    "audience": "b2b",
    "respect_customer_timezone": true,
    "quiet_hours": {
      "enabled": true,
      "from": "21:00",
      "to": "08:00"
    },
    "days_off": [],
    "skip_weekends": true,
    "skip_public_holidays": true,
    "windows": {
      "weekday": [
        ["09:00", "17:00"]
      ],
      "weekend": []
    }
  },
  "presets": {
    "b2b": {
      "respect_customer_timezone": true,
      "quiet_hours": {
        "enabled": true,
        "from": "21:00",
        "to": "08:00"
      },
      "days_off": [],
      "skip_weekends": true,
      "skip_public_holidays": true,
      "windows": {
        "weekday": [
          ["09:00", "17:00"]
        ],
        "weekend": []
      }
    },
    "b2c": {
      "respect_customer_timezone": true,
      "quiet_hours": {
        "enabled": true,
        "from": "21:00",
        "to": "08:00"
      },
      "days_off": [],
      "skip_weekends": false,
      "skip_public_holidays": false,
      "windows": {
        "weekday": [
          ["11:00", "13:00"],
          ["18:00", "21:00"]
        ],
        "weekend": [
          ["10:00", "20:00"]
        ]
      }
    }
  },
  "suggested_audience": null
}

Change the audience or any option

Each option can be switched on or off; null removes an override (back to the preset).

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): not available.

BODY PARAMETERS

RETURNS

ERRORS

PATCH/v1/account/send_schedule
curl https://api.nochurn.sh/v1/account/send_schedule \
  -X PATCH \
  -H "Authorization: Bearer nc_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "audience": "b2c",
    "days_off": ["2026-12-24"]
  }'
const res = await fetch("https://api.nochurn.sh/v1/account/send_schedule", {
  method: "PATCH",
  headers: {
    Authorization: "Bearer nc_test_…",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "audience": "b2c",
    "days_off": ["2026-12-24"]
  }),
});
const sendSchedule = await res.json();
import requests

res = requests.patch(
    "https://api.nochurn.sh/v1/account/send_schedule",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
    json={
      "audience": "b2c",
      "days_off": ["2026-12-24"]
    },
)
send_schedule = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/account/send_schedule")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
  "audience" => "b2c",
  "days_off" => ["2026-12-24"]
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
send_schedule = JSON.parse(res.body)
RESPONSE 200
{
  "object": "send_schedule",
  "audience": "b2c",
  "overrides": {
    "days_off": ["2026-12-24"]
  },
  "effective": {
    "audience": "b2c",
    "respect_customer_timezone": true,
    "quiet_hours": {
      "enabled": true,
      "from": "21:00",
      "to": "08:00"
    },
    "days_off": ["2026-12-24"],
    "skip_weekends": false,
    "skip_public_holidays": false,
    "windows": {
      "weekday": [
        ["11:00", "13:00"],
        ["18:00", "21:00"]
      ],
      "weekend": [
        ["10:00", "20:00"]
      ]
    }
  },
  "presets": {
    "b2b": {
      "respect_customer_timezone": true,
      "quiet_hours": {
        "enabled": true,
        "from": "21:00",
        "to": "08:00"
      },
      "days_off": [],
      "skip_weekends": true,
      "skip_public_holidays": true,
      "windows": {
        "weekday": [
          ["09:00", "17:00"]
        ],
        "weekend": []
      }
    },
    "b2c": {
      "respect_customer_timezone": true,
      "quiet_hours": {
        "enabled": true,
        "from": "21:00",
        "to": "08:00"
      },
      "days_off": [],
      "skip_weekends": false,
      "skip_public_holidays": false,
      "windows": {
        "weekday": [
          ["11:00", "13:00"],
          ["18:00", "21:00"]
        ],
        "weekend": [
          ["10:00", "20:00"]
        ]
      }
    }
  },
  "suggested_audience": null
}

Setup checklist: what is installed and what is missing

  • Secret key (nc_test_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

RETURNS

  • 200 billing by API: no Stripe import step; an active support tool counts — a Setup
GET/v1/account/setup
curl https://api.nochurn.sh/v1/account/setup \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/account/setup", {
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const setup = await res.json();
import requests

res = requests.get(
    "https://api.nochurn.sh/v1/account/setup",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
setup = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/account/setup")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer nc_test_…"

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
setup = JSON.parse(res.body)
RESPONSE 200
{
  "object": "setup",
  "livemode": false,
  "billing_source": "stripe",
  "api_base_url": "https://api.nochurn.sh",
  "complete": false,
  "steps": [
    {
      "key": "billing_connected",
      "done": true,
      "fix": null,
      "skipped": false
    },
    {
      "key": "billing_synced",
      "done": true,
      "fix": null,
      "skipped": false
    },
    {
      "key": "history_imported",
      "done": false,
      "fix": "Wait for the 12-month history import to finish, or re-run the import from Integrations.",
      "skipped": false
    },
    {
      "key": "webhook_received",
      "done": false,
      "fix": "Trigger an event in your billing tool (e.g. update a customer) and check it arrives.",
      "skipped": false
    },
    {
      "key": "api_key_created",
      "done": true,
      "fix": null,
      "skipped": false
    },
    {
      "key": "first_api_call",
      "done": true,
      "fix": null,
      "skipped": false
    },
    {
      "key": "support_connected",
      "done": false,
      "fix": "Connect your support tool (Intercom, Zendesk, Crisp, HubSpot, Help Scout or Gorgias) from Integrations.",
      "skipped": false
    },
    {
      "key": "cancel_modal_detected",
      "done": false,
      "fix": "Install the cancel modal on your cancel button: NoChurn detects its first load.",
      "skipped": false
    },
    {
      "key": "email_domain_verified",
      "done": false,
      "fix": "Add your sending domain and its DNS records, then check them (Settings › Email domain).",
      "skipped": false
    },
    {
      "key": "cancel_flow_published",
      "done": false,
      "fix": "Publish your cancel flow (test mode first) from Cancel flow › Settings.",
      "skipped": false
    },
    {
      "key": "recovery_retries_configured",
      "done": true,
      "fix": null,
      "skipped": false
    },
    {
      "key": "prevention_signals_enabled",
      "done": true,
      "fix": null,
      "skipped": false
    }
  ],
  "onboarding": {
    "step": "billing",
    "skipped": [],
    "completed_at": null
  },
  "counters": {
    "customers_imported": 0,
    "active_customers": 0,
    "mrr": [],
    "failed_payments_to_recover": {
      "count": 0,
      "amounts": []
    },
    "high_risk_customers": 0,
    "scored_customers": 0,
    "first_api_call_at": "2026-11-04T10:00:00.000Z",
    "customers_with_multiple_subscriptions": 0,
    "sessions_without_subscription_30d": 0
  },
  "history_import": {
    "status": "pending",
    "since": null,
    "progress": {},
    "error": null,
    "started_at": null,
    "finished_at": null
  }
}