# SignWith API authentication: API keys

> Every request to the SignWith API carries an API key in the Authorization header. Keys belong to the user who created them, and they can be live or test, full access or read-only.

Source: https://signwith.co/docs/api/authentication · Updated 2026-10-02

## Send the key as a Bearer token

Put the key in the `Authorization` header of every request:

```http
Authorization: Bearer sw_live_3xAmPl3K3y...
```

A request without a valid key gets `401 unauthenticated`. There is no other way to authenticate with the REST API: OAuth tokens are only for the [MCP server](https://signwith.co/docs/mcp), not for `/api/v1`.

## Create and revoke keys

[Get your API key](https://app.signwith.co/settings/api)

Create keys in SignWith under [Settings → Developers](https://app.signwith.co/settings/api), and revoke them there too. Each key belongs to the user who created it and acts with that user's permissions, so a key can only see the templates and signature requests its user can see.

Keep keys on your server. Anyone with a key can act as its user, so never put one in a web page, a mobile app or a public repository. To replace a key, create the new one, deploy it, then revoke the old one.

## Live and test keys

The prefix tells you which environment a key acts on:

| Prefix | Environment | What happens |
| --- | --- | --- |
| `sw_live_` | Live | Sends real signing emails and uses credits. |
| `sw_test_` | Test | Acts on your test account. Use it while you build; its data is kept apart from live data. |

Both use the same base URL, `https://app.signwith.co/api/v1`. To check which environment, user and account a key belongs to, call [`GET /me`](https://signwith.co/docs/api/account#get-the-current-api-keys-user-and-account):

cURL:

```bash
curl https://app.signwith.co/api/v1/me \
  -H "Authorization: Bearer $SIGNWITH_API_KEY"
```

Node:

```javascript
const response = await fetch('https://app.signwith.co/api/v1/me', {
  headers: {
    Authorization: `Bearer ${process.env.SIGNWITH_API_KEY}`,
  },
})

console.log(response.status, await response.json())
```

Python:

```python
import os

import requests

response = requests.get(
    "https://app.signwith.co/api/v1/me",
    headers={
        "Authorization": f"Bearer {os.environ['SIGNWITH_API_KEY']}",
    },
)

print(response.status_code, response.json())
```

The response's `environment` is `live` or `test`, and `api_key.permission` is `full` or `read`.

## Full-access and read-only keys

A **full-access** key can call every endpoint. A **read-only** key can call:

- any `GET` endpoint,
- [`POST /documents/verify`](https://signwith.co/docs/api/documents#verify-a-signed-pdf), which checks a PDF without changing anything,
- [`POST /feedback`](https://signwith.co/docs/api/feedback#send-feedback-to-the-signwith-team), which sends feedback to the SignWith team.

Every other write with a read-only key returns `403 read_only_api_key`. Use read-only keys for dashboards, reporting and anything else that only needs to look. Each endpoint in the [API reference](https://signwith.co/docs/api) says which kind of key it needs.

## Key expiry

A key can have an expiry date, shown as `api_key.expires_at` in [`GET /me`](https://signwith.co/docs/api/account#get-the-current-api-keys-user-and-account) (`null` if it never expires). After that date every request returns `401 api_key_expired`, and you need a new key.

## Authentication errors

| HTTP | Code | What to do |
| --- | --- | --- |
| 401 | `unauthenticated` | The key is missing, malformed or revoked, or its account was archived. Check the header format and the key. |
| 401 | `api_key_expired` | The key passed its expiry date. Create a new one. |
| 403 | `read_only_api_key` | A read-only key tried to change something. Use a full-access key. |
| 403 | `forbidden` | The key's user can't access this resource. |

All error codes are on the [errors page](https://signwith.co/docs/api/errors).
