API guide for developers

How to read and write docket over its GraphQL API: lists, filters and pages, booking time, creating tickets, reading invoices, and what an error looks like.

This guide shows the calls most automations start with. How to switch the API on and create a key is in 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). 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.

Who am I

query {
  viewer {
    id
    firstname
    lastname
    role
  }
}

Lists: filter, order, pages

Every list takes the same arguments.

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

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.

mutation Book($input: CreateTimetrackingInput!) {
  createTimetracking(input: $input) {
    timetracking {
      id
      date
      duration
    }
    errors
  }
}
{
  "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

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

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 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 · User roles