BROWSER · PUBLISHABLE KEY
In-app
Called from your customer's browser, by the script (js.nochurn.sh) or your own code, with a publishable key (pk_test_…)
and a customer token signed by your server. See Authentication.
The Inapp state object
The in-app banner and wall of one customer, for the browser script.
ATTRIBUTES
- object enum
- inapp_state
- customer string
- access enum
restricted exactly when wall.show is true: the same calculation as GET /v1/customers/{id}/access (DECISIONS 81, 87).
- full
- restricted
- access_reason enum | null
Why access is restricted; null when full. paused: only when the cancel flow's pause.inapp is wall.
- payment_failed
- paused
- locale enum
?locale=, then the customer's language, then the account's.
- en
- fr
- de
- it
- pt
- texts object | null
Banner and wall texts in this locale (banner_text, banner_cta, dismiss, banner_days_left, banner_last_day, wall_title, wall_text, wall_cta, pay_cta, amount_due, card_expired, decline_expired, decline_insufficient_funds, decline_authentication_required, decline_declined, checking_payment, back_in_title, back_in_text, data_safe, paused_banner_text, paused_title, paused_text, paused_resumes, resume_cta, resuming, resumed_title, resumed_text, cancel_now): the recovery flow's inapp.texts, else NoChurn's. {{amount}} is already filled (the unpaid amount, or the regular price when paused); {{days}}, {{date}} and {{exp}} are for the widget to fill (from details, days_left, in the customer's locale). Null when nothing is shown.
- theme object | null
The cancel flow's design (same tokens as the modal). Null when nothing is shown.
Show child attributesHide child attributes
- object enum
- cancel_flow_theme
- preset enum
- light
- dark
- playful
- custom_css
- headless
- format enum
modal (centred), panel (side panel) or page (full page).
- modal
- panel
- page
- tokens object
The --nc-* custom properties (--nc-accent-text for accent_text). Enumerated values are mapped to CSS by the widget.
Show child attributesHide child attributes
- bg string
- surface string
- text string
- muted string
- accent string
- accent_text string
- border string
- danger string
-
A font stack. Its first family loads from font_url when it is an allowed Google Font.
-
Base size, px.
-
Modal corners, px.
-
Buttons and fields, px.
-
Backdrop opacity.
-
Modal width, px.
- shadow enum
- none
- sm
- md
- lg
- spacing enum
Density.
- compact
- regular
- comfortable
-
px.
- border_style enum
- solid
- dashed
- none
-
Title size = font_size × font_scale.
- font_weight_title enum
- 400
- 500
- 600
- 700
-
- font_url string | null
Google Fonts stylesheet of the font token's first family (allowed fonts only), else null.
- brand enum
What the header shows: logo (logo_url, else app_name), name (app_name) or none (nothing). logo_url and app_name already follow it: null when the header must not show them.
- logo
- name
- none
- logo_url string | null
https URL of the logo (uploaded, or set in the editor) when brand is logo, else null (show app_name).
- app_name string | null
The account's name: the text shown in place of a logo; null when brand is none.
- end_screen object
The modal's last step (draft.design.end_screen). A cancelled customer's end screen is always calm, whatever the style.
Show child attributesHide child attributes
- style enum
Saved customer: celebrate (default), glow or quiet.
- celebrate
- glow
- quiet
-
Saved screen only: closes by itself after this (default 6); null = never.
-
- custom_css string
Cleaned on the server (no @import, url() only to Google Fonts).
- cancel_button_check enum
- ok
- fallback_applied
- classes array of enums
- nc-modal
- nc-title
- nc-reason
- nc-offer
- nc-btn-primary
- nc-btn-cancel
- nc-footer
-
Show 2 more attributesHide 2 attributes
- banner object
Show child attributesHide child attributes
- show boolean
- text string
- cta_url string | null
- cta_label string | null
- kind enum
- payment_failed
- paused
- audience enum
- admins
- everyone
- dismissible_for_hours integer
- dismiss_label string
-
payment_failed: when the wall comes up if still unpaid (wall enabled, not up yet); else null.
-
Whole days until restricts_at, in the customer's time zone (0 = last day); null with restricts_at.
- details object | null
The facts the banner draws (same as wall.details); null when the banner is not shown.
-
- wall object
Show child attributesHide child attributes
- show boolean
- text string
- cta_url string | null
- cta_label string | null
- kind enum
- payment_failed
- paused
- shows_after_days integer | null
payment_failed: the grace in days; null for paused (at once).
- cancel object
Click to cancel on the wall: open the cancel modal (NoChurn.cancel, POST /v1/cancel_sessions); if that fails, POST the customer token as a form field
tokento fallback_url.Show child attributesHide child attributes
- label string
The cancel flow's cancel_now text (or the in-app override).
- fallback_url string | null
The publishable key's fallback cancel link (null for a secret key).
-
- details object | null
The facts the wall draws; null when the wall is not shown. paused: cta_label is the resume button (POST /v1/inapp/resume), null when resume_allowed is false; cta_url is null.
- kind enum
- payment_failed
- amount_due_cents integer
- currency string | null
-
The invoice's due date when the billing set one, else null.
- payment_method object | null
The method of the failed payment, else the customer's default one; null when unknown.
- decline enum
Mapped from the provider's decline or failure code (expired_card → expired; insufficient_funds; authentication_required; anything else → declined).
- expired
- insufficient_funds
- authentication_required
- declined
- kind enum
- paused
-
When the pause started (null when NoChurn never saw it start).
-
When it resumes by itself (null: no date set).
- regular_amount_cents integer
The price per interval once resumed (quantity included).
- currency string | null
- resume_allowed boolean
POST /v1/inapp/resume can resume it (Stripe, or a provider NoChurn resumes); false for billing by API.
-
-
Schema: InappState
{
"object": "inapp_state",
"customer": "cst_vDH4Ubcpyj5pzVs1",
"failing_since": "2026-11-04T10:00:00Z",
"locale": "fr",
"texts": {
"banner_text": "Votre dernier paiement de €49.00 n'est pas passé. Mettez à jour votre moyen de paiement pour garder votre compte.",
"banner_cta": "Payer maintenant",
"dismiss": "Masquer",
"banner_days_left": "Plus que {{days}} jours avant la suspension de votre compte.",
"banner_last_day": "Dernier jour avant la suspension de votre compte.",
"wall_title": "Votre compte est suspendu",
"wall_text": "Votre compte est suspendu jusqu'au paiement de €49.00.",
"wall_cta": "Mettre à jour le moyen de paiement",
"pay_cta": "Mettre à jour la carte et payer €49.00",
"amount_due": "Montant dû",
"card_expired": "Expirée {{exp}}",
"decline_expired": "Carte expirée",
"decline_insufficient_funds": "Fonds insuffisants",
"decline_authentication_required": "Votre banque demande de valider le paiement",
"decline_declined": "Refusée par votre banque",
"checking_payment": "Vérification du paiement…",
"back_in_title": "Vous revoilà",
"back_in_text": "Paiement reçu. Merci !",
"data_safe": "Vos données sont en sécurité. Rien n'est supprimé.",
"paused_banner_text": "Votre abonnement est en pause jusqu'au {{date}}.",
"paused_title": "Votre abonnement est en pause",
"paused_text": "Aucun prélèvement pendant la pause. Vos données sont conservées.",
"paused_resumes": "Reprise automatique dans {{days}} jours, à €49.00.",
"resume_cta": "Reprendre maintenant",
"resuming": "Reprise…",
"resumed_title": "Bon retour",
"resumed_text": "Votre abonnement est de nouveau actif.",
"cancel_now": "Résilier mon abonnement"
},
"theme": {
"object": "cancel_flow_theme",
"preset": "light",
"format": "modal",
"tokens": {
"bg": "#f5f6fa",
"surface": "#ffffff",
"text": "#1c1e4a",
"muted": "#686b85",
"accent": "#4637d9",
"accent_text": "#ffffff",
"border": "#e3e5ec",
"danger": "#c2410c",
"font": "system-ui, sans-serif",
"font_size": 15,
"radius": 12,
"radius_control": 8,
"overlay": 0.5,
"max_width": 480,
"shadow": "md",
"spacing": "regular",
"border_width": 1,
"border_style": "solid",
"font_scale": 1.25,
"font_weight_title": 600
},
"font_url": null,
"brand": "logo",
"logo_url": null,
"app_name": "Acme",
"end_screen": {
"style": "celebrate",
"auto_close_seconds": 6
},
"custom_css": "",
"cancel_button_check": "ok",
"classes": [
"nc-modal",
"nc-title",
"nc-reason",
"nc-offer",
"nc-btn-primary",
"nc-btn-cancel",
"nc-footer"
]
},
"access": "full",
"access_reason": null,
"banner": {
"show": true,
"kind": "payment_failed",
"audience": "admins",
"text": "Votre dernier paiement de €49.00 n'est pas passé. Mettez à jour votre moyen de paiement pour garder votre compte.",
"cta_url": "http://localhost:3000/c/okIJi02RYcm_ylJZATU-2KwNQpxXw64R",
"cta_label": "Payer maintenant",
"dismiss_label": "Masquer",
"dismissible_for_hours": 24,
"restricts_at": "2026-11-11T10:00:00Z",
"days_left": 7,
"details": {
"kind": "payment_failed",
"amount_due_cents": 4900,
"currency": "eur",
"invoice_due_at": null,
"payment_method": null,
"decline": "expired"
}
},
"wall": {
"show": false,
"kind": "payment_failed",
"text": "Votre compte est suspendu jusqu'au paiement de €49.00.",
"cta_url": "http://localhost:3000/c/okIJi02RYcm_ylJZATU-2KwNQpxXw64R",
"cta_label": "Mettre à jour le moyen de paiement",
"shows_after_days": 7,
"cancel": {
"label": "Résilier mon abonnement",
"fallback_url": "http://localhost:3000/k/pk_test_bCx4srPPXRCaL7ykidyP3qc498kijZxq"
},
"details": null
}
} Resume the paused subscription (in-app "Resume now")
From the paused wall or banner (DECISIONS 87): publishable key + customer token (its subscription claim names the subscription when there are several), or a secret key and ?customer=. Resumes the paused subscription at the provider (Stripe with an Idempotency-Key), writes a subscription.resumed event, and returns the new state (access full at once). 409 nothing_to_resume when nothing is paused (a second click included). Rate-limited like /v1/inapp/dismiss.
- Publishable key (
pk_test_…) + customer token (X-NoChurn-Customer-Token), from an allowed Origin. - From a server: a secret key (
nc_test_…) and thecustomerinstead of a token.
QUERY PARAMETERS
- customer string
HEADER PARAMETERS
- X-NoChurn-Customer-Token string
- Origin string
RETURNS
-
200resumed: access full again, an event, the access webhook restored — the Inapp state object
ERRORS
-
401unauthorized a token from another account never resumes this customer -
409nothing_to_resume nothing paused
curl https://api.nochurn.sh/v1/inapp/resume \
-X POST \
-H "Authorization: Bearer pk_test_…" \
-H "X-NoChurn-Customer-Token: eyJhbGciOiJIUzI1NiJ9…"const res = await fetch("https://api.nochurn.sh/v1/inapp/resume", {
method: "POST",
headers: {
Authorization: "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
},
});
const inappState = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/inapp/resume",
headers={
"Authorization": "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
},
)
inapp_state = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/inapp/resume")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer pk_test_…"
req["X-NoChurn-Customer-Token"] = "eyJhbGciOiJIUzI1NiJ9…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
inapp_state = JSON.parse(res.body) {
"object": "inapp_state",
"customer": "cst_33a3ErfXGkaBhxVd",
"failing_since": null,
"locale": "en",
"texts": null,
"theme": null,
"access": "full",
"access_reason": null,
"banner": {
"show": false
},
"wall": {
"show": false
}
} What the customer's app shows: payment banner and wall
From the browser: publishable key + customer token (HS256 { kind, value, exp } signed with your widget secret) + an allowed Origin. From a server: secret key and ?customer=.
- Publishable key (
pk_test_…) + customer token (X-NoChurn-Customer-Token), from an allowed Origin. - From a server: a secret key (
nc_test_…) and thecustomerinstead of a token.
QUERY PARAMETERS
- customer string
- locale string
Texts language; default: the customer's, then the account's.: *
en*fr*de*it*pt
HEADER PARAMETERS
- X-NoChurn-Customer-Token string
- Origin string
RETURNS
-
200a failing payment: banner on, wall after 7 days — the Inapp state object
ERRORS
-
401unauthorized a token signed with another secret -
403forbidden an origin that is not allowed
curl https://api.nochurn.sh/v1/inapp/state \
-H "Authorization: Bearer pk_test_…" \
-H "X-NoChurn-Customer-Token: eyJhbGciOiJIUzI1NiJ9…"const res = await fetch("https://api.nochurn.sh/v1/inapp/state", {
headers: {
Authorization: "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
},
});
const inappState = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/inapp/state",
headers={
"Authorization": "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
},
)
inapp_state = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/inapp/state")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer pk_test_…"
req["X-NoChurn-Customer-Token"] = "eyJhbGciOiJIUzI1NiJ9…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
inapp_state = JSON.parse(res.body) {
"object": "inapp_state",
"customer": "cst_vDH4Ubcpyj5pzVs1",
"failing_since": "2026-11-04T10:00:00Z",
"locale": "fr",
"texts": {
"banner_text": "Votre dernier paiement de €49.00 n'est pas passé. Mettez à jour votre moyen de paiement pour garder votre compte.",
"banner_cta": "Payer maintenant",
"dismiss": "Masquer",
"banner_days_left": "Plus que {{days}} jours avant la suspension de votre compte.",
"banner_last_day": "Dernier jour avant la suspension de votre compte.",
"wall_title": "Votre compte est suspendu",
"wall_text": "Votre compte est suspendu jusqu'au paiement de €49.00.",
"wall_cta": "Mettre à jour le moyen de paiement",
"pay_cta": "Mettre à jour la carte et payer €49.00",
"amount_due": "Montant dû",
"card_expired": "Expirée {{exp}}",
"decline_expired": "Carte expirée",
"decline_insufficient_funds": "Fonds insuffisants",
"decline_authentication_required": "Votre banque demande de valider le paiement",
"decline_declined": "Refusée par votre banque",
"checking_payment": "Vérification du paiement…",
"back_in_title": "Vous revoilà",
"back_in_text": "Paiement reçu. Merci !",
"data_safe": "Vos données sont en sécurité. Rien n'est supprimé.",
"paused_banner_text": "Votre abonnement est en pause jusqu'au {{date}}.",
"paused_title": "Votre abonnement est en pause",
"paused_text": "Aucun prélèvement pendant la pause. Vos données sont conservées.",
"paused_resumes": "Reprise automatique dans {{days}} jours, à €49.00.",
"resume_cta": "Reprendre maintenant",
"resuming": "Reprise…",
"resumed_title": "Bon retour",
"resumed_text": "Votre abonnement est de nouveau actif.",
"cancel_now": "Résilier mon abonnement"
},
"theme": {
"object": "cancel_flow_theme",
"preset": "light",
"format": "modal",
"tokens": {
"bg": "#f5f6fa",
"surface": "#ffffff",
"text": "#1c1e4a",
"muted": "#686b85",
"accent": "#4637d9",
"accent_text": "#ffffff",
"border": "#e3e5ec",
"danger": "#c2410c",
"font": "system-ui, sans-serif",
"font_size": 15,
"radius": 12,
"radius_control": 8,
"overlay": 0.5,
"max_width": 480,
"shadow": "md",
"spacing": "regular",
"border_width": 1,
"border_style": "solid",
"font_scale": 1.25,
"font_weight_title": 600
},
"font_url": null,
"brand": "logo",
"logo_url": null,
"app_name": "Acme",
"end_screen": {
"style": "celebrate",
"auto_close_seconds": 6
},
"custom_css": "",
"cancel_button_check": "ok",
"classes": [
"nc-modal",
"nc-title",
"nc-reason",
"nc-offer",
"nc-btn-primary",
"nc-btn-cancel",
"nc-footer"
]
},
"access": "full",
"access_reason": null,
"banner": {
"show": true,
"kind": "payment_failed",
"audience": "admins",
"text": "Votre dernier paiement de €49.00 n'est pas passé. Mettez à jour votre moyen de paiement pour garder votre compte.",
"cta_url": "http://localhost:3000/c/okIJi02RYcm_ylJZATU-2KwNQpxXw64R",
"cta_label": "Payer maintenant",
"dismiss_label": "Masquer",
"dismissible_for_hours": 24,
"restricts_at": "2026-11-11T10:00:00Z",
"days_left": 7,
"details": {
"kind": "payment_failed",
"amount_due_cents": 4900,
"currency": "eur",
"invoice_due_at": null,
"payment_method": null,
"decline": "expired"
}
},
"wall": {
"show": false,
"kind": "payment_failed",
"text": "Votre compte est suspendu jusqu'au paiement de €49.00.",
"cta_url": "http://localhost:3000/c/okIJi02RYcm_ylJZATU-2KwNQpxXw64R",
"cta_label": "Mettre à jour le moyen de paiement",
"shows_after_days": 7,
"cancel": {
"label": "Résilier mon abonnement",
"fallback_url": "http://localhost:3000/k/pk_test_bCx4srPPXRCaL7ykidyP3qc498kijZxq"
},
"details": null
}
} Hide the banner for 24 hours
- Publishable key (
pk_test_…) + customer token (X-NoChurn-Customer-Token), from an allowed Origin. - From a server: a secret key (
nc_test_…) and thecustomerinstead of a token.
HEADER PARAMETERS
- X-NoChurn-Customer-Token string
- Origin string
RETURNS
-
200hidden; the wall cannot be dismissed — the Inapp state object
curl https://api.nochurn.sh/v1/inapp/dismiss \
-X POST \
-H "Authorization: Bearer pk_test_…" \
-H "X-NoChurn-Customer-Token: eyJhbGciOiJIUzI1NiJ9…"const res = await fetch("https://api.nochurn.sh/v1/inapp/dismiss", {
method: "POST",
headers: {
Authorization: "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
},
});
const inappState = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/inapp/dismiss",
headers={
"Authorization": "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
},
)
inapp_state = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/inapp/dismiss")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer pk_test_…"
req["X-NoChurn-Customer-Token"] = "eyJhbGciOiJIUzI1NiJ9…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
inapp_state = JSON.parse(res.body) {
"object": "inapp_state",
"customer": "cst_vCz9mjyDe3XdmdTJ",
"failing_since": "2026-11-04T10:00:00Z",
"locale": "en",
"texts": {
"banner_text": "Your last payment of €49.00 didn't go through. Update your payment method to keep your account.",
"banner_cta": "Update payment method",
"dismiss": "Dismiss",
"banner_days_left": "{{days}} days left before your account is on hold.",
"banner_last_day": "Last day before your account is on hold.",
"wall_title": "Your account is on hold",
"wall_text": "Your account is on hold until the payment of €49.00 goes through.",
"wall_cta": "Update payment method",
"pay_cta": "Update card and pay €49.00",
"amount_due": "Amount due",
"card_expired": "Expired {{exp}}",
"decline_expired": "Card expired",
"decline_insufficient_funds": "Insufficient funds",
"decline_authentication_required": "Your bank needs you to approve the payment",
"decline_declined": "Declined by your bank",
"checking_payment": "Checking your payment…",
"back_in_title": "You're back in",
"back_in_text": "Payment received. Thanks!",
"data_safe": "Your data is safe. Nothing is deleted.",
"paused_banner_text": "Your plan is paused until {{date}}.",
"paused_title": "Your plan is paused",
"paused_text": "No charges while it's paused. Your data is kept.",
"paused_resumes": "Resumes by itself in {{days}} days, at €49.00.",
"resume_cta": "Resume now",
"resuming": "Resuming…",
"resumed_title": "Welcome back",
"resumed_text": "Your plan is active again.",
"cancel_now": "Cancel my subscription"
},
"theme": {
"object": "cancel_flow_theme",
"preset": "light",
"format": "modal",
"tokens": {
"bg": "#f5f6fa",
"surface": "#ffffff",
"text": "#1c1e4a",
"muted": "#686b85",
"accent": "#4637d9",
"accent_text": "#ffffff",
"border": "#e3e5ec",
"danger": "#c2410c",
"font": "system-ui, sans-serif",
"font_size": 15,
"radius": 12,
"radius_control": 8,
"overlay": 0.5,
"max_width": 480,
"shadow": "md",
"spacing": "regular",
"border_width": 1,
"border_style": "solid",
"font_scale": 1.25,
"font_weight_title": 600
},
"font_url": null,
"brand": "logo",
"logo_url": null,
"app_name": "Account 1",
"end_screen": {
"style": "celebrate",
"auto_close_seconds": 6
},
"custom_css": "",
"cancel_button_check": "ok",
"classes": [
"nc-modal",
"nc-title",
"nc-reason",
"nc-offer",
"nc-btn-primary",
"nc-btn-cancel",
"nc-footer"
]
},
"access": "full",
"access_reason": null,
"banner": {
"show": false,
"kind": "payment_failed",
"audience": "admins",
"text": "Your last payment of €49.00 didn't go through. Update your payment method to keep your account.",
"cta_url": "http://localhost:3000/c/XjLe-todmcu_jXSv4LVBfduidG16OE8l",
"cta_label": "Update payment method",
"dismiss_label": "Dismiss",
"dismissible_for_hours": 24,
"restricts_at": "2026-11-11T10:00:00Z",
"days_left": 7,
"details": null
},
"wall": {
"show": false,
"kind": "payment_failed",
"text": "Your account is on hold until the payment of €49.00 goes through.",
"cta_url": "http://localhost:3000/c/XjLe-todmcu_jXSv4LVBfduidG16OE8l",
"cta_label": "Update payment method",
"shows_after_days": 7,
"cancel": {
"label": "Cancel my subscription",
"fallback_url": "http://localhost:3000/k/pk_test_BBBxnJuz1rBMZ7B1jEkAwn4sKy8napzG"
},
"details": null
}
}