API REFERENCE v1

DEVELOPERS

Developers

The Webhook endpoint object

Webhook endpoints, their deliveries, and the versions of NoChurn's client code you run.

ATTRIBUTES

Show 4 more attributesHide 4 attributes

Schema: WebhookEndpoint

THE WEBHOOK ENDPOINT OBJECT
{
  "object": "webhook_endpoint",
  "id": "whk_PjDej35aHSE8rdvD",
  "livemode": false,
  "url": "https://hooks.acme.com/nochurn",
  "description": null,
  "events": ["recovery.recovered", "billing.retry_requested"],
  "status": "enabled",
  "secret": "whsec_EXAMPLE00000000000000000",
  "last_status": null,
  "last_delivery_at": null,
  "failure_streak": 0,
  "created_at": "2026-11-04T10:00:00Z"
}

Add a webhook endpoint

Deliveries are signed: NoChurn-Signature: t=<unix>,v1=<HMAC-SHA256 of "t.body">. The secret is shown once.

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): configure scope, test mode only.

BODY PARAMETERS

  • url string REQUIRED
  • events array of enums REQUIRED
    • recovery.opened
    • recovery.recovered
    • recovery.lost
    • outcome.confirmed
    • outcome.reverted
    • billing.retry_requested
    • billing.cancel_requested
    • billing.discount_requested
    • billing.pause_requested
    • billing.downgrade_requested
    • customer.scored
    • customer.flagged
    • customer.access_restricted
    • customer.access_restored
    • cancel_session.started
    • cancel_session.offer_accepted
    • cancel_session.completed
    • winback.reactivated
    • *
  • description string

RETURNS

ERRORS

POST/v1/webhook_endpoints
curl https://api.nochurn.sh/v1/webhook_endpoints \
  -H "Authorization: Bearer nc_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.acme.com/nochurn",
    "events": ["recovery.recovered", "billing.retry_requested"]
  }'
const res = await fetch("https://api.nochurn.sh/v1/webhook_endpoints", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "url": "https://hooks.acme.com/nochurn",
    "events": ["recovery.recovered", "billing.retry_requested"]
  }),
});
const webhookEndpoint = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/webhook_endpoints",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
    json={
      "url": "https://hooks.acme.com/nochurn",
      "events": ["recovery.recovered", "billing.retry_requested"]
    },
)
webhook_endpoint = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/webhook_endpoints")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
  "url" => "https://hooks.acme.com/nochurn",
  "events" => ["recovery.recovered", "billing.retry_requested"]
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
webhook_endpoint = JSON.parse(res.body)
RESPONSE 201
{
  "object": "webhook_endpoint",
  "id": "whk_PjDej35aHSE8rdvD",
  "livemode": false,
  "url": "https://hooks.acme.com/nochurn",
  "description": null,
  "events": ["recovery.recovered", "billing.retry_requested"],
  "status": "enabled",
  "secret": "whsec_EXAMPLE00000000000000000",
  "last_status": null,
  "last_delivery_at": null,
  "failure_streak": 0,
  "created_at": "2026-11-04T10:00:00Z"
}

Change an endpoint, disable it or rotate its secret

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): configure scope, test mode only.

PATH PARAMETERS

  • id string REQUIRED

BODY PARAMETERS

  • url string
  • events array of enums
    • recovery.opened
    • recovery.recovered
    • recovery.lost
    • outcome.confirmed
    • outcome.reverted
    • billing.retry_requested
    • billing.cancel_requested
    • billing.discount_requested
    • billing.pause_requested
    • billing.downgrade_requested
    • customer.scored
    • customer.flagged
    • customer.access_restricted
    • customer.access_restored
    • cancel_session.started
    • cancel_session.offer_accepted
    • cancel_session.completed
    • winback.reactivated
    • *
  • description string
  • status enum
    • enabled
    • disabled
  • rotate_secret boolean

    The new secret is returned once.

RETURNS

PATCH/v1/webhook_endpoints/{id}
curl https://api.nochurn.sh/v1/webhook_endpoints/whk_6Z1istZvLLDV7b7p \
  -X PATCH \
  -H "Authorization: Bearer nc_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "rotate_secret": true,
    "status": "disabled"
  }'
