> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://api.docs.modulards.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://api.docs.modulards.com/_mcp/server.

# Authentication

The Public API authenticates every request with a **personal access token**. Send it as a bearer token:

```bash
curl https://api.modulards.com/api/public/v1/sites \
  -H "Authorization: Bearer $MODULAR_DS_TOKEN"
```

A session cookie from the dashboard never authenticates a Public API request, even when you are logged in.

## Creating a token

Create a token in Modular DS under **My profile > API > Create key**. The token belongs to your membership in the organization you create it in: it acts in that organization only, with the role you have there (see [Organizations](/organizations)). If your membership is removed, the token stops working.

Keep tokens secret. Anyone holding a token can do in that organization everything its abilities and your role allow.

## Read-only and read and write tokens

When you create a token you choose whether it can write:

| Token          | Can call                       |
| -------------- | ------------------------------ |
| Read-only      | `GET` requests only            |
| Read and write | Every request your role allows |

A read-only token that sends a `POST`, `PATCH`, `PUT` or `DELETE` gets a `403`:

```json
{
  "errors": [
    { "status": "403", "title": "Forbidden", "detail": "This token is read-only." }
  ],
  "jsonapi": { "version": "1.1" }
}
```

A few reads hand out credentials and therefore need a read and write token too. Reading a website's manual connection credentials is one of them.

## Failed authentication

A missing, malformed, unknown or expired token answers `401`:

```json
{
  "errors": [
    { "status": "401", "title": "Unauthenticated", "detail": "Unauthenticated." }
  ],
  "jsonapi": { "version": "1.1" }
}
```

The token is checked before the URL is resolved, so an unauthenticated request answers `401` whether or not the resource it names exists.

## Credential handling

Ordinary responses never carry a credential value:

* **Manual connection credentials** are revealed through a signed URL that is valid for 10 minutes and can be used once.
* **The connection plugin** is downloaded from a presigned URL that is valid for one hour.
* **Backup archives** cannot be downloaded through the Public API. Downloads and restorations stay in the dashboard.