DEVELOPERS
Developers
The Webhook endpoint object
Webhook endpoints, their deliveries, and the versions of NoChurn's client code you run.
ATTRIBUTES
- object enum
- webhook_endpoint
- id string
- livemode boolean
- url string
- description string | null
- events array of strings
- status enum
- enabled
- disabled
- secret string | null
whsec_…: only when created or rotated.
Show 4 more attributesHide 4 attributes
- last_status integer | null
- failure_streak integer
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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth):
configurescope, test mode only.
BODY PARAMETERS
-
- 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
-
201with its secret, once — the Webhook endpoint object
ERRORS
-
422invalid_request not https or an unknown event
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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth):
configurescope, test mode only.
PATH PARAMETERS
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
-
200secret rotated — the Webhook endpoint object
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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth):
configurescope, test mode only.
PATH PARAMETERS
RETURNS
-
204removed
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); // 204import requests
res = requests.delete(
"https://api.nochurn.sh/v1/webhook_endpoints/whk_qPf7RbQXC4oY7SjA",
headers={
"Authorization": "Bearer nc_test_…",
},
)
print(res.status_code) # 204require "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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
RETURNS
-
200the list, never the secrets — a list of Webhook endpoint object (WebhookEndpointList)
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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth):
configurescope, test mode only.
PATH PARAMETERS
RETURNS
-
201delivered — a WebhookDeliveryDetail
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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
QUERY PARAMETERS
- status string
: *
pending*retrying*delivered*failed - endpoint string
- type string
- limit integer
- starting_after string
RETURNS
-
200the list — a list of WebhookDelivery (WebhookDeliveryList)
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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
PATH PARAMETERS
RETURNS
-
200found — a WebhookDeliveryDetail
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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
PATH PARAMETERS
RETURNS
-
201sent again now — a WebhookDeliveryDetail
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_…ornc_live_…). Restricted keys need read access. - AI apps (OAuth):
readscope.
RETURNS
-
200never shows the secret — a WidgetSecret
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_…ornc_live_…). Restricted keys need write access. - AI apps (OAuth): not available.
RETURNS
-
201shown once — a WidgetSecret
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"
}