const res = await fetch("https://api.nochurn.sh/v1/webhook_endpoints/whk_6Z1istZvLLDV7b7p", {
  method: "PATCH",
  headers: {
    Authorization: "Bearer nc_test_…",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "rotate_secret": true,
    "status": "disabled"
  }),
});
const webhookEndpoint = await res.json();
import requests

res = requests.patch(
    "https://api.nochurn.sh/v1/webhook_endpoints/whk_6Z1istZvLLDV7b7p",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
    json={
      "rotate_secret": True,
      "status": "disabled"
    },
)
webhook_endpoint = res.json()
require "net/http"
require "json"

uri = URI("https://api.nochurn.sh/v1/webhook_endpoints/whk_6Z1istZvLLDV7b7p")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer nc_test_…"
req["Content-Type"] = "application/json"
req.body = {
  "rotate_secret" => true,
  "status" => "disabled"
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
webhook_endpoint = JSON.parse(res.body)
RESPONSE 200
{
  "object": "webhook_endpoint",
  "id": "whk_6Z1istZvLLDV7b7p",
  "livemode": false,
  "url": "https://hooks.acme.com/x",
  "description": null,
  "events": ["*"],
  "status": "disabled",
  "secret": "whsec_EXAMPLE00000000000000000",
  "last_status": null,
  "last_delivery_at": null,
  "failure_streak": 0,
  "created_at": "2026-11-04T10:00:00Z"
}

Remove an endpoint

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): configure scope, test mode only.

PATH PARAMETERS

  • id string REQUIRED

RETURNS

  • 204 removed
DELETE/v1/webhook_endpoints/{id}
curl https://api.nochurn.sh/v1/webhook_endpoints/whk_qPf7RbQXC4oY7SjA \
  -X DELETE \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/webhook_endpoints/whk_qPf7RbQXC4oY7SjA", {
  method: "DELETE",
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
console.log(res.status); // 204
import requests

res = requests.delete(
    "https://api.nochurn.sh/v1/webhook_endpoints/whk_qPf7RbQXC4oY7SjA",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
print(res.status_code)  # 204
require "net/http"
require "json"

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

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code # 204
RESPONSE 204
// No content

List webhook endpoints

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

RETURNS

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

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

uri = URI("https://api.nochurn.sh/v1/webhook_endpoints")
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": "webhook_endpoint",
      "id": "whk_qPf7RbQXC4oY7SjA",
      "livemode": false,
      "url": "https://hooks.acme.com/x",
      "description": null,
      "events": ["*"],
      "status": "enabled",
      "secret": null,
      "last_status": null,
      "last_delivery_at": null,
      "failure_streak": 0,
      "created_at": "2026-11-04T10:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Send a signed test event now

  • Secret key (nc_test_… or nc_live_…). Restricted keys need write access.
  • AI apps (OAuth): configure scope, test mode only.

PATH PARAMETERS

  • id string REQUIRED

RETURNS

POST/v1/webhook_endpoints/{id}/test
curl https://api.nochurn.sh/v1/webhook_endpoints/whk_qPf7RbQXC4oY7SjA/test \
  -X POST \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/webhook_endpoints/whk_qPf7RbQXC4oY7SjA/test", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const webhookDelivery = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/webhook_endpoints/whk_qPf7RbQXC4oY7SjA/test",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
webhook_delivery = res.json()
require "net/http"
require "json"

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

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
webhook_delivery = JSON.parse(res.body)
RESPONSE 201
{
  "object": "webhook_delivery",
  "id": "whd_7Fx1qjkiZwLiEgRG",
  "livemode": false,
  "endpoint": "whk_S569GCh7fAHwYDn7",
  "destination": "http",
  "event": null,
  "type": "recovery.opened",
  "status": "delivered",
  "attempts": 1,
  "next_attempt_at": null,
  "response_status": 200,
  "response_ms": 42,
  "error": null,
  "delivered_at": "2026-11-04T10:00:00Z",
  "failed_at": null,
  "replay_of": null,
  "created_at": "2026-11-04T10:00:00Z",
  "payload": {
    "id": "nevt_test_5accf2bcd660",
    "data": {
      "test": true,
      "message": "Test webhook from NoChurn"
    },
    "type": "test",
    "object": "event",
    "created": 1793786400,
    "livemode": false
  },
  "response_excerpt": null
}

Webhooks sent, with their attempts

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

QUERY PARAMETERS

RETURNS

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

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

uri = URI("https://api.nochurn.sh/v1/webhook_deliveries")
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": [],
  "has_more": false,
  "next_cursor": null
}

