> ## 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.

# Authenticate from n8n

> Call the Attract API from n8n's HTTP Request node: with your API key's token alone, or with the token and an HMAC signature made by n8n's Crypto node.

> [card]
>
> ![n8n](https://mintlify.s3.us-west-1.amazonaws.com/fetchski-attract-api/logo/n8n.png)
>
> ![Fetchski Attract](https://mintlify.s3.us-west-1.amazonaws.com/fetchski-attract-api/logo/attract-glyph-ball-green-on-light-scalable.svg)
>
> ![Fetchski Attract](https://mintlify.s3.us-west-1.amazonaws.com/fetchski-attract-api/logo/attract-glyph-ball-green-on-dark-scalable.svg)
>
> Coming soon: the Fetchski Attract node for n8n
>
> A full community node for the Attract API: every call a ready-made action, with your key and its
> signing handled for you. Until it ships, n8n's **HTTP Request** node reaches the whole API, as
> this page shows.

Every call from n8n is an **HTTP Request** node with your API key attached. How it attaches the key
depends on one setting of the key, its **Request signing**:

> [card] [Token only](#token-only)
>
> One credential, one node. The HTTP Request node sends the token in a header. That's all.

> [card] [Token + HMAC signature](#token-and-hmac-signature)
>
> Two small nodes before the HTTP Request node sign it. A stolen token alone gets nowhere.

## At a glance

> [tab] Token only
>
> Trigger
>
> any trigger
>
> HTTP Request
>
> Header Auth: your token
>
> The HTTP Request node carries the token itself, from a Header Auth credential.

> [tab] Token + HMAC signature
>
> Trigger
>
> any trigger
>
> What to sign
>
> Edit Fields
>
> Sign it
>
> Crypto: Hmac
>
> HTTP Request
>
> token + 2 signature headers
>
> Edit Fields writes down what is signed, Crypto signs it, and the HTTP Request node sends both.

## Which requests need a signature

| 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.

So a key on **Token + signature on writes** reads with the token alone and signs only its writes.
[Signing requests](https://docs.fetchski-attract.com/signing.md) explains what a signature proves.

## Make the API key

**1. Open API Keys**

In your site's CMS, press **⌘K** (**Ctrl+K** on Windows), type `API Keys`, press **Enter**, then
click **Create API Key**.

**2. Fill it in**

* **Label:** `n8n`
* **Scopes:** only what your workflow calls. To list and read pages, keep **Start from a pack**
  and pick the **Read pages** pack. For anything else, choose **Pick scopes one by one**: every
  page of the API reference names its scope under **Access**.
* **Request signing:** **Token only**, **Token + signature on writes** or **Token + signature
  on every request**.

The **Read pages** pack ticks one scope:

| Scope | Why |
| - | - |
| `content:pages:read` | List and read your published pages |

Picked the **Read pages** pack? Check **Granted** before you create the key: it should list
exactly the scope above. Your site's packs can be edited, so if it shows any other scope (a
publish or delete one, say), take it out with **Edit scopes**, or have someone who manages API
keys choose **Restore default packs** in **Scope Packs**. Picked your scopes one by one? Keep
the ones your workflow calls.

**3. Copy what it shows you**

Click **Create key**. The CMS shows these once:

> [card] Secret token
>
> `fsk_acme_…`
>
> Sent with **every** request, in the `Authorization` header.

> [card] Signing secret
>
> `fss_…`
>
> Only for signed requests: the key the HMAC is made with. **Never sent.**

> [!WARNING]
>
> Both are shown once. Keep them in n8n **credentials**, never typed into a node: a credential is
> stored encrypted and never appears in the workflow, its exports or its executions.

## Token only

The site reads the token from one header: the word `ApiKey`, a space, then the token.

```http theme={"dark"}
Authorization: ApiKey fsk_acme_…
```

**1. Save the token as a Header Auth credential**

In n8n open **Credentials** → **Create credential** → **Header Auth**, and fill in:

| Field | Value |
| - | - |
| **Name** | `Authorization` |
| **Value** | `ApiKey fsk_acme_…`: `ApiKey`, a space, then your whole token |

Save it as `Fetchski Attract key`.

![n8n's Header Auth credential with Name Authorization and the Value filled in](https://mintcdn.com/fetchski-attract-api/mn9qfQRsqaASh7UE/images/n8n/header-auth-credential.png?fit=max\&auto=format\&n=mn9qfQRsqaASh7UE\&q=85\&s=000c487a25e89bb1358e49c4b3c5fe60)

The Header Auth credential

**2. Pick it on the HTTP Request node**

Add an **HTTP Request** node. Its **URL** is your site and the route, for example:

```text URL wrap theme={"dark"}
https://acme.example/api/v1/@apostrophecms/page
```

and its other settings:

| Setting | Value |
| - | - |
| **Method** | The route's method, e.g. `GET` |
| **Authentication** | `Generic Credential Type` |
| **Generic Auth Type** | `Header Auth` |
| **Header Auth** | `Fetchski Attract key` |

![n8n's HTTP Request node set to GET the pages list with the Fetchski Attract key credential](https://mintcdn.com/fetchski-attract-api/mn9qfQRsqaASh7UE/images/n8n/list-your-pages.png?fit=max\&auto=format\&n=mn9qfQRsqaASh7UE\&q=85\&s=a530f06907ea35a3d6ba2af88768991a)

The HTTP Request node, with the Fetchski Attract key credential picked under Header Auth

**3. Run it**

Click **Execute step**. **You should see** your pages in the output panel.

> [!TIP]
>
> That's all a token only key needs: every HTTP Request node that calls your site picks the same
> credential. Next: [List pages, then get each one](https://docs.fetchski-attract.com/recipes/n8n-list-pages-then-get-each.md).

## Token and HMAC signature

A signed request sends the same `Authorization` header, plus two more:

```http theme={"dark"}
Authorization: ApiKey fsk_acme_…
X-Fetchski-Timestamp: 1792000000
X-Fetchski-Signature: v1=3f1c…e9a0
```

### What gets signed

The signature is an **HMAC-SHA256** of four parts joined with dots, made with your signing secret
and written as lower-case hex. For a `GET` of your pages list:

> [card] timestamp
>
> Now, in Unix seconds. The same value goes in `X-Fetchski-Timestamp`. The site allows five
> minutes either way.

> [card] method
>
> The request's method, in capitals: `GET`, `POST`, `PATCH`, `DELETE`.

> [card] path + query
>
> From `/api/v1`, with the query exactly as sent. Not the site's address.

> [card] body
>
> The exact body sent, character for character. Empty for a `GET`, so the string ends in a dot.

### Build it in n8n

Two nodes go just before the HTTP Request node:

What to sign

Edit Fields: 4 fields

Sign it

Crypto: adds signature

HTTP Request

sends the headers

**1. Save the signing secret as a Crypto credential**

In n8n open **Credentials** → **Create credential** → **Crypto**. Paste the whole signing secret,
`fss_` included, in **Hmac Secret**, and leave the other fields empty. Save it as `Fetchski
        Attract signing secret`.

Make the `Fetchski Attract key` **Header Auth** credential too, as in [Token only](#token-only).

![n8n's Crypto credential with the Hmac Secret field filled in](https://mintcdn.com/fetchski-attract-api/mn9qfQRsqaASh7UE/images/n8n/crypto-credential.png?fit=max\&auto=format\&n=mn9qfQRsqaASh7UE\&q=85\&s=0afd7d1436023795baa147c93b7daea4)

The Crypto credential: only Hmac Secret is filled in

**2. What to sign: an Edit Fields node**

Add an **Edit Fields (Set)** node with four fields, one for each part:

| Name | Type | Value |
| - | - | - |
| `timestamp` | Number | `{{ $now.toUnixInteger() }}` |
| `method` | String | `GET` |
| `path` | String | `/api/v1/@apostrophecms/page?all=1&flat=1&fields=title` |
| `body` | String | Empty for a `GET`; for a write, the exact JSON it sends |

![n8n's Edit Fields node with the timestamp, method, path and body fields](https://mintcdn.com/fetchski-attract-api/mn9qfQRsqaASh7UE/images/n8n/edit-fields-what-to-sign.png?fit=max\&auto=format\&n=mn9qfQRsqaASh7UE\&q=85\&s=57147d500df1db7df36e9be3bf9552f4)

What to sign: the four parts

**3. Sign it: a Crypto node**

Add a **Crypto** node after it:

| Setting | Value |
| - | - |
| **Credential to connect with** | `Fetchski Attract signing secret` |
| **Action** | `Hmac` |
| **Type** | `SHA256` |
| **Property Name** | `signature` |
| **Encoding** | `HEX` |

and its **Value**, the four parts joined with dots:

```text Value wrap theme={"dark"}
{{ $json.timestamp }}.{{ $json.method }}.{{ $json.path }}.{{ $json.body }}
```

It adds `signature` to the item and keeps the four fields, so the next node can read all five.
This node is the same for every request you sign.

![n8n's Crypto node set to Hmac SHA256 with HEX encoding, writing the signature property](https://mintcdn.com/fetchski-attract-api/mn9qfQRsqaASh7UE/images/n8n/crypto-sign-it.png?fit=max\&auto=format\&n=mn9qfQRsqaASh7UE\&q=85\&s=7292049d1001d73be2fbc01309939e2a)

Sign it: Hmac, SHA256, HEX, and the signature it adds to the item

**4. Send it: the HTTP Request node**

Set it as for a token only key, but take the **URL** from `path`:

```text URL wrap theme={"dark"}
https://acme.example{{ $json.path }}
```

| Setting | Value |
| - | - |
| **Method** | `GET`, the same as `method` |
| **Authentication** | `Generic Credential Type` |
| **Generic Auth Type** | `Header Auth` |
| **Header Auth** | `Fetchski Attract key` |
| **Send Headers** | On |

Under **Send Headers**, add the two signature headers:

| Name | Value |
| - | - |
| `X-Fetchski-Timestamp` | `{{ $json.timestamp }}` |
| `X-Fetchski-Signature` | `v1={{ $json.signature }}` |

![n8n's HTTP Request node with the X-Fetchski-Timestamp and X-Fetchski-Signature headers](https://mintcdn.com/fetchski-attract-api/mn9qfQRsqaASh7UE/images/n8n/http-request-signed.png?fit=max\&auto=format\&n=mn9qfQRsqaASh7UE\&q=85\&s=7a9583ab30c4c4f5f6a56c0168bcddee)

The HTTP Request node sends the two signature headers

**5. Run it**

Click **Execute workflow**. **You should see** every node turn green, a `signature` on **Sign
it**, and your pages on the HTTP Request node.

![The n8n canvas after a signed run: trigger, What to sign, Sign it and List your pages, all green](https://mintcdn.com/fetchski-attract-api/mn9qfQRsqaASh7UE/images/n8n/signed-canvas.png?fit=max\&auto=format\&n=mn9qfQRsqaASh7UE\&q=85\&s=0dcbaae133e274fc93dc1caf75f2257c)

A signed run: What to sign, Sign it, then the request the site accepted

> [accordion] Signing a write (POST, PATCH, DELETE)
>
> Two things change. In **What to sign**, set `method` to the write's method and `body` to the exact
> JSON you send, for example:
>
> ```text body wrap theme={"dark"}
> {{ JSON.stringify({ title: 'New title' }) }}
> ```
>
> On the HTTP Request node, turn on **Send Body**, pick **Body Content Type** `Raw`, set **Content
> Type** to `application/json` and **Body** to `{{ $json.body }}`, so the body sent is the one
> signed.

> [!WARNING]
>
> **Send exactly what you signed.**
>
> * Put the query in `path`. Leave **Send Query Parameters** off: a query added there isn't signed.
> * Send a body as **Raw** `{{ $json.body }}`, never as **JSON**, which n8n rebuilds and may change.
> * Sign in the same run as the request: a signature expires after five minutes, and a signed write
>   works once.

## If a request is refused

Click the red node. n8n shows its own message at the top, and the site's answer, which names the
fix, under **Error details**.

> [accordion] Authorization failed - please check your credentials
>
> The site answered `401`. Its message tells you which case it is:
>
> | The site's message starts | What to fix |
> | - | - |
> | `Invalid API key` | The Header Auth **Value** must be `ApiKey `, then the whole token, on one line. A deleted, switched-off or expired key is refused too. |
> | `This request must be signed` | The key's **Request signing** needs a signature on this request: add **What to sign** and **Sign it**. |
> | `X-Fetchski-Signature does not match this request` | The **Hmac Secret** must be the whole `fss_…` secret, the URL must be your site plus `{{ $json.path }}`, and a body must be sent **Raw**. |
> | `X-Fetchski-Timestamp is missing, malformed or more than 5 minutes` | Check the clock of the machine n8n runs on. |
> | `This signature was already used` | A signed write was sent twice. Run it again: each run signs afresh. |

> [accordion] Forbidden - perhaps check your credentials?
>
> The site answered `403`: the key lacks the scope this request needs, and the error's details
> name it. Edit the key in **API Keys** and tick that scope.

> [accordion] The service is receiving too many requests from you
>
> The site answered `429`: the key went over its requests a minute (600 reads and 120 writes
> unless set otherwise). On the HTTP Request node open **Options** → **Batching** and send fewer
> items a minute.

## Related

> [card] [List pages, then get each one](https://docs.fetchski-attract.com/recipes/n8n-list-pages-then-get-each.md)
>
> Chain two HTTP Request nodes through your pages' ids

> [card] [Signing requests](https://docs.fetchski-attract.com/signing.md)
>
> The signature in detail, with every refusal

> [card] [Authentication](https://docs.fetchski-attract.com/authentication.md)
>
> Scopes and each key's security policy

> [card] [Required scopes](https://docs.fetchski-attract.com/scopes.md)
>
> Every operation's scope, per content type


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