> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shieldlabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Security

> Learn how ShieldLabs protects your data in transit and how to verify what you receive.

Securing a ShieldLabs integration comes down to four things: verify that every webhook really came from us, keep your secret key on the server, run everything over HTTPS, and know exactly what the snippet's payload protection does and does not give you. This page is the reference for each.

<Note>
  ShieldLabs scores visits; your code owns the decision. Nothing here changes that: these are integration security controls, not a verdict engine. The Risk Score is `0-100` (Clean / Low / Medium / High) and you act on it in your own backend.
</Note>

## Webhook authenticity (HMAC-SHA256)

Your webhook URL is a public endpoint. Anyone who learns it can POST to it. The only thing that proves a delivery actually came from ShieldLabs is its signature, so **verify every webhook before you trust the body.** The signature is the `X-Shield-Signature` header: `sha256=` plus the hex HMAC-SHA256 (a keyed cryptographic hash that only someone holding the secret can produce) of the **raw request body**, keyed with that endpoint's `whsec_…` signing secret, constant-time compared, rejecting with `401` on a mismatch.

The exact formula, the Node, Go, and Python handlers, the raw-bytes gotcha, and idempotency on `request_id` all live on the [webhooks](/setup/webhooks) page.

## Key handling

Every domain has a **public key**, a **private API key**, and a **secret key**, scoped to that single domain. They have different trust levels. The [API keys](/setup/keys) page is the full reference for which key each API uses.

| Key                        | Where it belongs                          | What it can do                                                                                                           | Safe in the browser? |
| -------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------- |
| **Public key**             | The snippet URL on your site              | Identifies the domain so the server knows which account a fingerprint belongs to                                         | Yes, by design       |
| **Private API key**        | Your server only (`sec_…`)                | Authenticates the [History API](/api/server-api) on `account.shieldlabs.ai` (`Authorization: Bearer`)                    | No, never            |
| **Secret key**             | Your server only                          | Authenticates the [Management API](/api/server-api) on `api.shieldlabs.ai` (`Authorization: Bearer` + `X-Shield-Domain`) | No, never            |
| **Webhook signing secret** | Your server only (`whsec_…` per endpoint) | Verifies the `X-Shield-Signature` header on incoming webhooks                                                            | No, never            |

The public key is meant to be visible. It ships in your page source as the `?publicKey=` parameter and cannot read data, change settings, or authenticate against the Server API. A request is only accepted when the public key matches the domain it is served from, so a key lifted from your page will not work on someone else's site.

Your server-side keys (the private API key and the secret key) authenticate the Server API. **Anyone holding one can read your domain's data**, so they must never reach the browser. Webhook endpoints use separate `whsec_…` secrets; treat a leaked webhook secret the same way and rotate it from the dashboard.

<Warning>
  Never put the secret key in client-side code, the snippet, a public repository, a build artifact, or any place a browser can reach. Store it in an environment variable or a secrets manager. Use a different key set for every domain so a leak is contained to one site.
</Warning>

### Rotate when exposed

If a secret may have leaked (a committed `.env`, a log line, an offboarded teammate), rotate the domain's key set from the dashboard right away. Rotation issues a fresh public key and secret key, shows the new secret in full once, and invalidates the old set immediately, so update the snippet and your server together. The step-by-step rotation flow, masking, and the Profile health check live on the [API keys](/setup/keys) page.

## Transport security (HTTPS / TLS)

Everything moves over HTTPS.

* **ShieldLabs hosts** (`cdn.shieldlabs.ai`, `rest.shieldlabs.ai`, `webrtc.shieldlabs.ai`, `api.shieldlabs.ai`, `app.shieldlabs.ai`) are served over TLS (the encryption behind HTTPS).
* **Your webhook callback URL must be HTTPS.** It receives signed scores and identifiers, so terminate TLS in front of your handler.
* **Your Server API calls must be HTTPS.** They carry your server-side keys, so a plaintext request would put a credential on the wire. Always call over HTTPS (`https://account.shieldlabs.ai/…`, `https://api.shieldlabs.ai/…`).

