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

# Authentication

> Create an API key, understand its scope, and exchange it for an access token

The Filed API uses a **two-step token model**. You create a long-lived **API key**
in the Filed web app, then exchange it at request time for a short-lived
**access token** that you send as a `Bearer` token on every GraphQL call.

```mermaid theme={null}
flowchart LR
  A["API key<br/>(long-lived, workspace-scoped)"] -->|exchange| B["Access token<br/>(short-lived, ~30 min)"]
  B -->|"Bearer"| C["GraphQL API<br/>(router.apps.filed.com)"]
```

All requests go to a single endpoint:

```
https://router.apps.filed.com/graphql
```

## 1. Create an API key

API keys are created from the Filed web app, per workspace:

1. Open the workspace you want to grant access to.
2. Go to **Plugins → Filed API**.
3. Click **Create an API Key**.
4. Choose an **Expiry** and an **Access** level (see below).
5. Copy the key. **It is shown only once**, so store it in a secret manager. If you
   lose it, revoke it and create a new one.

<Note>
  An API key is **personal**: it authenticates as **you**, the user who created
  it. Every call made with a token minted from the key acts on your behalf and is
  limited to what your account is allowed to do in that workspace (the `userToken`
  from the exchange identifies you). If you leave the workspace or your access
  changes, the key's access changes with you. For a shared or service integration,
  create the key from an account you intend to own that integration.
</Note>

<Warning>
  An API key is a credential. Treat it like a password: never commit it to source
  control, never expose it in a browser or mobile client, and rotate it if it may
  have leaked.
</Warning>

### Expiry

The key is valid for the window you pick at creation time. After it expires, the
exchange step (below) stops working and you must create a new key.

| Option | Key lifetime      |
| ------ | ----------------- |
| `30`   | 30 days           |
| `60`   | 60 days           |
| `90`   | 90 days (default) |
| `365`  | 1 year            |

## 2. Scope: one key, one workspace

An API key is **scoped to the single workspace it was created in**. The access
token you get from it can only read and write data in that workspace. To
integrate with several workspaces, create one key per workspace.

<Note>
  This is different from legacy partner API keys, whose token could reach every
  workspace the partner created. New Filed API keys are deliberately
  workspace-scoped for tighter, per-workspace access control.
</Note>

### Access levels: read vs read-write

The **Access** level you pick at creation time is baked into every access token
minted from that key:

| Access             | Value        | What the token can do                                                                                       |
| ------------------ | ------------ | ----------------------------------------------------------------------------------------------------------- |
| **Read only**      | `read_only`  | Run **queries** to look up clients, tasks, documents, and other workspace data. All mutations are rejected. |
| **Read and write** | `read_write` | Everything read-only can do, **plus mutations**: upload documents, trigger runs, and modify workspace data. |

Pick the narrowest level that fits your integration. If you only pull data, use
**Read only** so a leaked key can never mutate your workspace.

## 3. Exchange the API key for an access token

Send your API key as the `refreshToken` argument to
`exchangeSurfaceRefreshTokenForAccessTokens`. This is a **public** mutation: it
is the only call you make *without* a `Bearer` token.

```graphql theme={null}
mutation ExchangeApiKey($apiKey: String!) {
  exchangeSurfaceRefreshTokenForAccessTokens(refreshToken: $apiKey) {
    userToken
    workspaceToken
  }
}
```

### Arguments

<ParamField path="refreshToken" type="String!" required>
  Your API key, exactly as copied from the Filed web app.
</ParamField>

### Returns: `AccessTokens`

<ResponseField name="userToken" type="String!">
  A short-lived token identifying **you** (the user the key belongs to) across
  your account, not tied to any one workspace.
</ResponseField>

<ResponseField name="workspaceToken" type="String!">
  A short-lived token that is **also you**, scoped to **the key's workspace**. This
  is the token you use for API calls. It carries the key's access level: a
  `read_only` key mints a read-only `workspaceToken`.
</ResponseField>

Both tokens authenticate as the user who created the key; neither is a separate
service identity. The `userToken` is you account-wide, the `workspaceToken` is you
within the key's workspace at the key's access level.

Both tokens are short-lived (about 30 minutes). When they expire, call the
exchange again with the same API key to mint fresh ones. The API key itself
lasts until its expiry.

