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.
The mistake to avoid
Section titled “The mistake to avoid”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.
Origins by region
Section titled “Origins by region”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.
Which directive needs which origin
Section titled “Which directive needs which origin”| 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.
Permissions-Policy
Section titled “Permissions-Policy”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.
selfis not quoted. - List the capture origin, not your own origin alone. An allowlist of
selfon its own grants the camera to your page and denies it to the frame. - Keep
selfin the list, even if your page never opens the camera itself. Your page can only delegate a permission it holds, so an allowlist that leavesselfout 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
cameradoes 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
cameradirective, the attribute is enough on its own. The frame gets the camera and you need no header. This is why a page that sends noPermissions-Policyat all works unchanged. - If your page does declare a
cameraallowlist, that allowlist must nameselfand 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.js
Section titled “Next.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}")`, }, ], }, ]; },};Express
Section titled “Express”const CHECKTIV_CAPTURE_ORIGIN = 'https://embed.us.checktiv.com';
app.use((req, res, next) => { res.setHeader('Permissions-Policy', `camera=(self "${CHECKTIV_CAPTURE_ORIGIN}")`); next();});A static _headers file
Section titled “A static _headers file”/* Permissions-Policy: camera=(self "https://embed.us.checktiv.com")Content-Security-Policy
Section titled “Content-Security-Policy”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 strictstyle-srcis 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 instyle-src(see the table above) and'unsafe-inline'does not. The SDK does set two layout-only inlinestyleattributes on the capture wrapper, which a strictstyle-srcdrops: capture still works, and the effect is cosmetic (slightly tighter spacing above one inline alert, and themaxWidthmount option is ignored). Addstyle-src-attr 'unsafe-inline'if you passmaxWidthand need it honored.
Headers that do not affect the journey
Section titled “Headers that do not affect the journey”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.
Confirm it works
Section titled “Confirm it works”-
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-policyIf 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.
-
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 asPermissions policy violation: camera is not allowed in this document. Either one means yourcameraallowlist 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
selfis in the list, unquoted. Your page can only delegate a camera grant it holds itself, so withoutselfthere 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
embedfrom the table above. The CDN host beginning withsdkis 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.
- The bare word
-
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 namingframe-srcmeans the frame was blocked: add the capture origin toframe-src, or add the directive if your policy only setsdefault-src. -
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.cssonce 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.
Related pages
Section titled “Related pages”- Quickstart - the full integration walkthrough
- Error reference - every error code with its cause and recovery step
- Modes and regions - test mode, live mode, and choosing your region