Skip to content

Security headers

Read this page if your page sends a Permissions-Policy or a Content-Security-Policy header. They do different jobs here: Permissions-Policy delegates the camera into the capture frame, while Content-Security-Policy governs your own page and decides, through frame-src, whether that frame loads at all. Getting either one wrong stops the applicant before the camera opens. There are two ways to get the camera grant wrong: an allowlist that leaves the capture origin out, and one that leaves self out.

If your page sends neither header, there is nothing to configure here. The SDK grants the camera to the capture frame itself, so the journey works with no header changes.

Two Checktiv hosts take part in an embedded journey, and they are easy to confuse:

  • The capture origin, https://embed.us.checktiv.com. The verification journey runs in a frame served from this host. This is the origin the browser checks when the frame asks for the camera.
  • The CDN origin, https://sdk.us.checktiv.com. The SDK script, the chunks it loads on demand, and its stylesheet are served from this host, on script-tag installs.

Grant the camera to the capture origin. The CDN origin is the intuitive guess, because it is the host in your <script> tag and the host that appears in stack traces. It is the wrong one: a browser matches a camera grant against the frame’s own document origin, never against the origin the frame’s scripts came from.

Use the region your publishable key encodes (ah_pk_us_* or ah_pk_eu_*). See Modes and regions.

Host US region EU region
Capture origin (camera and frame) https://embed.us.checktiv.com https://embed.eu.checktiv.com
API origin (session and token calls) https://sdk-api.us.checktiv.com https://sdk-api.eu.checktiv.com
CDN origin (script-tag installs) https://sdk.us.checktiv.com https://sdk.eu.checktiv.com

The rest of this page uses the US hosts. Swap us for eu throughout if your key is an EU key.

What your page does Header directive Origin to allow
Lets the journey open the camera Permissions-Policy: camera Capture origin, plus self
Opens the verification frame frame-src Capture origin
Calls the session and token endpoints connect-src API origin
Loads the SDK and its on-demand chunks script-src CDN origin
Loads the SDK stylesheet style-src CDN origin

The script-src and style-src entries apply to script-tag installs only. If you install from npm, your bundler serves the SDK and its styles from your own origin and no CDN entry is needed.

The script-src entry is load-bearing for every script-tag integration, not just for pages using a particular feature. The file your <script> tag names does not contain the modules. Each one is a separate file on the CDN origin that the SDK fetches the first time an applicant reaches a step needing it, so script-src governs the whole journey rather than the opening tag alone. Allow the CDN origin there and every step loads; leave it out and the tag still loads while the first real step does not.

A blocked module does not leave a blank area, which is worth knowing before you spend time on the wrong header. Each module that renders something replaces its area with a short line asking the applicant to reload, and your onEvent handler receives an error carrying sdk_load_failed. Fraud signal collection has no visible surface, so it reports through onEvent alone. An area that renders nothing at all is a different problem, so read Confirm it works before changing this directive.

The stylesheet is what gives the capture frame its size and shape, and the custom-form step its field styling. The SDK links it from the CDN origin the first time a step renders, so there is nothing to add to your page. If style-src blocks it, the journey still runs and still completes, but the capture frame renders at the browser’s default frame size instead of filling the space you gave it. Allow the CDN origin in style-src and it renders correctly.

camera is the only Permissions Policy feature the journey needs. It never asks for the microphone, the screen, or the location, so leave your other feature allowlists as they are.

You do not need to allow anything the capture frame loads for itself. The frame fetches its own styles, its scan engine, and its supporting files under the policy Checktiv serves on the frame. Your page’s policy governs your page.

If you also embed the hosted review workspace, allow its origin in frame-src as well. See Workspace reviewer.

The camera grant is the entry integrations miss. At minimum:

Permissions-Policy: camera=(self "https://embed.us.checktiv.com")

Syntax rules that are easy to get wrong:

  • Wrap the allowlist in parentheses, and quote every origin. self is not quoted.
  • List the capture origin, not your own origin alone. An allowlist of self on its own grants the camera to your page and denies it to the frame.
  • Keep self in the list, even if your page never opens the camera itself. Your page can only delegate a permission it holds, so an allowlist that leaves self out leaves your page with no camera grant and nothing to hand to the frame. An allowlist naming the capture origin alone denies the frame just as surely as one that leaves the capture origin out.
  • Keep the other directives you already send. Adding camera does not remove them.

