# API für Agenten

Anmeldung, Grenzen, Idempotenz, Fehler und fertige Beispiele für die öffentliche API.

## Hinweise für KI-Agenten

- API-Basisadresse: `https://api.eventwerk.eu/api/v1`
- Einstieg für Agenten: `/docs/api-fuer-agenten` mit Erklärungen und Beispielen in curl, JavaScript und Python.
- OpenAPI 3.1: `https://api.eventwerk.eu/openapi.json` und `/api/v1/openapi.json`. Daraus lassen sich Clients und Typprüfungen generieren.
- Anmeldung: `Authorization: Bearer ew_...`
- Schlüssel: Einstellungen unter `/dashboard/einstellungen`, nur mit Eventwerk Plus und nur für Eigentümer. Der volle Schlüssel wird nur einmal angezeigt. Name und Zeitpunkt der letzten Nutzung werden geführt und ausgegeben.
- Zeiten: Alle Zeitpunkte kommen in UTC mit Suffix `Z`. Die Zeitzone des Events steht getrennt in `timezone` als IANA-Name, Standard `Europe/Vienna`. Clients rechnen selbst um.
- Paginierung: Die Event- und Anmeldelisten liefern `{items, has_more, next_cursor}` mit `limit` (Standard 50, höchstens 200), `cursor` und `updated_since`.
- Grenzen: 200 Anfragen pro Minute je Schlüssel. Der Zähler liegt im Speicher je Instanz, ist hinter mehreren Instanzen ein Vielfaches und beginnt nach einem Deploy neu. Bei Überschreitung antwortet die API mit 429, `rate_limited` und `Retry-After`.
- Gastgrenzen: Anmelde-Link 20 je Stunde je IP-Adresse, Anmeldung 60 je Stunde je IP-Adresse und 3 je Stunde je E-Mail-Adresse und Event.
- Fehler: JSON mit `code` und `message`, optional mit `details`.

