Validation
Wire the /validate API into your backend when no platform integration covers your stack.
Overview
If a platform integration covers your stack, use it. This page is for custom backends and platforms not yet supported. You will call POST /validate for every protected request and act on the response.
When to call /validate
- On the server, after you have parsed the request.
- Once per request, before your application handles it.
- Skip locally for paths you do not want to protect — the validator returns
not_matchedfor those, but skipping saves a round trip.
Handle each decision
Call POST /validate with the request URL, method, client IP, all headers, and the _centinel cookie.
Apply every entry from response.headers to your outgoing response, and every entry from response.cookies as a Set-Cookie.
Branch on response.decision.
For block or redirect, base64-decode response.response_html and return it as the response body.
For allow or not_matched, pass the request through to your application.
type ValidateDecision = 'allow' | 'block' | 'redirect' | 'not_matched';
async function enforce(req, res, next) {
const result = await callValidate({
url: req.url,
method: req.method,
ip: clientIpFrom(req),
referrer: req.headers.referer,
cookie: req.cookies['_centinel'],
headers: req.headers,
});
for (const [name, value] of Object.entries(result.headers)) res.setHeader(name, value);
for (const c of result.cookies ?? []) res.cookie(c.name, c.value, { path: c.path, domain: c.domain });
switch (result.decision) {
case 'allow':
case 'not_matched':
return next();
case 'block':
return res.status(403).send(Buffer.from(result.response_html, 'base64').toString('utf8'));
case 'redirect':
return res.status(200).send(Buffer.from(result.response_html, 'base64').toString('utf8'));
}
}Decoding response_html
Both block and redirect carry response_html as base64-encoded UTF-8. Decode and return as the body. Use HTTP 403 for block, 200 for redirect — or honour response.status_code if you prefer the validator's recommendation.
// Node.js
const html = Buffer.from(response.response_html, 'base64').toString('utf8');// Web platforms (Cloudflare Workers, Deno, browsers)
const html = new TextDecoder().decode(
Uint8Array.from(atob(response.response_html), c => c.charCodeAt(0))
);Applying headers
Every response carries a headers object Centinel wants set on your outgoing response — Content-Type, security headers, observability identifiers. Copy them through; do not hardcode your own.
// Express
for (const [name, value] of Object.entries(response.headers)) {
res.setHeader(name, value);
}// Cloudflare Worker
const headers = new Headers();
for (const [name, value] of Object.entries(response.headers)) {
headers.set(name, value);
}
return new Response(body, { status, headers });Common pitfalls
- Calling
/validatefrom the browser. Thex-api-keyis server-only. Anyone with the key has full tenant access. - Hardcoding
Content-Type. Always copyresponse.headersthrough. They may include observability or session headers your app needs. - Ignoring
cookiesonallow. Thecookiesarray can be non-empty on any decision (session cookies on a happy-path response, challenge cookies onredirect). Apply them all. - Passing your CDN's IP as
ip. Pass the real client IP fromX-Forwarded-Foror your platform's equivalent. Wrong IP breaks detection. - Dropping the
_centinelcookie. Without it, session tracking across interstitial challenges fails.