# API-Leitfaden für Entwickler

Dieser Leitfaden zeigt die Aufrufe, mit denen die meisten Automationen anfangen. Wie ihr die API
einschaltet und einen Schlüssel anlegt, steht unter [API und Webhooks](https://docket.rocks/doku/api-und-webhooks).
Jedes Beispiel wird gegen dockets Schema geprüft, sobald sich docket ändert.

## Die Grundlagen

- **Eine Adresse:** `POST https://euerworkspace.docket.rocks/graphql` mit
  `Authorization: Bearer dkt_…` und einem JSON-Body `{ "query": "…", "variables": { … } }`.
- **Ein Schlüssel ist eine Person.** Er sieht und darf, was seine Besitzerin oder sein Besitzer in
  der App darf: Der Schlüssel eines Projektmanagers liest Rechnungen, der eines Zeiterfassers sieht
  nur die eigenen Zeiten. Was die Rolle nicht erlaubt, kommt als `null` oder als Fehler zurück, nie
  als fremde Daten.
- **Die Referenz ist das Schema:** `GET /graphql/schema` liefert es als Text, mit einer
  Beschreibung an jeder Abfrage, jeder Mutation und den Objekten, die sie zurückgeben. Die
  Beschreibungen sind englisch. Ladet es in euren GraphQL-Client, dann ergänzt er Felder von selbst.
- **IDs** sind UUIDs, dieselben in der API, in Webhook-Ereignissen und in den Adressen der App.
- **Daten** sind `JJJJ-MM-TT` in der Zeitzone eures Workspace. **Dauern** sind Minuten. **Beträge**
  sind netto in der Währung eures Workspace, außer ein Feld sagt brutto.

## Anmelden

Für eigene Integrationen gibt es einen Weg: einen persönlichen **API-Schlüssel** im Header
`Authorization`. Keine Cookies, kein Login-Aufruf, kein Token, das erneuert werden muss. Ein
Schlüssel gilt, bis die Person ihn löscht oder archiviert wird oder ein Admin die API ausschaltet.

dockets eigene Programme melden sich anders an: die Web-App mit einer Sitzung nach E-Mail, Passwort
und zweitem Faktor, die Mac-App mit denselben Zugangsdaten und KI-Apps über eine Zustimmung im
Browser (siehe [KI-Apps verbinden](https://docket.rocks/doku/ki-apps-verbinden)). Diese Wege gehören dockets Clients und
können sich ohne Ankündigung ändern. Baut nicht darauf und schreibt kein Passwort in ein Skript:
Ein Schlüssel lässt sich einzeln löschen und kann nie Nutzer, Einstellungen oder andere Schlüssel
ändern.

Jede Abfrage und jede Mutation mit Beispielaufruf und Antwort, lesbar ohne Schlüssel, steht in
der [API-Referenz](https://docket.rocks/doku/api-referenz).

## Wer bin ich

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

## Listen: filtern, sortieren, blättern

Jede Liste nimmt dieselben Argumente.

```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
        }
      }
    }
  }
}
```

- **Summen kommen aus einem zweiten Feld ohne `perPage`** (oben `total`): Auf einer geblätterten
  Liste sehen `count` und `sum` nur die Seite. Fragt `page` 1, 2, 3 ab, bis ihr `total.count`
  Einträge habt.
- `orderBy` nimmt Feldnamen, ein `-` davor sortiert absteigend (`["-date"]`).
- Datumsfilter nehmen Vergleiche als Text: `">= 2026-10-01"`, `"<= 2026-10-31"` oder einen
  einzelnen Tag.
- ID-Filter nehmen Listen, die meisten haben einen Zwilling `…Not`, der ausschließt.

## Zeiteinträge eines Monats

```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
        }
      }
    }
  }
}
```

Mit `"dates": [">= 2026-10-01", "<= 2026-10-31"]`. `filter: { billing: [OPEN] }` listet, was noch
keine Rechnung abgerechnet hat. `invoice` nennt die Rechnung eines abgerechneten Eintrags (für
Schlüssel, die Rechnungen sehen dürfen).

## Zeit buchen

Zeit wird auf ein Ticket eines Projekts gebucht, mit einer Leistung (der Art der Arbeit) und einer
Dauer in Minuten, in Schritten von 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": "Startseite: Hero und Navigation"
  }
}
```

Ohne `userId` gehört die Zeit der Person, der der Schlüssel gehört. Die IDs findet ihr vorher über
`projects`, die `tickets` eines Projekts und `services`.

## Ein Ticket anlegen

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

Mit `{ "input": { "projectId": "…", "title": "Kontaktformular prüfen", "text": "Das Formular antwortet mit einem Fehler." } }`.
`text` ist einfacher Text, Leerzeilen trennen Absätze. Kommentare nehmen ihn auch (`createTicketComment`).

## Rechnungen

```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
      }
    }
  }
}
```

Rechnungen lesen nur Projektmanager und Admins. Wollt ihr in dem Moment erfahren, in dem eine
Rechnung veröffentlicht oder bezahlt wird, nehmt einen [Webhook](https://docket.rocks/doku/api-und-webhooks), statt
immer wieder zu fragen.

## Dateien

Wo die API eine Datei nennt (`invoice { downloadUrl }` an Fremdkosten, `attachments { url }` an
einem Ticket), gibt sie einen Pfad in eurem Workspace. Ruft ihn mit demselben Schlüssel ab:

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

Die Antwort leitet zur Datei weiter, euer Werkzeug muss also Weiterleitungen folgen. Ein Schlüssel
bekommt die Dateien, die seine Person in der App öffnen darf: einen Beleg nur mit dem Schlüssel
eines Projektmanagers oder Admins. Ohne Schlüssel, oder mit einem, der es nicht darf, ist die
Antwort 404. Dateiabrufe zählen zu den 120 Anfragen pro Minute des Schlüssels.

## Wenn etwas schiefgeht

| Was ihr bekommt | Was es heißt |
|-----------------|--------------|
| HTTP 401 | Der Schlüssel fehlt, ist falsch oder gelöscht, oder die API ist für den Workspace ausgeschaltet. |
| HTTP 429 | Mehr als 120 Anfragen in einer Minute mit diesem Schlüssel. Wartet und versucht es erneut. |
| HTTP 200 mit `errors` ganz oben in der Antwort | Die Abfrage passt nicht zum Schema, oder die Person hinter dem Schlüssel darf das nicht. |
| HTTP 200 mit `errors` in der Antwort der Mutation | Der Datensatz wurde nicht gespeichert. `errors` nennt jedes Feld mit seinen Meldungen, wie das Formular in der App sie zeigen würde. |

Eine Mutation, die geklappt hat, antwortet mit `errors: null`. Fragt `errors` immer mit ab und
prüft es.

## Was ein Schlüssel nicht kann

Workspace-Einstellungen, Nutzer und ihre Rollen, Verbindungen zu Jira und KI-Apps sowie
API-Schlüssel und Webhooks selbst ändert ihr nur in der App. Diese Mutationen sagen das im Schema.

Verwandt: [API und Webhooks](https://docket.rocks/doku/api-und-webhooks) · [Benutzerrollen](https://docket.rocks/doku/benutzerrollen)