One delivery, with its payload

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

PATH PARAMETERS

  • id string REQUIRED

RETURNS

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

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

uri = URI("https://api.nochurn.sh/v1/webhook_deliveries/whd_FySimzUDNEgANYU3")
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) }
webhook_delivery = JSON.parse(res.body)
RESPONSE 200
{
  "object": "webhook_delivery",
  "id": "whd_FySimzUDNEgANYU3",
  "livemode": false,
  "endpoint": "whk_1xbKsAAZvYm1Xmn7",
  "destination": "http",
  "event": "nevt_1",
  "type": "recovery.opened",
  "status": "failed",
  "attempts": 7,
  "next_attempt_at": null,
  "response_status": null,
  "response_ms": null,
  "error": null,
  "delivered_at": null,
  "failed_at": null,
  "replay_of": null,
  "created_at": "2026-11-04T10:00:00Z",
  "payload": {
    "id": "nevt_1",
    "type": "recovery.opened"
  },
  "response_excerpt": null
}

Replay a delivery (same payload, same event id)

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

PATH PARAMETERS

  • id string REQUIRED

RETURNS

POST/v1/webhook_deliveries/{id}/replay
curl https://api.nochurn.sh/v1/webhook_deliveries/whd_FySimzUDNEgANYU3/replay \
  -X POST \
  -H "Authorization: Bearer nc_test_…"
const res = await fetch("https://api.nochurn.sh/v1/webhook_deliveries/whd_FySimzUDNEgANYU3/replay", {
  method: "POST",
  headers: {
    Authorization: "Bearer nc_test_…",
  },
});
const webhookDelivery = await res.json();
import requests

res = requests.post(
    "https://api.nochurn.sh/v1/webhook_deliveries/whd_FySimzUDNEgANYU3/replay",
    headers={
        "Authorization": "Bearer nc_test_…",
    },
)
webhook_delivery = res.json()
require "net/http"
require "json"

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

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
webhook_delivery = JSON.parse(res.body)
RESPONSE 201
{
  "object": "webhook_delivery",
  "id": "whd_QgnnVzREeDX8CRSm",
  "livemode": false,
  "endpoint": "whk_pbUg1sc39Ttpip9b",
  "destination": "http",
  "event": "nevt_1",
  "type": "recovery.opened",
  "status": "delivered",
  "attempts": 1,
  "next_attempt_at": null,
  "response_status": 200,
  "response_ms": 42,
  "error": null,
  "delivered_at": "2026-11-04T10:00:00Z",
  "failed_at": null,
  "replay_of": "whd_63HJxMXhoVWNsVmW",
  "created_at": "2026-11-04T10:00:00Z",
  "payload": {
    "id": "nevt_1"
  },
  "response_excerpt": null
}

Is the widget signing secret set?

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

RETURNS

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

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

uri = URI("https://api.nochurn.sh/v1/account/widget_secret")
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) }
widget_secret = JSON.parse(res.body)
RESPONSE 200
{
  "object": "widget_secret",
  "configured": false,
  "created_at": null,
  "secret": null
}

Create or rotate the widget signing secret

Your server signs customer tokens with it (HS256). Shown once; rotating invalidates tokens signed with the old one.

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

RETURNS

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

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

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

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
widget_secret = JSON.parse(res.body)
RESPONSE 201
{
  "object": "widget_secret",
  "configured": true,
  "created_at": "2026-11-04T10:00:00Z",
  "secret": "ncws_EXAMPLE00000000000000000"
}