API REFERENCE v1

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
    • key enum
      • billing_connected
      • webhook_verified
      • identify_received
      • cancel_flow_opened
      • click_to_cancel
      • failed_payment_email
      • no_secret_in_code
    • status enum

      unknown: NoChurn cannot tell yet (email being written, no code sent).

      • pass
      • fail
      • unknown
    • detail string
    • fix string | null

      What to do next; null when it passes.

  • go_live_url string | null

    Where a human goes live, once everything passes.

Schema: IntegrationVerification

THE INTEGRATION VERIFICATION OBJECT
{
  "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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

RETURNS

ERRORS

GET/v1/integration/verify
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

BODY PARAMETERS

  • snippets array of objects 200 items at most REQUIRED
    Show child parametersHide child parameters

RETURNS

POST/v1/integration/verify
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

BODY PARAMETERS

  • files array of strings 5000 items at most
  • dependencies array of strings
  • manifests array of objects
    Show child parametersHide child parameters

RETURNS

POST/v1/integration/detect
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)
RESPONSE 200
{
  "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_… or nc_live_…). Restricted keys need read access.
  • AI apps (OAuth): read scope.

QUERY PARAMETERS

  • platform string
  • case enum
    • identify
    • track
    • webhooks
    • signed_token
    • billing_by_api
    • access_check
    • headless_cancel
    • modal
    • card_update

RETURNS

ERRORS

GET/v1/integration/snippets
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)
RESPONSE 200
{
  "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"
    }
  ]
}