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

# Introduction

> Read the offers of one HocDoc workspace and their timelines over a JSON API.

<Note>
  Version 1 is not released yet. The endpoints described here become available with the release.
</Note>

## What the API offers

The HocDoc API gives your own systems read access to the offers of one workspace. Use it to show the state of your offers in a CRM, a report or an automation tool.

The API is read-only. It cannot create, change or delete anything.

These resources exist:

| Resource | Path | Content |
| - | - | - |
| Workspace | `GET /workspace` | The workspace the API key belongs to. Use it to check that a key works. |
| Offers | `GET /offers` | A paginated list of offers, newest first. Filter by `status`. |
| Offer | `GET /offers/{id}` | One offer. |
| Offer events | `GET /offers/{id}/events` | The timeline of one offer, oldest event first. |

See the endpoint reference in the sidebar for every parameter and response.

## Base URL and versioning

All paths are relative to this base URL:

```text theme={null}
https://app.hocdoc.com/api/v1
```

The app shows the base URL too: open **Einstellungen**, then the **Integrationen** tab. The card **Öffentliche API** lists it under **Basis-Adresse** and links to the OpenAPI description (**OpenAPI-Beschreibung**).

The version is part of the path. New fields, new endpoints and new event types can be added to `/api/v1` at any time. Ignore fields and event types you do not know. Field names and their meaning do not change under `/api/v1`. A breaking change would be published under `/api/v2`.

The OpenAPI description (version 3.1) is available at `https://app.hocdoc.com/api/v1/openapi.json`. It needs no API key.

## Request and response format

* Send `GET` requests. Every response body is JSON (`application/json; charset=utf-8`). See [Errors](/api/errors) for `HEAD`, `OPTIONS` and all other methods.
* Field names are English and in `snake_case`.
* A single resource comes back as `{ "data": { ... } }`.
* A list comes back as `{ "data": [ ... ], "next_cursor": "..." }`. On the last page `next_cursor` is `null`. See [Pagination](/api/pagination).
* IDs are UUIDs.
* Timestamps are ISO 8601 in UTC with milliseconds, for example `2026-10-06T08:15:30.123Z`. Values outside the years 0001 to 9999 are reported as the nearest edge of that range.
* Money amounts are integers in the smallest currency unit, for example cents. Currencies are ISO 4217 codes.
* Responses of the endpoints carry `Cache-Control: no-store`. The OpenAPI description is served with `Cache-Control: public, max-age=300`.
* The API sets no CORS headers. Call it from a server, not from a browser.

Errors have one shape. See [Errors](/api/errors).

## Quickstart

<Steps>
  <Step title="Create an API key">
    Open **Einstellungen**, then the **Integrationen** tab. Select **API-Key erstellen**. Enter a **Name** (for example the name of the system that will use the key). Optionally set **Gültig bis (optional)**. Select **Erstellen**.

    The dialog **API-Key erstellt** shows the key once. Select **Kopieren** and store it safely, then select **Fertig**. Only owners and admins can create keys. See [Authentication](/api/authentication).
  </Step>

  <Step title="Store the key in your environment">
    ```bash theme={null}
    export HOCDOC_API_KEY="hd_live_..."
    ```
  </Step>

  <Step title="Send your first request">
    ```bash theme={null}
    curl "https://app.hocdoc.com/api/v1/offers?status=sent&limit=2" \
      -H "Authorization: Bearer $HOCDOC_API_KEY"
    ```
  </Step>
</Steps>

The response looks like this:

```json theme={null}
{
  "data": [
    {
      "id": "0b0e7c1e-2f6a-4c3b-9d1e-5a4b3c2d1e0f",
      "title": "Website-Relaunch",
      "status": "sent",
      "created_at": "2026-10-06T08:15:30.123Z",
      "updated_at": "2026-10-06T08:20:00.000Z",
      "sent_at": "2026-10-06T08:20:00.000Z",
      "expires_at": "2026-10-20T21:59:59.999Z",
      "accepted_at": null,
      "expired_at": null,
      "version": 1,
      "recipient": {
        "name": "Vera Kundin",
        "company": "Kunde GmbH",
        "email": "vera@kunde.example"
      },
      "accepted_by": null,
      "value": { "amount": 480000, "currency": "EUR" }
    }
  ],
  "next_cursor": null
}
```

To check only that a key works, call the workspace endpoint:

```bash theme={null}
curl "https://app.hocdoc.com/api/v1/workspace" \
  -H "Authorization: Bearer $HOCDOC_API_KEY"
```

```json theme={null}
{
  "data": {
    "id": "9f1c0a52-6d0e-4a6f-8f43-2f1d7f6a9b10",
    "name": "Studio Nord",
    "timezone": "Europe/Berlin",
    "currency": "EUR"
  }
}
```