### Example

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://router.apps.filed.com/graphql \
    -H "Content-Type: application/json" \
    -d '{
      "query": "mutation ExchangeApiKey($apiKey: String!) { exchangeSurfaceRefreshTokenForAccessTokens(refreshToken: $apiKey) { userToken workspaceToken } }",
      "variables": { "apiKey": "YOUR_API_KEY" }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "exchangeSurfaceRefreshTokenForAccessTokens": {
        "userToken": "eyJhbGciOiJFUzI1NiI...",
        "workspaceToken": "eyJhbGciOiJFUzI1NiI..."
      }
    }
  }
  ```
</ResponseExample>

## 4. Call the API with the access token

Send the `workspaceToken` in the `Authorization` header on every subsequent
request:

```http theme={null}
Authorization: Bearer YOUR_WORKSPACE_TOKEN
```

Verify it works with a `me` query. `me` returns the `Me` union, which resolves to
`WorkspaceUser` when you authenticate with a `workspaceToken` (and to `User` with
an account-wide `userToken`). Because it is a union, select fields with an inline
fragment on the type you expect:

<RequestExample>
  ```graphql theme={null}
  query Me {
    me {
      __typename
      ... on WorkspaceUser {
        id
        role
        createdAt
        user {
          id
          name
          email
        }
        workspace {
          id
          name
        }
      }
    }
  }
  ```
</RequestExample>

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "me": {
        "__typename": "WorkspaceUser",
        "id": "019f0fb6-37b1-7800-b7bc-0d11288504b1",
        "role": "admin",
        "createdAt": "2026-06-14T09:31:20.000Z",
        "user": {
          "id": "019f0fb6-26e9-74b7-a842-cb43a2a41682",
          "name": "Jane Preparer",
          "email": "jane@example-firm.com"
        },
        "workspace": {
          "id": "019f0fb6-379a-7f72-b7ec-ebd8f41ccfa1",
          "name": "Example Tax Firm"
        }
      }
    }
  }
  ```
</ResponseExample>

Run it over HTTP the same way as the exchange, adding your token:

```bash cURL theme={null}
curl -X POST https://router.apps.filed.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \
  -d '{ "query": "query Me { me { __typename ... on WorkspaceUser { id role createdAt user { id name email } workspace { id name } } } }" }'
```

### Types

```graphql theme={null}
union Me = User | WorkspaceUser

type WorkspaceUser {
  id: ID!
  role: WorkspaceRole!
  createdAt: Date!
  user: UserShortDetails!
  workspace: Workspace!
}

type UserShortDetails {
  id: ID!
  name: String!
  email: String!
}
```

<ResponseField name="me" type="Me">
  Union of `User` (returned with an account-wide `userToken`) and `WorkspaceUser`
  (returned with a workspace-scoped `workspaceToken`). Query it with
  `... on WorkspaceUser { ... }` to read workspace fields.
</ResponseField>

<ResponseField name="WorkspaceUser.id" type="ID!">
  The membership id that links this user to the workspace.
</ResponseField>

<ResponseField name="WorkspaceUser.role" type="WorkspaceRole!">
  The user's role in the workspace, for example `admin` or `member`.
</ResponseField>

<ResponseField name="WorkspaceUser.createdAt" type="Date!">
  When the user was added to the workspace.
</ResponseField>

<ResponseField name="WorkspaceUser.user" type="UserShortDetails!">
  The underlying user account: `id`, `name`, and `email`.
</ResponseField>

<ResponseField name="WorkspaceUser.workspace" type="Workspace!">
  The workspace this `workspaceToken` is scoped to.
</ResponseField>

## Putting it together

A typical integration:

1. **Once**, in the web app: create a workspace-scoped API key with the access
   level you need, and store it as a secret.
2. **On startup / on 401**: exchange the API key for a fresh `workspaceToken`.
3. **Per request**: send `Authorization: Bearer <workspaceToken>`.
4. **When the token expires (\~30 min)**: repeat step 2 with the same API key.

<Tip>
  Cache the `workspaceToken` and only re-exchange when it expires (or when a call
  returns an auth error) rather than exchanging on every request.
</Tip>

## Troubleshooting

**`exchangeSurfaceRefreshTokenForAccessTokens` returns an error**

* The API key is wrong, revoked, or past its expiry. Create a new one.
* Confirm you are posting to `https://router.apps.filed.com/graphql`.

**Queries work but mutations are rejected**

* The key was created as **Read only** (`read_only`). Create a **Read and write**
  key to allow mutations.

**Requests fail after \~30 minutes**

* The `workspaceToken` expired. Re-exchange the API key for a new one.