<Warning>
  The snippet requires a **secure context** (in practice, an HTTPS page): it uses the Web Crypto API (`crypto.subtle`), which browsers only expose over HTTPS (and on `localhost` for local development). On an insecure `http://` page the payload-encryption step is unavailable and the snippet cannot run as intended. Serve any page that loads the snippet over HTTPS.
</Warning>

## Payload protection (honest scope)

The snippet wraps its signal payload with **AES-256-GCM** (a strong symmetric encryption cipher) before POSTing it to `rest.shieldlabs.ai`. This is worth understanding precisely, because it is easy to overstate.

The encryption key is **derived from your public key**, and the public key is the same value that travels in cleartext in the snippet URL. So the wrapping protects payload **integrity and obfuscates it in transit on top of TLS**. It is not a secret-key scheme, and it is **not end-to-end encryption.**

<Warning>
  Do not describe ShieldLabs payloads as end-to-end encrypted. The wrapping key comes from a public value, so it cannot provide confidentiality against someone who has the public key. The actual confidentiality on the wire comes from **TLS**, which is why HTTPS is mandatory. Treat AES-256-GCM here as tamper-resistance and obfuscation layered on top of TLS, nothing more.
</Warning>

There is nothing for you to configure. The snippet handles wrapping automatically in a secure context, and the server accepts both wrapped and plain payloads. Your job is to keep the page on HTTPS so TLS does the real confidentiality work.

## Data handling on your side

A few practices keep the data you exchange with ShieldLabs clean.

* **Pass a hashed `UserHID`, never a raw identifier.** When you call `checkAuthenticatedUser`, send a hashed or pseudonymous account id, not a real email or user id. It is echoed back in webhooks and history, so keep it opaque.
* **The public key is the only credential in the browser.** Identifiers like the client cookie id and session id live client-side by design and break on storage clear. The durable identity (DeviceID) and the Risk Score are derived server-side and reach you through signed webhooks and the [Server API](/api/server-api), never assembled in the page.
* **Keep raw signals server-side.** Read scores and signals from your verified webhook handler or the History API, and apply your allow / challenge / review / block logic in your backend. There is no in-product rules engine to leak through.

The [privacy](/privacy) page covers what is and is not collected, and who controls retention.

## Responsible disclosure

If you find a security issue in ShieldLabs, report it privately to **[contact@shieldlabs.ai](mailto:contact@shieldlabs.ai)**. Please include enough detail to reproduce it, and give us a reasonable window to confirm and fix before any public disclosure. We do not pursue good-faith researchers who follow coordinated disclosure.

## Security checklist

<Steps>
  <Step title="Verify every webhook">
    Constant-time compare `X-Shield-Signature` against HMAC-SHA256 of the raw request body, keyed with the endpoint's `whsec_…` secret. Reject with `401` on a mismatch.
  </Step>

  <Step title="Keep the secret on the server">
    Secret key in an environment variable or secrets manager, never in the browser. One key set per domain.
  </Step>

  <Step title="Rotate on exposure">
    Rotate the key set the moment a secret may have leaked, then update the snippet and your server together.
  </Step>

  <Step title="HTTPS everywhere">
    Snippet pages, your callback URL, and your Server API calls all over TLS. The snippet needs a secure context to run.
  </Step>

  <Step title="Hash the UserHID">
    Send only a hashed or pseudonymous account id to `checkAuthenticatedUser`.
  </Step>
</Steps>

## Related pages

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/setup/webhooks">
    `X-Shield-Signature` verification in Node, Go, and Python, and idempotency on `request_id`.
  </Card>

  <Card title="API keys" icon="key" href="/setup/keys">
    Public and secret key lifecycle, masking, and rotation.
  </Card>

  <Card title="Content Security Policy" icon="shield-halved" href="/setup/csp">
    The exact `script-src` and `connect-src` directives the snippet needs.
  </Card>

  <Card title="Privacy" icon="user-shield" href="/privacy">
    What is and is not collected, and who controls retention.
  </Card>
</CardGroup>
