Jeder Suchlauf läuft nun in einem frischen Container pro Benutzer, dessen pro-Benutzer-Home als ~/.claude gemountet ist — Memories und Session-Contexte liegen damit strikt getrennt pro Benutzer. Die Such-Skills sind Shared-Code aus dem Image und werden im Container nur nach ~/.claude/skills verlinkt. Der Host-Runner startet pro Lauf `docker run --rm` und führt bis zu JOBSUCHE_MAX_PARALLEL (Default 4) Läufe parallel über verschiedene Benutzer aus (jeder hat eigenen Ollama-Key = getrennte Rate-Limits). Der alte gemeinsame ~/.claude-Pfad wird vom Runner nicht mehr beschrieben. - source/agent/: neues Agent-Image (Dockerfile + entrypoint + drei Skills) - scripts/jobsuche-runner.js: agentStarten als docker run, ensureAgentDir, runPool - scripts/jobsuche-runner.sh + bin/-Kopie: Image-Guard - package.json: docker:build-agent (lokal, ohne Registry-Push) Co-Authored-By: Claude <noreply@anthropic.com>
332 lines
18 KiB
Markdown
332 lines
18 KiB
Markdown
---
|
||
name: bewerbungs-tracker
|
||
description: Read and manage job applications ("Bewerbungen") via the local Bewerbungs-Tracker REST-API. Use whenever the user wants to list, create, update or delete Bewerbungen, manage their Statusverlauf/Timeline, download generated Unterlagen (PDF attachments), read E-Mail correspondence, trigger or check KI document generation, read/write Settings, get Statistics or an Export, browse Templates (Vorlagen), trigger document generation with optional static attachments (Anlagen), or manage JobOffers (Jobangebote). Trigger phrases: "Bewerbung(en)", "Bewerbungs-Tracker", "Statusverlauf", "Jobangebot(e)", "Bewerbungsstatistik", "Anschreiben generieren", "Anlagen beilegen".
|
||
---
|
||
|
||
# Bewerbungs-Tracker REST-API
|
||
|
||
REST-API zum Lesen und Verwalten von Bewerbungen, Statusverlauf, generierten
|
||
Unterlagen (PDFs), E-Mail-Korrespondenz, Einstellungen, Statistiken, Vorlagen
|
||
und Jobangeboten.
|
||
|
||
## Konfiguration
|
||
|
||
- **Base-URL:** `http://localhost:4327/api/v1` (override: env `BEWERBUNG_API_URL`)
|
||
- **Auth:** Header `X-API-Key: <token>` bei allen Endpunkten außer `/health`.
|
||
- **Key-Quelle:** env `BEWERBUNG_API_KEY`, sonst der Token des Benutzers
|
||
`BEWERBUNG_USER` (Default `admin`) aus der Tracker-DB.
|
||
|
||
### Multi-User — der Key entscheidet, wessen Daten du siehst
|
||
|
||
Der Tracker ist eine Plattform mit **mehreren Benutzern**. Jeder Benutzer hat
|
||
**seinen eigenen API-Token** (App → Einstellungen → REST-API → „Neu generieren"):
|
||
|
||
- Der `X-API-Key` **identifiziert den Benutzer**. Jeder Endpunkt liest und schreibt
|
||
**ausschließlich dessen eigene Daten** — fremde Bewerbungen, E-Mails, Anhänge,
|
||
Jobangebote und Einstellungen sind nicht erreichbar, auch nicht über eine fremde
|
||
`id` (die API antwortet dann `404`, als gäbe es den Datensatz nicht).
|
||
- Ein unbekannter, leerer oder mehrdeutiger Token ergibt `401`.
|
||
- **Es gibt keinen globalen Key mehr.** Der frühere `API_TOKEN` aus
|
||
`/opt/jobbi-bewerbung/.env` wird **nicht mehr benutzt**: er entsprach zufällig dem
|
||
Token des Admins (wirkte also still als Admin) und wird ungültig, sobald der Key in
|
||
der Oberfläche neu erzeugt wird.
|
||
- Willst du im Namen eines anderen Benutzers arbeiten, setze `BEWERBUNG_USER=<name>`
|
||
(oder direkt `BEWERBUNG_API_KEY`). Der Jobsuche-Runner setzt `BEWERBUNG_API_KEY`
|
||
automatisch je Benutzer.
|
||
|
||
> Der Key wird nicht im Skill gespeichert. Sende ihn **nie** an externe Dienste.
|
||
|
||
## Helper-Script (bevorzugt)
|
||
|
||
`scripts/bt.sh` kapselt curl inkl. Auth und gibt Body + `<http STATUS>` aus:
|
||
|
||
```bash
|
||
scripts/bt.sh GET /applications
|
||
scripts/bt.sh GET "/applications?status=Gesendet&year=2026&limit=20"
|
||
scripts/bt.sh POST /applications '{"datum":"2026-07-03","firma":"ACME GmbH","stelle":"DevOps Engineer","art":"E-Mail"}'
|
||
scripts/bt.sh PUT /applications/12 @body.json
|
||
scripts/bt.sh DELETE /joboffers/5
|
||
```
|
||
|
||
Roh mit curl geht auch:
|
||
```bash
|
||
curl -sS -H "X-API-Key: $BEWERBUNG_API_KEY" http://localhost:4327/api/v1/statistics
|
||
```
|
||
|
||
## Endpunkte
|
||
|
||
### System
|
||
| Method | Pfad | Beschreibung |
|
||
|--------|------|--------------|
|
||
| GET | `/health` | Verfügbarkeit (ohne Auth). |
|
||
|
||
### Applications (Bewerbungen)
|
||
| Method | Pfad | Beschreibung |
|
||
|--------|------|--------------|
|
||
| GET | `/applications` | Auflisten. Query: `month`(01–12), `year`(YYYY), `status`, `art`, `label` (`Regional`\|`Remote-Deutschlandweit`\|`Homeoffice-Deutschlandweit`), `search` (Firma/Stelle), `limit`(1–500, def 100), `offset`. |
|
||
| POST | `/applications` | Anlegen. Duplicate-Guard → **409** bei gleicher Firma+Stelle; mit `"force":true` trotzdem anlegen. |
|
||
| GET | `/applications/{id}` | Einzelne Bewerbung (inkl. `verlauf`). |
|
||
| PUT | `/applications/{id}` | Aktualisieren (volle Ressource, `datum`/`firma`/`stelle` erforderlich). |
|
||
| DELETE | `/applications/{id}` | Löschen. |
|
||
| GET | `/applications/{id}/timeline` | Statusverlauf abrufen. |
|
||
| POST | `/applications/{id}/timeline` | Statusverlauf-Eintrag hinzufügen. |
|
||
| DELETE | `/applications/{id}/timeline/{eintragId}` | Verlaufseintrag löschen. |
|
||
|
||
### Attachments (generierte PDFs)
|
||
| Method | Pfad | Beschreibung |
|
||
|--------|------|--------------|
|
||
| GET | `/applications/{id}/attachments` | Anhänge auflisten. |
|
||
| GET | `/applications/{id}/attachments/{attachmentId}` | Anhang herunterladen (`application/octet-stream` → mit `-o datei` speichern). |
|
||
|
||
### Emails
|
||
| Method | Pfad | Beschreibung |
|
||
|--------|------|--------------|
|
||
| GET | `/applications/{id}/emails` | E-Mail-Korrespondenz (in/out) inkl. Anhang-Metadaten. |
|
||
| GET | `/emails/{emailId}/attachments/{attachmentId}` | E-Mail-Anhang herunterladen (binär). |
|
||
|
||
### Generation (KI-Unterlagen)
|
||
| Method | Pfad | Beschreibung |
|
||
|--------|------|--------------|
|
||
| POST | `/applications/{id}/generate` | Generierung anstoßen. Body optional: `{"llm_notizen":"...","anlagen":[2]}`. `anlagen` = IDs statischer Basis-Anlagen (siehe „Anlagen bei der Generierung"). |
|
||
| GET | `/applications/{id}/generation-status` | Status: `nicht_gestartet`\|`ausstehend`\|`fertig`\|`fehler` (+ `anhaenge`). |
|
||
|
||
### Settings / Statistics / Export / Templates
|
||
| Method | Pfad | Beschreibung |
|
||
|--------|------|--------------|
|
||
| GET | `/settings` | Persönliche Angaben des eigenen Benutzers: `name`, `adresse`, `kundennummer`, `email`, `telefon`, `ort`, `webseite`, `geburtsdatum`. Noch nichts gespeichert → `{}` (gültiger Zustand). |
|
||
| PUT | `/settings` | Persönliche Angaben speichern. Es werden **nur die Felder geändert, die im Body stehen** — weggelassene behalten ihren Wert. Antwort enthält den neuen Stand (`{success, settings}`). |
|
||
| GET | `/statistics` | `total`, `byArt[]`, `byStatus[]` — nur eigene Bewerbungen. |
|
||
| GET | `/export` | Export (ohne `interne_notizen`). Query: `month`, `year`. |
|
||
| GET | `/templates` | Basis-Unterlagen / Vorlagen des Benutzers. Enthält den **Lebenslauf** — daraus leitet die Stellensuche Rolle und Skills ab. |
|
||
|
||
### JobOffers (Jobangebote)
|
||
| Method | Pfad | Beschreibung |
|
||
|--------|------|--------------|
|
||
| GET | `/joboffers` | Auflisten (neueste zuerst). |
|
||
| POST | `/joboffers` | Upsert (per `(quelle, external_id)`, sonst normalisierte `quelle_url`) → Response `action: created\|updated`. **409** wenn geblacklistet (siehe unten). |
|
||
| GET | `/joboffers/{id}` | Einzelnes Jobangebot. |
|
||
| DELETE | `/joboffers/{id}` | Löschen. **Setzt das Angebot standardmäßig zuvor auf die Blacklist** (Typ `auto`), damit dieselbe Stelle nie erneut eingespielt wird. Mit `?blacklist=false` hart löschen ohne Blockierung. Response `{success, blacklisted}`. |
|
||
|
||
### Blacklist (blockierte Jobangebote)
|
||
| Method | Pfad | Beschreibung |
|
||
|--------|------|--------------|
|
||
| GET | `/joboffers/blacklist` | Blacklist-Einträge auflisten (neueste zuerst). |
|
||
| POST | `/joboffers/blacklist` | Block anlegen (per URL, Domain, Firma oder Firma+Stelle) → **201** `{success, entry}`. |
|
||
| DELETE | `/joboffers/blacklist/{id}` | Block entfernen — betroffene Angebote sind danach wieder einspielbar. |
|
||
|
||
## JobOffers einspielen (Drittanbieter → Upsert)
|
||
|
||
`POST /joboffers` ist der Einspiel-Endpunkt für Drittanbietersoftware (CRM,
|
||
Scraper, Job-Board-Sync). Er **upsertet** anhand `(quelle, external_id)`, und
|
||
falls keine `external_id` passt, anhand der **normalisierten `quelle_url`**:
|
||
|
||
- Treffer vorhanden → Datensatz wird aktualisiert → Response
|
||
`"action":"updated"`, HTTP **200**.
|
||
- kein Treffer → neuer Datensatz → `"action":"created"`, HTTP **201**.
|
||
- Angebot steht auf der **Blacklist** → HTTP **409**, `BlacklistConflict`-Body,
|
||
Angebot wird **nicht** aufgenommen (siehe Abschnitt „Blacklist“).
|
||
|
||
> Damit ist der Import **idempotent**: dieselbe Quelle kann beliebig oft
|
||
> eingespielt werden, ohne Dubletten zu erzeugen — Voraussetzung ist eine
|
||
> stabile `external_id` pro Quelle.
|
||
|
||
> ⚠️ **Upsert ersetzt, er merged nicht.** Beim Update wird der Datensatz mit dem
|
||
> gesendeten Body überschrieben; weggelassene optionale Felder werden **geleert**.
|
||
> Beim Re-Import daher immer den **vollständigen** Datensatz senden, nicht nur die
|
||
> geänderten Felder.
|
||
|
||
### Felder
|
||
|
||
| Feld | Pflicht | Beschreibung |
|
||
|------|:---:|--------------|
|
||
| `firma` | ✅ | Arbeitgeber. |
|
||
| `stelle` | ✅ | Stellenbezeichnung. |
|
||
| `firma_slug` | ✅ | **Pflicht.** Firmenname als `lowercase` + `kebab-case` (z. B. `Acme GmbH` → `acme-gmbh`). Dient der eindeutigen Identifizierung beim Blacklist-Abgleich. |
|
||
| `external_id` | – | ID des Angebots in der Quellsoftware. **Upsert-Schlüssel** — für idempotenten Import immer setzen. |
|
||
| `quelle` | – | Herkunftssystem/Kanal (z. B. `mein-crm`). Default serverseitig `drittanbieter`. |
|
||
| `ort` | – | Arbeitsort. |
|
||
| `adresse` | – | Anschrift des Arbeitgebers (Straße, Hausnummer, PLZ, Ort). |
|
||
| `ansprechpartner` | – | Name des Ansprechpartners für die Bewerbung. |
|
||
| `gehalt` | – | Freitext (z. B. `55.000 €`). |
|
||
| `beschreibung` | – | Volltext der Stellenanzeige. |
|
||
| `quelle_url` | – | Deeplink zum Original-Inserat. |
|
||
| `art` | – | Bewerbungsart-Enum (siehe unten). |
|
||
| `status` | – | `offen` \| `uebernommen` \| `abgelehnt`. Default `offen`. |
|
||
| `anzeige_datum` | – | Veröffentlichungsdatum des Inserats (`YYYY-MM-DD`). Wird gespeichert. |
|
||
| `kontakt_email` | – | Kontakt-/Bewerbungs-E-Mail-Adresse der Stelle. |
|
||
| `labels` | – | Array von Arbeitsort-Labels (Enum, mehrere möglich): `Regional`, `Remote-Deutschlandweit`, `Homeoffice-Deutschlandweit`. Kennzeichnet den Arbeitsort-Typ der Stelle. |
|
||
| ~~`created_at`~~ | ⛔ | **Nicht mitsenden** — Einspiel-Zeitpunkt wird serverseitig gesetzt; ein mitgesendeter Wert wird ignoriert. |
|
||
|
||
### Beispiel
|
||
|
||
```bash
|
||
scripts/bt.sh POST /joboffers '{
|
||
"external_id": "job-12345",
|
||
"quelle": "mein-crm",
|
||
"firma": "Acme GmbH",
|
||
"stelle": "Softwareentwickler (m/w/d)",
|
||
"firma_slug": "acme-gmbh",
|
||
"ort": "Berlin",
|
||
"adresse": "Musterstraße 1, 10115 Berlin",
|
||
"gehalt": "55.000 €",
|
||
"beschreibung": "…",
|
||
"quelle_url": "https://acme.example/job/12345",
|
||
"art": "Online-Portal",
|
||
"anzeige_datum": "2026-07-01",
|
||
"kontakt_email": "bewerbung@acme.example",
|
||
"labels": ["Regional"]
|
||
}'
|
||
```
|
||
|
||
Roh mit curl:
|
||
```bash
|
||
curl -X POST "$BEWERBUNG_API_URL/joboffers" \
|
||
-H "X-API-Key: $BEWERBUNG_API_KEY" -H "Content-Type: application/json" \
|
||
-d '{"external_id":"job-12345","quelle":"mein-crm","firma":"Acme GmbH","stelle":"Softwareentwickler (m/w/d)","firma_slug":"acme-gmbh","art":"Online-Portal","anzeige_datum":"2026-07-01"}'
|
||
```
|
||
|
||
> JSON erlaubt **keine Kommentare** — die `// created_at …`-Zeile aus Notizen
|
||
> vor dem Absenden entfernen, sonst schlägt das Parsen fehl (HTTP 400).
|
||
|
||
### Batch-Einspielung
|
||
|
||
Kein Bulk-Endpunkt: pro Angebot ein `POST`. Für viele Angebote in einer Schleife
|
||
importieren und `action` je Antwort auswerten:
|
||
|
||
```bash
|
||
# offers.json = JSON-Array von JobOfferCreate-Objekten
|
||
jq -c '.[]' offers.json | while read -r offer; do
|
||
scripts/bt.sh POST /joboffers "$offer" | head -1
|
||
done
|
||
```
|
||
|
||
## Blacklist (Angebote dauerhaft blockieren)
|
||
|
||
Über `/joboffers/blacklist` lassen sich Angebote sperren. Ein geblacklistetes
|
||
Angebot wird beim `POST /joboffers` **nicht** aufgenommen, sondern mit **409**
|
||
und `BlacklistConflict`-Body abgelehnt (`blacklisted:true`, `matched_by`,
|
||
`blacklist_entry`). Der Filter greift auf **künftige** Einspielungen; bereits
|
||
importierte Angebote werden nicht rückwirkend gelöscht.
|
||
|
||
### Block-Typen (`typ`)
|
||
|
||
| `typ` | Body-Felder | Blockiert |
|
||
|-------|-------------|-----------|
|
||
| `url` | `wert` = konkrete URL | genau dieses Inserat (URL wird normalisiert, Tracking-Parameter entfernt). |
|
||
| `domain` | `wert` = Domain (z. B. `stellenbetrug.example`) | alle Angebote dieser Domain. |
|
||
| `firma` | `wert` = Firmenname | alle Angebote dieser Firma. |
|
||
| `firma_stelle` | `firma` + `stelle` (+ optional `ort`) | eine bestimmte Firma-Stelle-Kombination. |
|
||
|
||
`grund` (Freitext) ist überall optional. Firma/Stelle/URL werden serverseitig
|
||
normalisiert, der Vergleich ist also unabhängig von Groß-/Kleinschreibung und
|
||
Tracking-Parametern. GET-Antworten enthalten die normalisierten Vergleichsfelder
|
||
zusätzlich: `url_norm`, `domain`, `firma_norm`, `firma_slug`, `stelle_norm`,
|
||
`ort_norm` (nur lesbar, serverseitig berechnet — nicht mitsenden). (Der Typ `auto`
|
||
in Antworten markiert automatisch erzeugte Einträge — aus einer **Ablehnung** oder
|
||
aus dem **Löschen eines Jobangebots** (`DELETE /joboffers/{id}`); er kann nicht
|
||
direkt angelegt werden.)
|
||
|
||
### Beispiele
|
||
|
||
```bash
|
||
# Ganze Domain sperren
|
||
scripts/bt.sh POST /joboffers/blacklist '{"typ":"domain","wert":"stellenbetrug.example","grund":"Fake-Inserate"}'
|
||
|
||
# Eine Firma sperren
|
||
scripts/bt.sh POST /joboffers/blacklist '{"typ":"firma","wert":"Dubios GmbH"}'
|
||
|
||
# Konkrete Firma+Stelle sperren
|
||
scripts/bt.sh POST /joboffers/blacklist '{"typ":"firma_stelle","firma":"Acme GmbH","stelle":"Vertrieb (m/w/d)","ort":"Berlin"}'
|
||
|
||
# Auflisten und wieder entfernen
|
||
scripts/bt.sh GET /joboffers/blacklist
|
||
scripts/bt.sh DELETE /joboffers/blacklist/3
|
||
```
|
||
|
||
> ⚠️ Vor dem `POST /joboffers` (v. a. Batch-Import) kann ein 409 auftreten, weil
|
||
> das Angebot geblacklistet ist. In Schleifen den Statuscode auswerten und solche
|
||
> Angebote überspringen statt abzubrechen.
|
||
|
||
## Anlagen bei der Generierung (statische Basis-Anlagen)
|
||
|
||
Beim Anstoßen der KI-Generierung (`POST /applications/{id}/generate`) lassen sich
|
||
optional **statische Basis-Anlagen** (`basis_anhaenge`) beilegen — z. B. Zeugnisse,
|
||
Zertifikate. Feld **`anlagen`**: ein Array von **Integer-IDs** dieser Basis-Anlagen.
|
||
|
||
- **Default: keine.** Fehlt `anlagen` oder ist das Array leer, werden **keine**
|
||
statischen Anlagen beigelegt.
|
||
- Die gewählten Anlagen erscheinen zusätzlich **im Anschreiben unter „Anlagen"**.
|
||
- IDs verweisen auf die im Tracker hinterlegten Basis-Anlagen (`basis_anhaenge`).
|
||
Für die aktuell verfügbaren IDs/Namen im Tracker nachsehen (die Basis-Anlagen
|
||
werden dort verwaltet); nur belegte IDs senden, keine raten.
|
||
|
||
```bash
|
||
# Mit einer statischen Anlage (z. B. Abschlusszeugnis = ID 2) generieren
|
||
scripts/bt.sh POST /applications/12/generate '{"anlagen":[2]}'
|
||
|
||
# Mehrere Anlagen + KI-Kontext
|
||
scripts/bt.sh POST /applications/12/generate '{"llm_notizen":"…","anlagen":[2,5]}'
|
||
|
||
# Ohne Anlagen (Default) — Feld einfach weglassen
|
||
scripts/bt.sh POST /applications/12/generate '{}'
|
||
```
|
||
|
||
## Enums
|
||
|
||
- **art:** `E-Mail`, `Online-Portal`, `Indeed`, `StepStone`, `Firmenwebsite`, `Post`, `Initiativbewerbung`, `Arbeitsagentur`, `Sonstiges`
|
||
- **status (Bewerbung/Timeline):** `Entwurf`, `Gesendet`, `Eingangsbestätigung`, `In Bearbeitung`, `Interessiert`, `Warten auf Rückmeldung`, `Warten auf meine Antwort`, `Vorstellungsgespräch`, `Absage`, `Einstellung`, `Keine Rückmeldung`
|
||
- **joboffer.status:** `offen`, `uebernommen`, `abgelehnt`
|
||
- **labels (Arbeitsort, Array, mehrere möglich):** `Regional`, `Remote-Deutschlandweit`, `Homeoffice-Deutschlandweit` — auf `JobOffer` **und** `Application`; als `?label=…`-Filter auf `GET /applications` nutzbar.
|
||
- **blacklist.typ (anlegen):** `url`, `domain`, `firma`, `firma_stelle` (Antworten können zusätzlich `auto` enthalten)
|
||
- **email.direction:** `in`, `out`
|
||
- **generation.status:** `nicht_gestartet`, `ausstehend`, `fertig`, `fehler`
|
||
|
||
## Feld-Referenz (wichtigste)
|
||
|
||
**ApplicationCreate** — erforderlich `datum` (YYYY-MM-DD), `firma`, `stelle`;
|
||
optional `art`, `status`, `notizen`, `interne_notizen`, `ort`,
|
||
`stellenbeschreibung`, `quelle_url`, `llm_notizen`, `kommentar`, `force`,
|
||
`labels` (Array: `Regional`\|`Remote-Deutschlandweit`\|`Homeoffice-Deutschlandweit`;
|
||
auch als `?label=`-Filter beim Auflisten). `ApplicationUpdate` kennt `labels` ebenso.
|
||
|
||
**TimelineEntryCreate** — erforderlich `status`; optional `datum`, `kommentar`.
|
||
|
||
**GenerateRequest** (`POST /applications/{id}/generate`) — alle Felder optional:
|
||
`llm_notizen` (Freitext-Kontext für die KI), `anlagen` (Array von Integer-IDs
|
||
statischer Basis-Anlagen; Default **keine**, siehe Abschnitt „Anlagen bei der
|
||
Generierung“).
|
||
|
||
**JobOfferCreate** — erforderlich `firma`, `stelle`, `firma_slug` (Firmenname als
|
||
`lowercase` + `kebab-case`, z. B. `acme-gmbh` — eindeutige ID für den
|
||
Blacklist-Abgleich); optional `external_id`, `quelle`, `ort`, `adresse`,
|
||
`ansprechpartner`, `gehalt`, `beschreibung`, `quelle_url`, `art`, `status`,
|
||
`anzeige_datum`, `kontakt_email`, `labels` (Array:
|
||
`Regional`\|`Remote-Deutschlandweit`\|`Homeoffice-Deutschlandweit`, mehrere
|
||
möglich) (siehe Abschnitt „JobOffers einspielen“).
|
||
`created_at` wird **serverseitig** gesetzt und darf nicht mitgesendet werden.
|
||
|
||
**BlacklistEntryCreate** — erforderlich `typ` (`url`\|`domain`\|`firma`\|
|
||
`firma_stelle`); bei `url`/`domain`/`firma` das Feld `wert`; bei `firma_stelle`
|
||
die Felder `firma`+`stelle` (optional `ort`); `grund` überall optional (siehe
|
||
Abschnitt „Blacklist“).
|
||
|
||
## Hinweise / Fehlerbehandlung
|
||
|
||
- **401** `{"error":"Ungültiger oder fehlender API-Key ..."}` → Key falsch, leer oder
|
||
keinem Benutzer zugeordnet. Der Key liegt **in der Datenbank** (pro Benutzer), nicht
|
||
mehr in der `.env` — ein Serverneustart ist nach einer Key-Änderung **nicht** nötig,
|
||
ein neuer Key gilt sofort und macht den alten ungültig. Häufigste Ursache: Der Token
|
||
wurde in der Oberfläche neu generiert, die Umgebung nutzt noch den alten.
|
||
- **404 obwohl die ID existiert** → Die ID gehört einem **anderen Benutzer**. Die API
|
||
zeigt fremde Datensätze grundsätzlich als „nicht vorhanden". Prüfe, mit wessen Key du
|
||
arbeitest (`BEWERBUNG_USER` / `BEWERBUNG_API_KEY`).
|
||
- **409** bei `POST /applications` → mögliche Dublette (`matches[]`); mit `force:true` erzwingen.
|
||
- **409** bei `POST /joboffers` → Angebot steht auf der **Blacklist**
|
||
(`BlacklistConflict`: `blacklisted`, `matched_by`, `blacklist_entry`); nicht
|
||
erzwingbar — erst den Blacklist-Eintrag entfernen (`DELETE /joboffers/blacklist/{id}`).
|
||
- **404** → ID existiert nicht.
|
||
- Binär-Downloads (Attachments) mit `curl ... -o datei` speichern, nicht ins Terminal.
|
||
- Vor Löschungen (DELETE) beim User rückversichern.
|
||
|
||
Vollständige OpenAPI-Definition: `http://localhost:4327/swagger.json`.
|