# API guide for developers

This guide shows the calls most automations start with. How to switch the API on and create a key
is in [API and webhooks](https://docket.rocks/en/docs/api-and-webhooks). Every example is checked against docket's schema
whenever docket changes.

## The basics

- **One address:** `POST https://yourworkspace.docket.rocks/graphql` with
  `Authorization: Bearer dkt_…` and a JSON body `{ "query": "…", "variables": { … } }`.
- **A key is a person.** It sees and may do what its owner can in the app: a Project Manager's key
  reads invoices, a Time Tracker's key sees its own time only. What the role doesn't allow comes
  back as `null` or as an error, never as someone else's data.
- **The reference is the schema:** `GET /graphql/schema` returns it as text, with a description on
  every query, every mutation and the objects they return. Load it into your GraphQL client for
  autocompletion.
- **Ids** are UUIDs, the same in the API, in webhook events and in the app's addresses.
- **Dates** are `YYYY-MM-DD` in your workspace's time zone. **Durations** are minutes. **Money** is
  net, in your workspace's currency, unless a field says gross.

## Signing in

There is one way for your own integrations: a personal **API key** in the `Authorization` header.
No cookies, no login call, no token to renew. A key works until its owner deletes it or is
archived, or an admin switches the API off.

docket's own programs sign in differently: the web app with a session after e-mail, password and
second factor, the Mac app with the same credentials, and AI apps through a consent screen (see
[Connect AI apps](https://docket.rocks/en/docs/ai-connections)). Those ways belong to docket's clients and may change
without notice. Don't build on them, and don't put a password into a script: a key can be deleted
on its own and can never change users, settings or other keys.

Every query and every mutation with an example call and answer, readable without a key, is in the
[API reference](https://docket.rocks/en/docs/api-reference).

## Who am I

```graphql
query {
  viewer {
    id
    firstname
    lastname
    role
  }
}
```

## Lists: filter, order, pages

Every list takes the same arguments.

```graphql
query Projects($search: String, $page: Int) {
  total: projects(filter: { search: $search, archived: false }) {
    count
  }
  projects(filter: { search: $search, archived: false }, orderBy: ["title"], perPage: 50, page: $page) {
    edges {
      node {
        id
        pnr
        title
        slug
        customer {
          id
          company
        }
      }
    }
  }
}
```

- **Totals come from a second field without `perPage`** (`total` above): on a paged list, `count`
  and `sum` only see the page. Ask for `page` 1, 2, 3 until you have `total.count` entries.
- `orderBy` takes field names; a leading `-` sorts descending (`["-date"]`).
- Date filters take comparisons as text: `">= 2026-10-01"`, `"<= 2026-10-31"`, or a single day.
- Id filters take lists; most have a `…Not` twin that excludes.

## Time entries of a month

```graphql
query Time($project: [ID!], $dates: [String!]) {
  total: timetrackings(filter: { projectIds: $project, dates: $dates }) {
    count
    minutes: sum(field: "duration")
  }
  timetrackings(filter: { projectIds: $project, dates: $dates }, orderBy: ["date"], perPage: 100, page: 1) {
    edges {
      node {
        id
        date
        duration
        description
        user {
          id
          firstname
          lastname
        }
        service {
          id
          title
        }
        ticket {
          id
          number
          title
        }
        invoice {
          id
          number
        }
      }
    }
  }
}
```

With `"dates": [">= 2026-10-01", "<= 2026-10-31"]`. `filter: { billing: [OPEN] }` lists what no
invoice has billed yet; `invoice` names the invoice of an entry that is billed (for keys that may
see invoices).

## Booking time

Time is booked on a ticket of a project, with a service (the kind of work) and a duration in
minutes, in steps of 15.

```graphql
mutation Book($input: CreateTimetrackingInput!) {
  createTimetracking(input: $input) {
    timetracking {
      id
      date
      duration
    }
    errors
  }
}
```

```json
{
  "input": {
    "projectId": "…",
    "ticketId": "…",
    "serviceId": "…",
    "date": "2026-11-12",
    "duration": 90,
    "description": "Homepage: hero and navigation"
  }
}
```

Without `userId` the time is the key owner's. Find the ids first: `projects`, a project's
`tickets`, and `services`.

## Creating a ticket

```graphql
mutation NewTicket($input: CreateTicketInput!) {
  createTicket(input: $input) {
    ticket {
      id
      number
      title
    }
    errors
  }
}
```

With `{ "input": { "projectId": "…", "title": "Check the contact form", "text": "The form answers with an error." } }`.
`text` is plain text; blank lines separate paragraphs. Comments take it too (`createTicketComment`).

## Invoices

```graphql
query Invoices($year: Int) {
  invoices(filter: { year: $year, statuses: ["published", "paid"] }, orderBy: ["-date"], perPage: 50, page: 1) {
    edges {
      node {
        id
        number
        title
        status
        date
        dueDate
      }
    }
  }
}
```

Only Project Managers and Admins read invoices. To hear about an invoice the moment it is issued
or paid, use a [webhook](https://docket.rocks/en/docs/api-and-webhooks) instead of asking again and again.

## Files

Where the API names a file (an external cost's `invoice { downloadUrl }`, a ticket's
`attachments { url }`), it gives a path on your workspace. Fetch it with the same key:

```
GET https://yourworkspace.docket.rocks/rails/active_storage/blobs/redirect/…/receipt.pdf
Authorization: Bearer dkt_…
```

The answer redirects to the file, so let your tool follow redirects. A key gets the files its owner
may open in the app: a receipt only with a Project Manager's or an Admin's key. Without a key, or
with one that may not, the answer is 404. File requests count towards the key's 120 a minute.

## When something goes wrong

| What you get | What it means |
|--------------|---------------|
| HTTP 401 | The key is missing, wrong or deleted, or the API is switched off for the workspace. |
| HTTP 429 | More than 120 requests in a minute with this key. Wait and try again. |
| HTTP 200 with `errors` at the top of the answer | The query doesn't fit the schema, or the key's owner may not do this. |
| HTTP 200 with `errors` inside the mutation's answer | The record wasn't saved. `errors` names each field with its messages, as the form in the app would show them. |

A mutation that worked answers with `errors: null`. Always ask for `errors` and check it.

## What a key can't do

Workspace settings, users and their roles, connections to Jira and AI apps, and API keys and
webhooks themselves are changed in the app only. Those mutations say so in the schema.

Related: [API and webhooks](https://docket.rocks/en/docs/api-and-webhooks) · [User roles](https://docket.rocks/en/docs/user-roles)