DEVELOPERS
Agent integration
The Integration verification object
What coding agents use to add NoChurn to an app: platform detection, tested snippets and the checklist.
ATTRIBUTES
- object enum
- integration_verification
- livemode enum
- false
- complete boolean
- passed integer
- total integer
- checks array of objects
Show child attributesHide child attributes
- go_live_url string | null
Where a human goes live, once everything passes.
Schema: IntegrationVerification
{
"object": "integration_verification",
"livemode": false,
"complete": false,
"passed": 0,
"total": 7,
"checks": [
{
"key": "billing_connected",
"status": "fail",
"detail": "No billing connected and no customer received from your app yet.",
"fix": "Send a customer (POST /v1/customers) and its subscription (POST /v1/subscriptions) from your backend with your test key."
},
{
"key": "webhook_verified",
"status": "fail",
"detail": "No webhook endpoint in test mode.",
"fix": "Create one (webhook_endpoints_create) on a route of your backend that checks NoChurn-Signature (integration_get_snippets case webhooks)."
},
{
"key": "identify_received",
"status": "fail",
"detail": "No attribute received yet.",
"fix": "From your backend, send signup_source and onboarding_done (and the attributes of your app) when a user signs up or changes: POST /v1/customers/{id}/attributes (integration_get_snippets case identify)."
},
{
"key": "cancel_flow_opened",
"status": "fail",
"detail": "Your app has not opened the cancel flow yet (sandbox runs do not count).",
"fix": "Wire your cancel button: your backend signs a customer token and your app starts POST /v1/cancel_sessions (integration_get_snippets case signed_token; the cancel.js modal is not released yet), then click it once in test mode."
},
{
"key": "click_to_cancel",
"status": "fail",
"detail": "No cancellation in test mode yet.",
"fix": "Cancel once in test mode from your app (POST /v1/cancel_sessions/{id}/cancel), or play it with sandbox_run_cancel_flow."
},
{
"key": "failed_payment_email",
"status": "fail",
"detail": "No failed payment in test mode yet.",
"fix": "Create a sandbox customer (sandbox_create_customer), then sandbox_simulate_failed_payment."
},
{
"key": "no_secret_in_code",
"status": "unknown",
"detail": "No code sent.",
"fix": "Call integration_verify with snippets: the files that call NoChurn or hold configuration (front end and back end)."
}
],
"go_live_url": null
} Integration checklist
The 7 points of BRIEF §5.9.4, read from the account's test-mode data: pass, fail (with its fix) or unknown. Sandbox runs never count as your app's work for billing_connected and cancel_flow_opened.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
RETURNS
-
200the checklist — the Integration verification object
ERRORS
-
403test_mode_only live mode
curl https://api.nochurn.sh/v1/integration/verify \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/integration/verify", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const integrationVerification = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/integration/verify",
headers={
"Authorization": "Bearer nc_test_…",
},
)
integration_verification = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/integration/verify")
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) }
integration_verification = JSON.parse(res.body) {
"object": "integration_verification",
"livemode": false,
"complete": false,
"passed": 0,
"total": 7,
"checks": [
{
"key": "billing_connected",
"status": "fail",
"detail": "No billing connected and no customer received from your app yet.",
"fix": "Send a customer (POST /v1/customers) and its subscription (POST /v1/subscriptions) from your backend with your test key."
},
{
"key": "webhook_verified",
"status": "fail",
"detail": "No webhook endpoint in test mode.",
"fix": "Create one (webhook_endpoints_create) on a route of your backend that checks NoChurn-Signature (integration_get_snippets case webhooks)."
},
{
"key": "identify_received",
"status": "fail",
"detail": "No attribute received yet.",
"fix": "From your backend, send signup_source and onboarding_done (and the attributes of your app) when a user signs up or changes: POST /v1/customers/{id}/attributes (integration_get_snippets case identify)."
},
{
"key": "cancel_flow_opened",
"status": "fail",
"detail": "Your app has not opened the cancel flow yet (sandbox runs do not count).",
"fix": "Wire your cancel button: your backend signs a customer token and your app starts POST /v1/cancel_sessions (integration_get_snippets case signed_token; the cancel.js modal is not released yet), then click it once in test mode."
},
{
"key": "click_to_cancel",
"status": "fail",
"detail": "No cancellation in test mode yet.",
"fix": "Cancel once in test mode from your app (POST /v1/cancel_sessions/{id}/cancel), or play it with sandbox_run_cancel_flow."
},
{
"key": "failed_payment_email",
"status": "fail",
"detail": "No failed payment in test mode yet.",
"fix": "Create a sandbox customer (sandbox_create_customer), then sandbox_simulate_failed_payment."
},
{
"key": "no_secret_in_code",
"status": "unknown",
"detail": "No code sent.",
"fix": "Call integration_verify with snippets: the files that call NoChurn or hold configuration (front end and back end)."
}
],
"go_live_url": null
} Integration checklist, with the code to scan for secrets
Same checklist; no_secret_in_code scans the files sent (secret keys nc_, sk_/rk_, whsec_, ncws_). Nothing is stored: findings only carry the file, the line and a masked preview.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
BODY PARAMETERS
RETURNS
-
200the checklist — the Integration verification object
curl https://api.nochurn.sh/v1/integration/verify \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"snippets": [
{
"path": "src/app.js",
"content": "const k = '\''nc_live_EXAMPLE00000000000000000'\''"
}
]
}'const res = await fetch("https://api.nochurn.sh/v1/integration/verify", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"snippets": [
{
"path": "src/app.js",
"content": "const k = 'nc_live_EXAMPLE00000000000000000'"
}
]
}),
});
const integrationVerification = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/integration/verify",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"snippets": [
{
"path": "src/app.js",
"content": "const k = 'nc_live_EXAMPLE00000000000000000'"
}
]
},
)
integration_verification = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/integration/verify")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"snippets" => [
{
"path" => "src/app.js",
"content" => "const k = 'nc_live_EXAMPLE00000000000000000'"
}
]
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
integration_verification = JSON.parse(res.body) {
"object": "integration_verification",
"livemode": false,
"complete": false,
"passed": 0,
"total": 7,
"checks": [
{
"key": "billing_connected",
"status": "fail",
"detail": "No billing connected and no customer received from your app yet.",
"fix": "Send a customer (POST /v1/customers) and its subscription (POST /v1/subscriptions) from your backend with your test key."
},
{
"key": "webhook_verified",
"status": "fail",
"detail": "No webhook endpoint in test mode.",
"fix": "Create one (webhook_endpoints_create) on a route of your backend that checks NoChurn-Signature (integration_get_snippets case webhooks)."
},
{
"key": "identify_received",
"status": "fail",
"detail": "No attribute received yet.",
"fix": "From your backend, send signup_source and onboarding_done (and the attributes of your app) when a user signs up or changes: POST /v1/customers/{id}/attributes (integration_get_snippets case identify)."
},
{
"key": "cancel_flow_opened",
"status": "fail",
"detail": "Your app has not opened the cancel flow yet (sandbox runs do not count).",
"fix": "Wire your cancel button: your backend signs a customer token and your app starts POST /v1/cancel_sessions (integration_get_snippets case signed_token; the cancel.js modal is not released yet), then click it once in test mode."
},
{
"key": "click_to_cancel",
"status": "fail",
"detail": "No cancellation in test mode yet.",
"fix": "Cancel once in test mode from your app (POST /v1/cancel_sessions/{id}/cancel), or play it with sandbox_run_cancel_flow."
},
{
"key": "failed_payment_email",
"status": "fail",
"detail": "No failed payment in test mode yet.",
"fix": "Create a sandbox customer (sandbox_create_customer), then sandbox_simulate_failed_payment."
},
{
"key": "no_secret_in_code",
"status": "fail",
"detail": "Secrets found: src/app.js:1 nochurn_live_secret_key (nc_live_…ZZ).",
"fix": "Move them to environment variables read by your backend only (e.g. process.env.NOCHURN_API_KEY), never in the app or the front end; revoke a key that was committed."
}
],
"go_live_url": null
} Detect the project's platforms and plan the integration
The agent sends file paths, dependency names and the dependency manifests; nothing is stored. Flags App Store / Google Play subscriptions, which NoChurn cannot cancel server side.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
BODY PARAMETERS
- dependencies array of strings
- manifests array of objects
RETURNS
-
200platforms and plan — a ProjectDetection
curl https://api.nochurn.sh/v1/integration/detect \
-H "Authorization: Bearer nc_test_…" \
-H "Content-Type: application/json" \
-d '{
"files": ["next.config.mjs"],
"manifests": [
{
"path": "package.json",
"content": "{\"dependencies\":{\"next\":\"15\",\"stripe\":\"17\"}}"
}
]
}'const res = await fetch("https://api.nochurn.sh/v1/integration/detect", {
method: "POST",
headers: {
Authorization: "Bearer nc_test_…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"files": ["next.config.mjs"],
"manifests": [
{
"path": "package.json",
"content": "{\"dependencies\":{\"next\":\"15\",\"stripe\":\"17\"}}"
}
]
}),
});
const projectDetection = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/integration/detect",
headers={
"Authorization": "Bearer nc_test_…",
},
json={
"files": ["next.config.mjs"],
"manifests": [
{
"path": "package.json",
"content": "{\"dependencies\":{\"next\":\"15\",\"stripe\":\"17\"}}"
}
]
},
)
project_detection = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/integration/detect")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
"files" => ["next.config.mjs"],
"manifests" => [
{
"path" => "package.json",
"content" => "{\"dependencies\":{\"next\":\"15\",\"stripe\":\"17\"}}"
}
]
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
project_detection = JSON.parse(res.body) {
"object": "project_detection",
"platforms": [
{
"key": "nextjs",
"name": "Next.js",
"kind": "web",
"evidence": ["dependency next", "file next.config.mjs"]
}
],
"backend": "nextjs",
"frontend": "nextjs",
"mobile": [],
"billing": {
"stripe": true,
"store_subscriptions": {
"detected": false,
"stores": [],
"evidence": [],
"note": null
}
},
"plan": {
"steps": [
{
"key": "setup",
"title": "Read what is installed and what is missing",
"status": "available",
"tools": ["account_get_setup"],
"snippet_cases": []
},
{
"key": "billing",
"title": "Connect Stripe in test mode (a human clicks the link)",
"status": "available",
"tools": ["account_connect_stripe_link"],
"snippet_cases": []
},
{
"key": "keys",
"title": "Create a test secret key for the backend (env NOCHURN_API_KEY) and get the publishable key",
"status": "available",
"tools": ["api_keys_create_test", "publishable_key_get"],
"snippet_cases": []
},
{
"key": "identify",
"title": "Send customers and their attributes (identify) and usage events (track)",
"status": "available",
"tools": ["attributes_define", "integration_get_snippets"],
"snippet_cases": ["identify", "track"],
"snippet_platform": "nextjs"
},
{
"key": "webhooks",
"title": "Receive NoChurn's webhooks on a route that checks the signature",
"status": "available",
"tools": [
"webhook_endpoints_create",
"integration_get_snippets",
"webhook_endpoints_send_test"
],
"snippet_cases": ["webhooks"],
"snippet_platform": "nextjs"
},
{
"key": "cancel_flow",
"title": "Configure the cancel flow (reasons, offers, theme from the project's colours) and publish it in test mode",
"status": "available",
"tools": [
"cancel_flow_get",
"cancel_flow_update_draft",
"cancel_flow_set_theme",
"flow_publish_test",
"cancel_flow_preview_url"
],
"snippet_cases": []
},
{
"key": "customer_token",
"title": "Sign the customer token on your backend (widget secret, env NOCHURN_WIDGET_SECRET)",
"status": "available",
"tools": ["integration_get_snippets"],
"snippet_cases": ["signed_token"],
"snippet_platform": "nextjs"
},
{
"key": "cancel_button_web",
"title": "Wire your cancel button: headless cancel flow (the cancel.js modal is not released yet)",
"status": "available",
"tools": ["integration_get_snippets"],
"snippet_cases": ["headless_cancel"],
"snippet_platform": "web"
},
{
"key": "access_check",
"title": "Block your app in the backend while a payment fails (GET /v1/customers/{id}/access): the widget's wall alone can be bypassed",
"status": "available",
"tools": ["integration_get_snippets"],
"snippet_cases": ["access_check"],
"snippet_platform": "nextjs"
},
{
"key": "sandbox",
"title": "Play the scenarios end to end in test mode",
"status": "available",
"tools": [
"sandbox_create_customer",
"sandbox_simulate_failed_payment",
"sandbox_simulate_card_removed",
"sandbox_run_cancel_flow"
],
"snippet_cases": []
},
{
"key": "verify",
"title": "Run the checklist until everything is green, sending the code you changed",
"status": "available",
"tools": ["integration_verify"],
"snippet_cases": []
},
{
"key": "go_live",
"title": "Hand over to a human: going live (live keys, live publication) is approved in the dashboard",
"status": "available",
"tools": ["request_go_live"],
"snippet_cases": []
}
],
"warnings": []
}
} Tested code for a platform and a case
Only code tested against this API, versioned. Cases without a released SDK (modal, native apps) answer not_available_yet.
- Secret key (
nc_test_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
QUERY PARAMETERS
- platform string
- case enum
- identify
- track
- webhooks
- signed_token
- billing_by_api
- access_check
- headless_cancel
- modal
- card_update
RETURNS
-
200the snippets — a IntegrationSnippets
ERRORS
-
400invalid_request unknown platform
curl "https://api.nochurn.sh/v1/integration/snippets?platform=nextjs" \
-H "Authorization: Bearer nc_test_…"const res = await fetch("https://api.nochurn.sh/v1/integration/snippets?platform=nextjs", {
headers: {
Authorization: "Bearer nc_test_…",
},
});
const integrationSnippets = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/integration/snippets?platform=nextjs",
headers={
"Authorization": "Bearer nc_test_…",
},
)
integration_snippets = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/integration/snippets?platform=nextjs")
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) }
integration_snippets = JSON.parse(res.body) {
"object": "integration_snippets",
"version": "2026-11-06.2",
"platform": "rails",
"language": "ruby",
"api_base_url": "https://api.nochurn.sh",
"snippets": [
{
"case": "webhooks",
"status": "available",
"title": "Verify NoChurn's webhook signature",
"language": "ruby",
"label": "Ruby (Net::HTTP, OpenSSL)",
"syntax": "ruby",
"runs_on": "server",
"insert": "A new POST route of your backend (e.g. /webhooks/nochurn), registered with webhook_endpoints_create.",
"env": ["NOCHURN_WEBHOOK_SECRET"],
"file": "webhooks/ruby.rb",
"code": "# NoChurn: verify a webhook (NoChurn-Signature: t=<unix>,v1=<hex HMAC-SHA256 of \"t.<raw body>\">) in Ruby.\n# Env: NOCHURN_WEBHOOK_SECRET (whsec_…). Use the raw body (Rails: request.raw_post).\nrequire \"openssl\"\n\ndef verify_nochurn_signature(raw_body, header, secret = ENV.fetch(\"NOCHURN_WEBHOOK_SECRET\"), tolerance: 300)\n parts = header.to_s.split(\",\").to_h { |part| part.split(\"=\", 2) }\n t = Integer(parts[\"t\"].to_s, exception: false)\n return false if t.nil? || (Time.now.to_i - t).abs > tolerance || parts[\"v1\"].nil?\n\n expected = OpenSSL::HMAC.hexdigest(\"SHA256\", secret, \"#{parts['t']}.#{raw_body}\")\n expected.bytesize == parts[\"v1\"].bytesize && OpenSSL.fixed_length_secure_compare(expected, parts[\"v1\"])\nend\n\n# Rails example (skip CSRF on this API controller):\n# def create\n# return head(:bad_request) unless verify_nochurn_signature(request.raw_post, request.headers[\"NoChurn-Signature\"])\n# event = JSON.parse(request.raw_post)\n# head :ok\n# end\n"
}
]
}