## Resources

### Workspace

<ResponseField name="id" type="string (uuid)">The workspace ID.</ResponseField>
<ResponseField name="name" type="string">The workspace name.</ResponseField>
<ResponseField name="timezone" type="string">IANA time zone, for example `Europe/Berlin`.</ResponseField>
<ResponseField name="currency" type="string">Default ISO 4217 currency code, for example `EUR`.</ResponseField>

### Offer

<ResponseField name="id" type="string (uuid)">The offer ID.</ResponseField>
<ResponseField name="title" type="string">The offer title.</ResponseField>
<ResponseField name="status" type="string">One of `draft`, `sent`, `accepted`, `declined`, `expired`, `withdrawn`. A sent offer whose `expires_at` has passed is reported as `expired`.</ResponseField>
<ResponseField name="created_at" type="string (timestamp)">When the offer was created.</ResponseField>
<ResponseField name="updated_at" type="string (timestamp)">When the offer was last updated.</ResponseField>
<ResponseField name="sent_at" type="string (timestamp) or null">When the offer was last sent. `null` for a draft.</ResponseField>
<ResponseField name="expires_at" type="string (timestamp) or null">Deadline for acceptance. `null` for a draft.</ResponseField>
<ResponseField name="accepted_at" type="string (timestamp) or null">When the offer was accepted. `null` until the offer is accepted.</ResponseField>
<ResponseField name="expired_at" type="string (timestamp) or null">When the offer expired. Only set while `status` is `expired`, otherwise `null`.</ResponseField>
<ResponseField name="version" type="integer or null">Number of the version that was last sent, starting at 1. `null` for a draft.</ResponseField>
<ResponseField name="recipient" type="object">The person the offer was sent to, with `name`, `company` and `email`. Each can be `null`. All three are `null` for a draft.</ResponseField>
<ResponseField name="accepted_by" type="object or null">The person who accepted the offer, with `name` and `email`. `null` until the offer is accepted.</ResponseField>

<ResponseField name="value" type="object">
  `amount` (integer, smallest currency unit) and `currency`. The value of the recommended package of the first visible pricing section, otherwise of the first package, otherwise 0.
</ResponseField>

### Offer event

<ResponseField name="sequence" type="integer">Position of the event in the timeline of its offer, starting at 1. Stable.</ResponseField>
<ResponseField name="type" type="string">Event type. Known values: `created`, `edited`, `sent`, `viewed`, `section_viewed`, `calculator_used`, `summary_viewed`, `code_requested`, `code_failed`, `code_verified`, `accepted`, `declined`, `expired`, `withdrawn`. New types may be added.</ResponseField>
<ResponseField name="occurred_at" type="string (timestamp)">When the event happened.</ResponseField>
<ResponseField name="version" type="integer or null">Number of the offer version the event refers to, if any.</ResponseField>
<ResponseField name="data" type="object">Details that depend on `type`. Can be empty.</ResponseField>

The fields in `data`. Every field is optional, so check that a field is present before you read it:

| Event type | Fields |
| - | - |
| `sent` | `recipient_email` |
| `section_viewed` | `section_id`, `duration_ms` |
| `expired` | `expires_at`: the deadline that passed, truncated to whole seconds. It can differ from `expires_at` of the offer in the milliseconds, so do not compare the two values as keys. |
| `code_requested`, `code_failed`, `code_verified`, `accepted` | `signer_email` |
| All other types | None |

Example response of `GET /offers/{id}/events`:

```json theme={null}
{
  "data": [
    {
      "sequence": 1,
      "type": "sent",
      "occurred_at": "2026-10-06T08:20:00.000Z",
      "version": 1,
      "data": { "recipient_email": "vera@kunde.example" }
    },
    {
      "sequence": 2,
      "type": "viewed",
      "occurred_at": "2026-10-06T09:02:11.480Z",
      "version": 1,
      "data": {}
    },
    {
      "sequence": 3,
      "type": "section_viewed",
      "occurred_at": "2026-10-06T09:02:40.112Z",
      "version": 1,
      "data": { "section_id": "pricing-1", "duration_ms": 4200 }
    }
  ],
  "next_cursor": null
}
```

## What the API never returns

* The link to the offer for the recipient.
* The content of an offer: sections, texts and prices in detail.
* IP addresses and browser identifiers of recipients.
* Contacts, templates, PDFs and certificates.
* Anything from another workspace.

Writing is not possible. A `POST`, `PUT`, `PATCH` or `DELETE` request to one of the four endpoints returns `405`. See [Errors](/api/errors).

## Next steps

* [Authentication](/api/authentication): create, send and revoke keys.
* [Pagination](/api/pagination): walk through long lists.
* [Errors](/api/errors): error format and codes.
* [Rate limits](/api/rate-limits): limits and response headers.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.