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

# Authentication

> API keys, their scopes and each key's security policy.

An **API key** is issued in the site's CMS under **API Keys** and sent in the `Authorization`
header, either with `ApiKey ` in front or bare, exactly as the CMS showed it:

```http With the scheme theme={"dark"}
Authorization: ApiKey fsk_<site>_<key id>_<secret>
```

```http Bare (as the playground sends it) theme={"dark"}
Authorization: fsk_<site>_<key id>_<secret>
```

## In the SDKs

The SDKs take the key exactly as the site showed it, from the environment, and add the header (and
each signature) themselves:

```ini .env theme={"dark"}
FETCHSKI_ATTRACT_SITE=acme                    # Laravel: the slug in the key
FETCHSKI_ATTRACT_URL=https://acme.example
FETCHSKI_ATTRACT_API_KEY=fsk_acme_…
FETCHSKI_ATTRACT_SIGNING_SECRET=fss_…         # only when the key has one
```

> [!TIP]
>
> In Laravel, confirm the site answers, the key is accepted and the clocks agree (a signature allows five
> minutes):
>
> ```bash Terminal theme={"dark"}
> php artisan attract:check
> ```

## What a key may do

Each key holds only the scopes its app needs. A family's `…:all` scope holds every scope in it, and
`admin:all` holds them all.

> [card] Media
>
> `images:read` · `videos:read` · `photo-feeds:read`
>
> Family: `media:all`

> [card] Blog
>
> `content:blog:read` · `:write` (drafts, imports too) · `:publish` (going live) · `:delete` ·
> `:generate-image` (AI images, which cost money). An import's new images and videos need the media
> `content:` scopes.
>
> Family: `content:blog:all`

> [card] Forms
>
> `forms:read`: the submissions visitors send.
>
> Family: `forms:all`

> [card] Ads & conversions
>
> `conversions:read` · `conversions:write` · `attribution:read` · `ads-metrics:read`
>
> Family: `ads:all`

> [card] Request log
>
> `request-log:read` · `request-log:params:read`

> [card] Content
>
> `content:<type>:read` · `:write` · `:publish` · `:delete` per content family: `pages` covers every
> page type, `images` / `files` / `videos` their tags and folders too. Images and files go live on
> every save, so their `:write` publishes too and they have no `:publish`. The page templates are not
> pages of the site: the Pages list leaves them out, and `content:pages:read-templates` reads them
> through Page Templates. [Required scopes](https://docs.fetchski-attract.com/scopes.md) lists each operation's.
>
> Per family: `content:<type>:all`. Family: `content:all`

> [card] Site settings & design
>
> `global:read` · `global:write` · `global:publish` · `global:css` (Custom CSS)
>
> Family: `global:all`

> [card] Languages
>
> `languages:read` · `languages:write` (add, rename and remove languages: a save is live at once)
>
> Family: `languages:all`

> [card] Everything
>
> `admin:all`

Every route names the scope it needs in the API reference. A key without it gets
403, and the message names the scope. One exception: on a public type, a
key without the read scope still reads the published documents' public fields, as anyone can. A key
never manages people, roles, API keys or the site's connections: those stay with a person signed in to
the CMS.

## What a key reaches

> [card] [Every content type](https://docs.fetchski-attract.com/scopes.md)
>
> List, read, create, change, publish and delete through each type's own routes, with that type's
> scopes. [Required scopes](https://docs.fetchski-attract.com/scopes.md) lists every operation's.

> [card] The feature routes
>
> Media, the blog import, form submissions, conversions and attribution, ads metrics and the
> request log: each with its scope above.

> [!NOTE]
>
> A write scope works on **drafts**. On a type with drafts, a write in published mode (the default) also
> takes the document live, so it needs the type's publish scope too; send `aposMode=draft` to change only
> the draft. A type without drafts (redirects, photo feeds, the media folders) needs only its write scope.
> A key with only a read scope sees published documents only.

> [!TIP]
>
> Without a key, the public still reads the published documents of the public types (blog posts, authors,
> team members, …) as visitors see them.

## Each key's security policy

Chosen when the key is created, and shown on its **Security** tab.

> [accordion] Signing mode
>
> **Token only**, **signed writes**, or **every request signed**. A signed request carries
> `X-Fetchski-Timestamp` and `X-Fetchski-Signature`, made with the key's signing secret (`fss_…`).
>
> | Request | Must it be signed? |
> | - | - |
> | Any request from a key whose signing mode is **Token only** | No |
> | A write (`POST`, `PUT`, `PATCH`, `DELETE`) from a key on **Signed writes** | Yes, uploads included |
> | Any request from a key on **Every request signed** | Yes, reads too |
> | A Conversion Data API write: [record a conversion](https://docs.fetchski-attract.com/api-reference/conversion-events/post-a-conversion-another-system-saw.md) or [adjust one](https://docs.fetchski-attract.com/api-reference/conversion-events/retract-or-restate-a-conversion-event.md) | Always, from every API key, whatever its mode |
> | A person signed in to the CMS (bearer token or session) | Never |
>
> An unsigned request that had to be signed gets 401, and the message says so.
>
> Each signature works once, so a replayed request is refused. The SDKs sign for you: give them the
> secret along with the key. [Signing requests](https://docs.fetchski-attract.com/signing.md) shows how to sign by hand, uploads included.

> [accordion] Network allowlist
>
> Optional: the addresses the key may be used from. A call from anywhere else gets `403`.

> [accordion] Rate limits
>
> Reads and writes per minute, set per key: 600 reads and 120 writes by default. Over the limit the
> site answers `429` with `Retry-After`, the seconds to wait. The SDKs wait and retry for you.

## How a signed request is checked

```mermaid theme={"dark"}
sequenceDiagram
    participant App as Your app
    participant Site as The site
    App->>App: HMAC-SHA256 of timestamp.METHOD.path.body, keyed with the signing secret
    App->>Site: Request + Authorization, X-Fetchski-Timestamp, X-Fetchski-Signature
    Site->>Site: Check the key, its scopes and allowlist
    Site->>Site: Check the signature, its timestamp (within 5 minutes) and that it was never used
    Site-->>App: The answer, or 401 / 403 saying why
```

An upload signs the SHA-256 of its body in the body's place, declared in `X-Fetchski-Content-SHA256`.
[Signing requests](https://docs.fetchski-attract.com/signing.md) has the details and code in cURL, Node, Laravel and Python.

## Trying calls in the playground

**1. Set your site**

In any endpoint's playground, set **host** to your site's host name (`acme.example`).

**2. Paste your key**

In **Authorization**, paste the key exactly as the CMS showed it (`fsk_…`). Nothing goes in front.

**3. Send**

The playground can't sign, so use a key whose signing mode is **token only**, holding the scope
the endpoint names under **Access**. A Conversion Data API write must always be signed, so try it
from code instead ([Signing requests](https://docs.fetchski-attract.com/signing.md)).

> [!WARNING]
>
> The playground sends each request through the docs site's own proxy, so your key travels with it. Make a
> key just for trying calls: only the scopes you are trying and an **Expires** date soon, then switch it off or
> delete it under **API Keys** when you are done. Never paste the key your app runs on.

## People, not apps

A person signed in to the CMS uses a bearer token or a session instead: see **Signing in (people)**
in the API reference. Apps always use an API key.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.