Skip to main content
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. 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.
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.
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.

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.
OptionKey lifetime
3030 days
6060 days
9090 days (default)
3651 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.
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.

Access levels: read vs read-write

The Access level you pick at creation time is baked into every access token minted from that key:
AccessValueWhat the token can do
Read onlyread_onlyRun queries to look up clients, tasks, documents, and other workspace data. All mutations are rejected.
Read and writeread_writeEverything 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.
mutation ExchangeApiKey($apiKey: String!) {
  exchangeSurfaceRefreshTokenForAccessTokens(refreshToken: $apiKey) {
    userToken
    workspaceToken
  }
}

Arguments

refreshToken
String!
required
Your API key, exactly as copied from the Filed web app.

Returns: AccessTokens

userToken
String!
A short-lived token identifying you (the user the key belongs to) across your account, not tied to any one workspace.
workspaceToken
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.
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

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" }
  }'
{
  "data": {
    "exchangeSurfaceRefreshTokenForAccessTokens": {
      "userToken": "eyJhbGciOiJFUzI1NiI...",
      "workspaceToken": "eyJhbGciOiJFUzI1NiI..."
    }
  }
}

4. Call the API with the access token

Send the workspaceToken in the Authorization header on every subsequent request:
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:
query Me {
  me {
    __typename
    ... on WorkspaceUser {
      id
      role
      createdAt
      user {
        id
        name
        email
      }
      workspace {
        id
        name
      }
    }
  }
}
{
  "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"
      }
    }
  }
}
Run it over HTTP the same way as the exchange, adding your token:
cURL
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

union Me = User | WorkspaceUser

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

type UserShortDetails {
  id: ID!
  name: String!
  email: String!
}
me
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.
WorkspaceUser.id
ID!
The membership id that links this user to the workspace.
WorkspaceUser.role
WorkspaceRole!
The user’s role in the workspace, for example admin or member.
WorkspaceUser.createdAt
Date!
When the user was added to the workspace.
WorkspaceUser.user
UserShortDetails!
The underlying user account: id, name, and email.
WorkspaceUser.workspace
Workspace!
The workspace this workspaceToken is scoped to.

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.
Cache the workspaceToken and only re-exchange when it expires (or when a call returns an auth error) rather than exchanging on every request.

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.