The SDK already sets allow="camera" on the capture frame, so you never write that attribute yourself. What that attribute does depends on your header:

  • If your page declares no camera directive, the attribute is enough on its own. The frame gets the camera and you need no header. This is why a page that sends no Permissions-Policy at all works unchanged.
  • If your page does declare a camera allowlist, that allowlist must name self and the capture origin. The attribute delegates the camera to the frame, but delegation only passes on what your page already holds: it can neither add an origin your header left out nor give the frame a grant your page does not have itself.
next.config.js
const CHECKTIV_CAPTURE_ORIGIN = 'https://embed.us.checktiv.com';
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Permissions-Policy',
value: `camera=(self "${CHECKTIV_CAPTURE_ORIGIN}")`,
},
],
},
];
},
};
const CHECKTIV_CAPTURE_ORIGIN = 'https://embed.us.checktiv.com';
app.use((req, res, next) => {
res.setHeader('Permissions-Policy', `camera=(self "${CHECKTIV_CAPTURE_ORIGIN}")`);
next();
});
/*
Permissions-Policy: camera=(self "https://embed.us.checktiv.com")

Configure this only if your page sends a CSP. Merge these directives into the policy you already send:

Content-Security-Policy:
script-src 'self' https://sdk.us.checktiv.com;
style-src 'self' https://sdk.us.checktiv.com;
connect-src 'self' https://sdk-api.us.checktiv.com;
frame-src https://embed.us.checktiv.com;

Two things you do not need:

  • 'wasm-unsafe-eval'. The capture frame runs WebAssembly under the policy Checktiv serves on the frame, not under yours.
  • style-src 'unsafe-inline'. A strict style-src is supported. The SDK adds the styles it builds at runtime through the CSS object model rather than through an inline <style> element, and it loads the rest as an ordinary stylesheet from the CDN origin, which is why that origin belongs in style-src (see the table above) and 'unsafe-inline' does not. The SDK does set two layout-only inline style attributes on the capture wrapper, which a strict style-src drops: capture still works, and the effect is cosmetic (slightly tighter spacing above one inline alert, and the maxWidth mount option is ignored). Add style-src-attr 'unsafe-inline' if you pass maxWidth and need it honored.

X-Frame-Options and the CSP frame-ancestors directive control who may frame your page. They place no restriction on frames your page opens, so you can keep X-Frame-Options: DENY and run the journey.

  1. Read the header back off the deployed page. Headers are often set per route or per environment, so a config change that landed on the wrong route looks the same as no change at all.

    Terminal window
    curl -sI https://your-page.example.com | grep -i -e permissions-policy -e content-security-policy

    If the header is absent or still the old value, fix the route pattern or the environment your header rule applies to, then re-run this command.

  2. Start a verification and read the browser console. On Chrome and Edge the SDK checks the capture frame’s own camera permission as the frame starts up, and logs a [Checktiv] line naming the capture origin the frame needs, what the frame is currently allowed, and the full header line to add. The browser logs its own message too, such as Permissions policy violation: camera is not allowed in this document. Either one means your camera allowlist does not reach the capture frame.

    Take the allowlist you read back in step 1 and check both halves. Either half missing denies the frame on its own, so finding one of them present is not confirmation:

    • The bare word self is in the list, unquoted. Your page can only delegate a camera grant it holds itself, so without self there is nothing to hand to the frame however precisely the rest of the list is written.
    • A quoted capture origin is in the list, the host beginning with embed from the table above. The CDN host beginning with sdk is the wrong one and grants the frame nothing.

    Add whichever half is absent, deploy, and re-run step 1. The line under Permissions-Policy above is the shape both halves make together. Other browsers log neither message, so a silent console here is not confirmation. Use step 1 for that.

  3. Check the Network tab for the capture frame. Look for a request to embed.us.checktiv.com/idv/capture. A CSP violation in the console naming frame-src means the frame was blocked: add the capture origin to frame-src, or add the directive if your policy only sets default-src.

  4. If the capture area renders but is unstyled or tiny, this is not a header problem. That is the missing capture stylesheet. Import @checktiv/sdk-web/capture-ui/style.css once in your app, as shown in the Quickstart.

If capture still does not start after step 3 passes, see camera_policy_blocked and sdk_load_failed in the error reference for the remaining causes.