# Authentication

Source: https://www.solscanner.app/docs/embed/authentication

> API keys and domain whitelisting.

API key [#api-key]

You get one key per org. It starts with `emb_` and is 68 characters long. Keys can be viewed and rotated from the admin dashboard. After a rotation the old key stops working within 5 minutes, because key lookups are cached for that long. Plan the swap with that window in mind, and treat a leaked key as live until it has passed.

Pass it as a query param:

```
/sol/scanner/{ADDRESS}/map?embed=1&key=emb_a1b2c3d4e5f6...
/rh/scanner/{ADDRESS}/map?embed=1&key=emb_a1b2c3d4e5f6...
```

Embed keys are publishable: they sit in your page's HTML, so treat them like a public identifier rather than a secret. Your domain list controls where the map can be framed and filters API calls on a best-effort basis. Your usage limits are what bound spend on a key.

Domain whitelisting [#domain-whitelisting]

Both chains use the same key and domain settings.

**No domains configured (public mode).** If your org has no allowed domains, the key works on any site: the iframe can be framed anywhere and API calls are accepted from any origin. This is the default for a new org until you ask us to add domains.

**Domains configured.** Once at least one domain is set:

* The iframe response sets a `Content-Security-Policy: frame-ancestors` rule for your domains, and the browser blocks embedding on other sites.
* API calls from inside the iframe originate from Scanner itself, so the backend trusts Scanner's frontend origin while separately validating the embed key and quota.
* Direct API calls from other origins are checked against the allowed-domain list and receive `403 EMBED_DOMAIN_REJECTED` if rejected.
* Requests that carry the key but send neither an `Origin` nor a `Referer` header (for example `curl` or a server-side fetch) are rejected with `403 EMBED_DOMAIN_REJECTED`. Browsers always send `Origin` on the iframe's own API calls, so real embeds are unaffected.

Server-to-server calls [#server-to-server-calls]

Embed keys are for browsers. If you need Scanner data from your backend, use an `sk_live_` API key instead; see [API keys](/docs/api/keys). Header checks are a filter, not authentication: a script can send any `Origin` it likes, which is why quotas and rate limits apply to every key.

Matching rules:

| You register      | Works on                                                    | Doesn't work on            |
| ----------------- | ----------------------------------------------------------- | -------------------------- |
| `app.example.com` | `app.example.com`                                           | `example.com`, `other.com` |
| `example.com`     | `example.com`, `sub.example.com`                            | `other.com`                |
| `localhost`       | `http://localhost:3000`, `http://localhost:5173` (any port) | Remote origins             |
| `localhost:5173`  | `http://localhost:5173` only                                | Other ports                |

If you add `example.com`, all subdomains work too. To add or change domains, contact us.

Local development [#local-development]

For dev environments, register either `localhost` (matches any port) or the specific `localhost:PORT` you run your integrator on. Both schemes `http://` and `https://` are accepted by the iframe's `frame-ancestors` CSP.

Validate a key [#validate-a-key]

Check if a key is valid without triggering a scan:

```
GET https://api.scanner.net/embeds/validate?key={API_KEY}
```

No auth needed. Returns:

```json
{ "valid": true, "domains": ["app.example.com", "staging.example.com"] }
```

Or if invalid:

```json
{ "valid": false, "domains": [] }
```

Use this before mounting the iframe to fail fast when a key is rotated, disabled, or typo'd: the iframe itself cannot report invalid-key state to the host page (cross-origin), so a pre-flight check is the cleanest way to surface it.

Errors [#errors]

| Status | Code                    | Meaning                                                                                                                                                                                                                                               |
| ------ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 403    | `EMBED_KEY_INVALID`     | Bad key or key not found                                                                                                                                                                                                                              |
| 403    | `EMBED_ORG_DISABLED`    | Org disabled by admin                                                                                                                                                                                                                                 |
| 403    | `EMBED_DOMAIN_REJECTED` | The request's `Origin` (or `Referer`) is not on your domain list, or your org has domains set and the request sent neither header. Check the page's domain against your list; for server-to-server calls, use an `sk_live_` [API key](/docs/api/keys) |
