GUIDES
Access check
Block your app on the server after a failed payment or during a paused plan, with GET /v1/customers/{id}/access and the customer.access_* webhooks.
The in-app wall is a convenience: a script blocker or a direct call to your API gets around it. If access to your app must stop after a failed payment, check it on your server too. NoChurn uses one calculation for the wall, this endpoint and the webhooks, so they always agree.
When a customer is restricted
- Failed payment. The recovery flow is published and running in the mode, its in-app wall is on, and an invoice is still unpaid after the grace period:
wall.after_days(0 to 60) after the failure, orannual_grace_days(0 to 90) for an annual plan.0restricts as soon as the payment fails. - Paused plan. The cancel flow’s pause setting shows a wall (
pause.inapp: "wall", off by default) and every live subscription of the customer is paused.
A customer held out of the flow, a customer whose flows are paused, a paused flow or a wall turned off: access stays full.
Check it on your server
Secret key, read access to customers. {id} is any of the customer’s ids (cst_…, cus_… or your user id).
{
"object": "customer_access",
"customer": "cst_4w9Nj8EJEvWj7MDT",
"livemode": false,
"access": "restricted",
"reason": "payment_failed",
"since": "2026-11-11T10:00:00Z",
"failing_since": "2026-11-04T10:00:00Z",
"restricts_at": null,
"invoice": "ninv_1dMMAnaZyLmBNtDB",
"pay_url": "https://…",
"cancel_url": "https://cancel.nochurn.sh/a/…",
"subscription": null,
"paused_at": null,
"resumes_at": null
}
access:fullorrestricted;reason:null,payment_failedorpaused.since: when the restriction started;restricts_at: when it will start if nothing is paid (nullonce restricted, or without a wall).pay_url: the same payment link as the recovery emails.cancel_url: a signed page with one Cancel button, valid 30 days. A blocking page must offer both: paying and cancelling (click to cancel).- Pause:
subscription,paused_atandresumes_at.
Block your app’s pages and API when access is restricted, never the pages to pay or cancel. If NoChurn cannot be reached, let the customer in (fail open): a paying customer is never locked out by an outage.
curl https://api.nochurn.sh/v1/customers/user_123/access -H "Authorization: Bearer $NOCHURN_API_KEY"// Node 18+, server side only. Fails open: if NoChurn cannot be reached, the customer keeps access.
const NOCHURN_API_URL = process.env.NOCHURN_API_URL || "https://api.nochurn.sh";
export async function checkAccess(userId) {
try {
const response = await fetch(`${NOCHURN_API_URL}/v1/customers/${encodeURIComponent(String(userId))}/access`, {
headers: { Authorization: `Bearer ${process.env.NOCHURN_API_KEY}` },
signal: AbortSignal.timeout(3000),
});
if (!response.ok) return { access: "full", reason: null, error: response.status };
return await response.json();
} catch (error) {
return { access: "full", reason: null, error: String(error) };
}
}
// Express: block the app, never the pages to pay or cancel.
// app.use("/app", async (req, res, next) => {
// const result = await checkAccess(req.user.id);
// if (result.access !== "restricted") return next();
// res.status(402).json({ reason: result.reason, pay_url: result.pay_url, cancel_url: result.cancel_url });
// });# Python, requests, server side only. Fails open.
import os
from urllib.parse import quote
import requests
NOCHURN_API_URL = os.environ.get("NOCHURN_API_URL", "https://api.nochurn.sh")
def check_access(user_id):
try:
response = requests.get(f"{NOCHURN_API_URL}/v1/customers/{quote(str(user_id), safe='')}/access",
headers={"Authorization": f"Bearer {os.environ['NOCHURN_API_KEY']}"}, timeout=3)
except Exception as error: # network error, timeout
return {"access": "full", "reason": None, "error": str(error)}
if response.status_code >= 300:
return {"access": "full", "reason": None, "error": response.status_code}
return response.json()# Ruby, Net::HTTP, server side only. Fails open.
require "net/http"
require "json"
require "uri"
require "erb"
def check_access(user_id)
uri = URI("#{ENV.fetch('NOCHURN_API_URL', 'https://api.nochurn.sh')}/v1/customers/#{ERB::Util.url_encode(user_id.to_s)}/access")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('NOCHURN_API_KEY')}"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 3, read_timeout: 3) { |http| http.request(request) }
return { "access" => "full", "reason" => nil, "error" => response.code.to_i } unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body)
rescue StandardError => e # network error, timeout
{ "access" => "full", "reason" => nil, "error" => e.message }
endCache it with the webhooks
Rather than calling the API on every request, listen to two webhooks and keep the state on your side:
customer.access_restricted: once, when the grace period ends (or the plan is paused with the wall on). It carriescustomer,access,access_reason,invoice,since,failing_since,grace_daysand a readablereason.customer.access_restored: when the invoice is paid, voided or marked uncollectible, recovery stops, the settings change or the plan resumes, withrestored_because.
Each transition is sent once. NoChurn re-checks customers with an unpaid invoice under recovery every 10 minutes, and right after a payment. When in doubt, GET /v1/customers/{id}/access is the truth at any time.