Die öffentliche API liegt unter `/api/v1` und richtet sich an Agenten, Skripte und eigene Anwendungen. Diese Seite fasst den Einstieg zusammen und zeigt drei vollständige Beispiele. Die verbindliche Beschreibung steht als OpenAPI 3.1 unter [https://api.eventwerk.eu/openapi.json](https://api.eventwerk.eu/openapi.json) und unter `/api/v1/openapi.json`.

## Anmeldung

Jede Anfrage trägt den API-Schlüssel im Kopf `Authorization`. Das Format ist `Bearer ew_...`.

```bash
Authorization: Bearer ew_...
```

Ein Schlüssel gehört zu genau einer Organisation und handelt mit den Rechten des Eigentümers. Als Eigentümer legen Sie ihn in den Einstellungen unter `/dashboard/einstellungen` an. Der volle Schlüssel wird nur einmal angezeigt.

- API-Zugang gibt es nur mit Eventwerk Plus.
- Je Organisation sind höchstens 25 aktive Schlüssel erlaubt.
- Höchstens 10 neue Schlüssel pro Tag, gezählt in einem rollierenden Zeitfenster von 24 Stunden.
- Ein zurückgezogener Schlüssel wird sofort abgelehnt und ergibt `invalid_api_key`.

> **Warnung:** Der Schlüssel ist ein Geheimnis. Speichern Sie ihn nicht in Repositorys, Protokollen oder öffentlichen Konfigurationen.

## Grenzen

Je Schlüssel sind 200 Anfragen pro Minute erlaubt. Der Zähler liegt im Arbeitsspeicher jeder Instanz und wird nicht über Instanzen geteilt. Bei Überschreitung antwortet die API mit Status 429, dem Code `rate_limited` und dem Kopf `Retry-After` in Sekunden. Warten Sie die genannte Zeit ab, bevor Sie es erneut versuchen.

## Schreibende Aufrufe wiederholen

`POST /events` akzeptiert den optionalen Kopf `Idempotency-Key`. Damit lässt sich eine Anfrage nach einer Zeitüberschreitung wiederholen, ohne ein zweites Event anzulegen. Der Kopf darf höchstens 255 Zeichen lang sein.

- Gleicher Schlüssel und gleicher Rumpf: Die API liefert dieselbe Antwort erneut.
- Gleicher Schlüssel und anderer Rumpf: Status 409 und `idempotency_conflict`.
- Ohne Kopf legt jeder Aufruf ein neues Event an.
- Verwenden Sie je beabsichtigter Anlage einen eigenen, zufälligen Schlüssel.

## Fehlerformat

Fehler kommen immer als JSON mit `code` und `message`. `code` ist stabil und für Programme gedacht. `message` ist eine deutsche Meldung für Menschen. Das optionale Feld `details` enthält zusätzliche Angaben.

```json
{
  "code": "idempotency_conflict",
  "message": "Der Idempotency-Key wurde bereits mit einem anderen Rumpf verwendet."
}
```

| Code | Status | Bedeutung |
| --- | --- | --- |
| invalid_json | 400 | Der Rumpf ist kein gültiges JSON. |
| unknown_field | 400 | Ein Feld im Rumpf ist unbekannt. `details.field` nennt den Namen. |
| invalid_filter | 400 | Ein Filterwert ist unbekannt. |
| invalid_limit | 400 | Ein `limit` ist ungültig, etwa keine ganze Zahl oder kleiner als 1. |
| invalid_api_key | 401 | Der Schlüssel fehlt, ist unbekannt oder wurde zurückgezogen. |
| plan_required | 403 | Die Aktion erfordert Eventwerk Plus. |
| event_not_found | 404 | Das Event wurde nicht gefunden. |
| registration_not_found | 404 | Die Anmeldung wurde nicht gefunden. |
| checkin_not_found | 404 | Der QR-Token ist unbekannt. |
| idempotency_conflict | 409 | Derselbe Idempotency-Key wurde mit einem anderen Rumpf verwendet. |
| event_has_registrations | 409 | Ein veröffentlichtes Event mit Anmeldungen lässt sich nicht löschen. |
| registration_not_confirmed, registration_not_checkinable | 409 | Die Anmeldung lässt sich nicht einchecken. |
| rate_limited | 429 | Mehr als 200 Anfragen pro Minute je Schlüssel und Instanz. |
| internal_error | 500 | Interner Fehler. |

> **Hinweis:** Prüfen Sie `code` und den HTTP-Status, nicht den deutschen Text von `message`.

## Webhooks

Webhooks gehören zu Eventwerk Plus. Eventwerk sendet bei einer neuen Benachrichtigung einen POST mit JSON-Rumpf an die hinterlegte URL. Die Verwaltung läuft über die Sitzungs-API unter `/api/webhooks`, nicht über den API-Schlüssel. Die vollständige Beschreibung steht unter [API: Webhooks](/docs/api-webhooks).

- Signatur: Der Kopf `X-Eventwerk-Signature` enthält `sha256=<HMAC-SHA256>` über den unveränderten Rumpf, berechnet mit dem Webhook-Geheimnis. Vergleichen Sie die Signatur in konstanter Zeit.
- Wiederholungen: Jeder Webhook erhält höchstens zwei Versuche im Abstand von 500 Millisekunden. Ein Versuch hat ein Zeitlimit von 5 Sekunden. Eine Antwort mit Status 2xx gilt als erfolgreich.
- Reihenfolge: Die Zustellungen sind nicht in einer festen Reihenfolge garantiert. Es gibt keine dauerhafte Warteschlange; nach einem Neustart gehen nicht zugestellte Aufrufe verloren.

> **Warnung:** Fehler bei der Zustellung werden nur protokolliert. Der auslösende Vorgang, etwa eine Anmeldung, bleibt gültig.

## Verweise

- OpenAPI 3.1: [https://api.eventwerk.eu/openapi.json](https://api.eventwerk.eu/openapi.json) und `/api/v1/openapi.json`.
- Kurze Fassung für Agenten: [llms.txt](/llms.txt).
- Alle Seiten als Markdown: [llms-full.txt](/llms-full.txt).
- API-Überblick im Web: [API-Referenz](/docs/api).

## Beispiele

Die Beispiele lesen den Schlüssel aus `EVENTWERK_API_KEY`. `EVENT_ID` ist die Kennung des Events, `QR_TOKEN` der Token aus dem Ticket. Die Basisadresse ist `https://api.eventwerk.eu/api/v1`. Alle Zeitpunkte in Antworten sind UTC und enden auf `Z`; die Zeitzone des Events steht getrennt in `timezone`.

> **Hinweis:** Der QR-Token steht nicht in der Anmeldeliste. Er steckt im Ticket des Gasts unter `/ticket/<token>`.

### Events auflisten

`GET /events` liefert eine Seite mit `items`, `has_more` und `next_cursor`. Jeder Eintrag enthält unter anderem `id`, `title`, `start_at`, `timezone`, `status` und `registration_count`. `limit` hat den Standard 50. Werte über 200 werden auf 200 begrenzt.

```bash
curl -s "https://api.eventwerk.eu/api/v1/events?limit=50" \
  -H "Authorization: Bearer $EVENTWERK_API_KEY"
```

```javascript
const apiBase = "https://api.eventwerk.eu/api/v1";
const apiKey = process.env.EVENTWERK_API_KEY;

async function main() {
  const response = await fetch(apiBase + "/events?limit=50", {
    headers: { Authorization: "Bearer " + apiKey },
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.code + ": " + error.message);
  }

  const page = await response.json();

  for (const event of page.items) {
    console.log(event.id, event.title, event.start_at);
  }
}

main();
```

```python
import os

import requests

api_base = "https://api.eventwerk.eu/api/v1"
api_key = os.environ["EVENTWERK_API_KEY"]

response = requests.get(
    f"{api_base}/events",
    params={"limit": 50},
    headers={"Authorization": f"Bearer {api_key}"},
    timeout=30,
)
response.raise_for_status()
page = response.json()

for event in page["items"]:
    print(event["id"], event["title"], event["start_at"])
```

Die Antwort hat die Form `{ "items": [...], "has_more": false, "next_cursor": null }`. Ist `has_more` false, ist `next_cursor` null.

### Anmeldungen eines Events lesen

`GET /events/{eventID}/registrations` liefert eine Seite mit `items`, `has_more` und `next_cursor`. Jeder Eintrag enthält unter anderem `id`, `name`, `email`, `status`, `ticket_type_id`, `price_paid_cents`, `checked_in_at` und `created_at`.

```bash
curl -s "https://api.eventwerk.eu/api/v1/events/$EVENT_ID/registrations?status=confirmed&limit=100" \
  -H "Authorization: Bearer $EVENTWERK_API_KEY"
```

```javascript
const apiBase = "https://api.eventwerk.eu/api/v1";
const apiKey = process.env.EVENTWERK_API_KEY;
const eventId = process.env.EVENT_ID;

async function main() {
  const url =
    apiBase +
    "/events/" +
    encodeURIComponent(eventId) +
    "/registrations?status=confirmed&limit=100";

  const response = await fetch(url, {
    headers: { Authorization: "Bearer " + apiKey },
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.code + ": " + error.message);
  }

  const page = await response.json();

  for (const registration of page.items) {
    console.log(
      registration.id,
      registration.name,
      registration.status,
      registration.checked_in_at,
    );
  }
}

main();
```

```python
import os

import requests

api_base = "https://api.eventwerk.eu/api/v1"
api_key = os.environ["EVENTWERK_API_KEY"]
event_id = os.environ["EVENT_ID"]

response = requests.get(
    f"{api_base}/events/{event_id}/registrations",
    params={"status": "confirmed", "limit": 100},
    headers={"Authorization": f"Bearer {api_key}"},
    timeout=30,
)
response.raise_for_status()
page = response.json()

for registration in page["items"]:
    print(
        registration["id"],
        registration["name"],
        registration["status"],
        registration["checked_in_at"],
    )
```

`status` filtert nach `pending`, `confirmed`, `waitlisted`, `cancelled` oder `declined`. `q` sucht in Name und E-Mail-Adresse. `limit` hat den Standard 50. Werte über 200 werden auf 200 begrenzt.

### Check-in setzen

`POST /events/{eventID}/checkin` setzt den Check-in einmalig. Der Rumpf enthält `qr_token`.

```bash
curl -s -X POST "https://api.eventwerk.eu/api/v1/events/$EVENT_ID/checkin" \
  -H "Authorization: Bearer $EVENTWERK_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"qr_token\":\"$QR_TOKEN\"}"
```

```javascript
const apiBase = "https://api.eventwerk.eu/api/v1";
const apiKey = process.env.EVENTWERK_API_KEY;
const eventId = process.env.EVENT_ID;
const qrToken = process.env.QR_TOKEN;

async function main() {
  const response = await fetch(
    apiBase + "/events/" + encodeURIComponent(eventId) + "/checkin",
    {
      method: "POST",
      headers: {
        Authorization: "Bearer " + apiKey,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ qr_token: qrToken }),
    },
  );

  const result = await response.json();

  if (!response.ok) {
    throw new Error(result.code + ": " + result.message);
  }

  console.log(result.status, result.already_checked_in, result.registration.id);
}

main();
```

```python
import os

import requests

api_base = "https://api.eventwerk.eu/api/v1"
api_key = os.environ["EVENTWERK_API_KEY"]
event_id = os.environ["EVENT_ID"]
qr_token = os.environ["QR_TOKEN"]

response = requests.post(
    f"{api_base}/events/{event_id}/checkin",
    headers={"Authorization": f"Bearer {api_key}"},
    json={"qr_token": qr_token},
    timeout=30,
)
response.raise_for_status()
result = response.json()

print(result["status"], result["already_checked_in"], result["registration"]["id"])
```

Die Antwort enthält `status`, `already_checked_in` und `registration`. Der erste Aufruf liefert `checked_in` und `already_checked_in: false`. Ein erneuter Aufruf mit demselben Token liefert `already_checked_in: true` und verändert den Zeitpunkt nicht. Ein unbekannter Token ergibt 404 `checkin_not_found`. Eine Anmeldung, die nicht eingecheckt werden kann, ergibt 409 mit `registration_not_confirmed` oder `registration_not_checkinable`.

> **Hinweis:** Der Check-in ist von sich aus wiederholbar. Ein zweiter Aufruf mit demselben Token legt keinen zweiten Check-in an, sondern meldet `already_checked_in`.
