API REFERENCE v1

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
  • failing_since string | null date-time
  • 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
    • 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
      • auto_close_seconds integer | null 3 to 30

        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).

      • 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
    • dismiss_label string
    • restricts_at string | null date-time

      payment_failed: when the wall comes up if still unpaid (wall enabled, not up yet); else null.

    • days_left integer | null ≥ 0

      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.

      • kind enum
        • payment_failed
      • currency string | null
      • invoice_due_at string | null date-time

        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.

        Show child attributesHide child attributes
      • 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
  • 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 token to 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
      • currency string | null
      • invoice_due_at string | null date-time

        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.

        Show child attributesHide child attributes
      • 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

Schema: InappState

THE INAPP STATE OBJECT
{
  "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 the customer instead of a token.

QUERY PARAMETERS

HEADER PARAMETERS

RETURNS

ERRORS

POST/v1/inapp/resume
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)
RESPONSE 200
{
  "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 the customer instead 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

RETURNS

ERRORS

GET/v1/inapp/state
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)
RESPONSE 200
{
  "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 the customer instead of a token.

HEADER PARAMETERS

RETURNS

POST/v1/inapp/dismiss
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)
RESPONSE 200
{
  "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
  }
}