Zum Inhalt

Datei als Distribution hochladen per API

Neben Tabellendaten und Geodiensten gehören zu vielen Datensätzen auch Dateien, die einfach „nur" bereitgestellt werden sollen: ein Bericht als PDF, ein Export im JSON-Format oder ein ZIP-Archiv mit mehreren Dateien. Solche Dateien werden über den CKAN-Service direkt als Distribution an einen Datensatz gehängt.

In diesem Guide legen wir dafür zunächst einen neuen Datensatz an und laden anschließend eine PDF-Datei hoch. Wie JSON- oder ZIP-Dateien hochgeladen werden, steht in Schritt 5.

CSV-Dateien gehören in die Staging-API

CSV-Dateien bitte nicht über diesen Weg hochladen. Sie laufen über die Staging-API, damit sie gegen ein Schema validiert und für die weitere Verarbeitung (z. B. Parquet, SPARQL) aufbereitet werden. Wie das funktioniert, beschreibt der Guide Datensatz hochladen per API.

Umgebungen

Über die Tabs bei den Beispiel-Requests kann zwischen Produktion und Test gewechselt werden.

Voraussetzungen

  • Client-ID und Client-Secret eines Keycloak Service Accounts liegen vor
  • Die hochzuladende Datei liegt lokal vor (im Beispiel luftqualitaet-jahresbericht-2025.pdf)
  • Metadaten sind bekannt (siehe Metadaten-IDs abrufen):
    • Katalog-Name (catalogue), z. B. opendata-sh
    • Lizenz-ID (licenseId), z. B. http://dcat-ap.de/def/licenses/cc-zero
    • Publisher-ID (publisherId), z. B. lfu
    • Kategorie-IDs (categoryIds), z. B. ["envi"]
    • Raumbezug-ID (spatialReferenceId), z. B. http://dcat-ap.de/def/politicalGeocoding/stateKey/01

Vorgehen

1. Token abrufen

Alle folgenden Requests erwarten ein JWT-Token im Authorization-Header – siehe API-Token abrufen.

Der Token ist nur einige Minuten gültig. Wer die Schritte nacheinander von Hand ausführt, sollte bei einem 401 einfach einen neuen Token holen.

2. Metadaten-IDs abrufen

Lizenz, Herausgeber, Kategorien und Raumbezug werden über IDs aus dem Metadaten-Service angegeben; die vollständige Übersicht steht unter Metadaten-IDs abrufen.

Häufig verwendete Werte:

Feld Wert Kommentar
catalogue opendata-sh
licenseId http://dcat-ap.de/def/licenses/cc-zero Creative Commons CC Zero
spatialReferenceId http://dcat-ap.de/def/politicalGeocoding/stateKey/01 Bundesland Schleswig-Holstein
categoryIds ["envi", "tran", "ener", "gove"] Umwelt, Verkehr, Energie, Regierung und öffentlicher Sektor

3. Datensatz anlegen

Eine Distribution kann nur an einen bestehenden Datensatz gehängt werden. Deshalb legen wir zuerst den Datensatz an – vorerst nur mit Metadaten und ohne Dateien.

Wir lassen publish hier bewusst weg. Der Datensatz bleibt dann ein Entwurf und ist im Portal noch nicht sichtbar, solange die Dateien fehlen. Veröffentlicht wird er ganz am Ende, wenn alle Dateien hochgeladen sind (siehe Schritt 7).

Endpunkt

POST https://ckan-service.odi.schleswig-holstein.de/dataset/{catalogue}

POST https://ckan-service.odi-stage.schleswig-holstein.de/dataset/{catalogue}

Felder

Feld Pflicht Typ Beschreibung Quelle
datasetId Pflicht string Frei wählbare ID des Datensatzes frei wählbar
title Pflicht JSON-Objekt Titel des Datensatzes (mehrsprachig) frei wählbar
description Pflicht JSON-Objekt Beschreibung des Datensatzes (mehrsprachig) frei wählbar
publisherId Pflicht string ID des Herausgebers Metadaten-Service /publisher
categoryIds Pflicht string[] Array der Kategorie-IDs Metadaten-Service /categories
licenseId Pflicht string ID der Lizenz Metadaten-Service /licenses
spatialReferenceId Pflicht string ID des Raumbezugs Metadaten-Service /places
startDate Pflicht string Startdatum des Datensatz-Zeitraums im ISO-8601-Format (z. B. 2025-01-01T00:00:00) bekannt
endDate Optional string Enddatum des Datensatz-Zeitraums im ISO-8601-Format (z. B. 2025-12-31T23:59:59) –
issuedBy Optional string Dienst oder Nutzer, der den Datensatz angelegt hat frei wählbar
publish Optional string "true" = sofort veröffentlichen; fehlt = Draft –

Info

title und description sind mehrsprachige JSON-Objekte, z. B.: {"de": "Luftqualität in Schleswig-Holstein 2025", "en": "Air quality in Schleswig-Holstein 2025"} Mindestens ein Sprachwert (de oder en) ist erforderlich.

Beispiel-Request

curl --request POST \
  --url https://ckan-service.odi.schleswig-holstein.de/dataset/opendata-sh \
  --header 'authorization: Bearer <access_token>' \
  --header 'content-type: application/json' \
  --data '{
    "datasetId": "luftqualitaet-sh-2025",
    "title": {
      "de": "Luftqualität in Schleswig-Holstein 2025"
    },
    "description": {
      "de": "Jahresbericht zur Luftqualität in Schleswig-Holstein mit den Messergebnissen der Stationen des Lufthygienischen Überwachungssystems."
    },
    "publisherId": "lfu",
    "categoryIds": ["envi"],
    "licenseId": "http://dcat-ap.de/def/licenses/cc-zero",
    "spatialReferenceId": "http://dcat-ap.de/def/politicalGeocoding/stateKey/01",
    "startDate": "2025-01-01T00:00:00",
    "endDate": "2025-12-31T23:59:59",
    "issuedBy": "ODI-Api-Examples"
  }'
curl --request POST \
  --url https://ckan-service.odi-stage.schleswig-holstein.de/dataset/opendata-sh \
  --header 'authorization: Bearer <access_token>' \
  --header 'content-type: application/json' \
  --data '{
    "datasetId": "luftqualitaet-sh-2025",
    "title": {
      "de": "Luftqualität in Schleswig-Holstein 2025"
    },
    "description": {
      "de": "Jahresbericht zur Luftqualität in Schleswig-Holstein mit den Messergebnissen der Stationen des Lufthygienischen Überwachungssystems."
    },
    "publisherId": "lfu",
    "categoryIds": ["envi"],
    "licenseId": "http://dcat-ap.de/def/licenses/cc-zero",
    "spatialReferenceId": "http://dcat-ap.de/def/politicalGeocoding/stateKey/01",
    "startDate": "2025-01-01T00:00:00",
    "endDate": "2025-12-31T23:59:59",
    "issuedBy": "ODI-Api-Examples"
  }'

Beispiel-Response (Erfolg)

{
  "datasetId": "luftqualitaet-sh-2025",
  "catalogue": "opendata-sh",
  "distributionIds": [],
  "modifiedDate": "2026-09-24T09:14:02.518734",
  "issuedDate": "2026-09-24T09:14:01.873000+00:00",
  "datasetUrl": "https://opendata.schleswig-holstein.de/dataset/luftqualitaet-sh-2025"
}

Die datasetId aus der Antwort brauchen wir im nächsten Schritt.

Fehlercodes

HTTP-Status Bedeutung Mögliche Ursache / Lösung
400 Ungültige Anfrage Pflichtfeld fehlt oder falsches Format
401 Nicht authentifiziert Access-Token fehlt oder ist abgelaufen → neuen Token abrufen. Oder dem Service Account ist kein gültiger ckan-token zugeordnet → Zugriff prüfen
403 Keine Berechtigung Das CKAN-Konto hinter dem ckan-token hat im Datenkatalog keine Schreibrechte für den Herausgeber
500 Interner Serverfehler Kontakt zum ODI-Support aufnehmen

4. PDF-Datei hochladen

Jetzt hängen wir den Jahresbericht als PDF an den Datensatz. Anders als beim Anlegen des Datensatzes wird hier kein JSON gesendet, sondern ein multipart/form-data-Request: Die Datei steckt im Feld file, die Metadaten der Distribution kommen als weitere Formularfelder dazu.

Endpunkt

POST https://ckan-service.odi.schleswig-holstein.de/distribution/{catalogue}/{datasetId}

POST https://ckan-service.odi-stage.schleswig-holstein.de/distribution/{catalogue}/{datasetId}

Felder (Form-Data)

Feld Pflicht Typ Beschreibung
file Pflicht Datei Die hochzuladende Datei; darf nicht leer sein
format Pflicht string Dateiformat, z. B. PDF, JSON oder ZIP (siehe Formate)
licenseId Pflicht string ID der Lizenz
title Pflicht JSON-String Titel der Distribution (mehrsprachig), z. B. {"de": "Jahresbericht 2025"}
description Optional JSON-String Beschreibung der Distribution (mehrsprachig)

Titel und Beschreibung als JSON-String

Da es sich um ein Formular handelt, werden title und description nicht als verschachteltes JSON-Objekt, sondern als String übergeben, der JSON enthält. In curl daher die einfachen Anführungszeichen um den Wert nicht vergessen: --form 'title={"de": "Jahresbericht 2025"}'.

Im Portal wird der deutsche Wert angezeigt. Ist keiner vorhanden, wird der englische genommen.

Beispiel-Request

Der Pfad hinter dem @ zeigt auf die lokale Datei. curl setzt den Content-Type des Requests inklusive Boundary selbst – den Header also nicht manuell angeben.

curl --request POST \
  --url https://ckan-service.odi.schleswig-holstein.de/distribution/opendata-sh/luftqualitaet-sh-2025 \
  --header 'authorization: Bearer <access_token>' \
  --form 'file=@./luftqualitaet-jahresbericht-2025.pdf' \
  --form 'format=PDF' \
  --form 'licenseId=http://dcat-ap.de/def/licenses/cc-zero' \
  --form 'title={"de": "Jahresbericht Luftqualität 2025"}' \
  --form 'description={"de": "Bericht mit Auswertungen und Grenzwertvergleichen für das Jahr 2025."}'
curl --request POST \
  --url https://ckan-service.odi-stage.schleswig-holstein.de/distribution/opendata-sh/luftqualitaet-sh-2025 \
  --header 'authorization: Bearer <access_token>' \
  --form 'file=@./luftqualitaet-jahresbericht-2025.pdf' \
  --form 'format=PDF' \
  --form 'licenseId=http://dcat-ap.de/def/licenses/cc-zero' \
  --form 'title={"de": "Jahresbericht Luftqualität 2025"}' \
  --form 'description={"de": "Bericht mit Auswertungen und Grenzwertvergleichen für das Jahr 2025."}'

Beispiel-Response (Erfolg)

Der Service antwortet mit 201 Created und liefert die neue Distribution zurück. Unter url ist die Datei ab sofort abrufbar, sobald der Datensatz veröffentlicht ist.

{
  "url": "https://opendata.schleswig-holstein.de/dataset/luftqualitaet-sh-2025/resource/3f6c2a1e-8d4b-4c57-9a0e-51b7d2e9c8f4/download/luftqualitaet-jahresbericht-2025.pdf",
  "distributionId": "3f6c2a1e-8d4b-4c57-9a0e-51b7d2e9c8f4",
  "identifier": "3f6c2a1e-8d4b-4c57-9a0e-51b7d2e9c8f4",
  "isAFile": true,
  "title": {
    "de": "Jahresbericht Luftqualität 2025",
    "en": "Jahresbericht Luftqualität 2025"
  },
  "description": {
    "de": "Bericht mit Auswertungen und Grenzwertvergleichen für das Jahr 2025.",
    "en": "Bericht mit Auswertungen und Grenzwertvergleichen für das Jahr 2025."
  },
  "format": "PDF",
  "licenseId": "",
  "modifiedDate": null,
  "issuedDate": "2026-09-24T09:15:47.302118"
}

Die distributionId sollte man sich merken. Mit ihr lässt sich die Datei später austauschen, ohne eine neue Distribution anzulegen (siehe Nächste Schritte).

Fehlercodes

HTTP-Status Bedeutung Mögliche Ursache / Lösung
400 Ungültige Anfrage Keine oder leere Datei, title kein gültiges JSON, unbekanntes format
401 Nicht authentifiziert Access-Token fehlt oder ist abgelaufen → neuen Token abrufen. Oder dem Service Account ist kein gültiger ckan-token zugeordnet → Zugriff prüfen
403 Keine Berechtigung Service Account darf in diesen Datensatz nicht schreiben
404 Datensatz nicht gefunden datasetId oder catalogue prüfen
413 Datei zu groß Datei verkleinern bzw. aufteilen oder mit dem ODI-Support klären
500 Interner Serverfehler Kontakt zum ODI-Support aufnehmen

5. Weitere Dateien hochladen (JSON, ZIP)

Ein Datensatz kann beliebig viele Distributionen haben. Für jede weitere Datei wird derselbe Endpunkt noch einmal aufgerufen – es ändern sich nur die Datei, das format und Titel/Beschreibung.

Im Beispiel ergänzen wir die Messwerte als JSON-Export und die Rohdaten der Stationen als ZIP-Archiv.

# Messwerte als JSON
curl --request POST \
  --url https://ckan-service.odi.schleswig-holstein.de/distribution/opendata-sh/luftqualitaet-sh-2025 \
  --header 'authorization: Bearer <access_token>' \
  --form 'file=@./luftqualitaet-messwerte-2025.json' \
  --form 'format=JSON' \
  --form 'licenseId=http://dcat-ap.de/def/licenses/cc-zero' \
  --form 'title={"de": "Jahresmittelwerte 2025 (JSON)"}'

# Rohdaten als ZIP
curl --request POST \
  --url https://ckan-service.odi.schleswig-holstein.de/distribution/opendata-sh/luftqualitaet-sh-2025 \
  --header 'authorization: Bearer <access_token>' \
  --form 'file=@./luftqualitaet-rohdaten-2025.zip' \
  --form 'format=ZIP' \
  --form 'licenseId=http://dcat-ap.de/def/licenses/cc-zero' \
  --form 'title={"de": "Rohdaten der Messstationen 2025"}' \
  --form 'description={"de": "Halbstündliche Messwerte aller Stationen, ein Ordner je Station."}'
# Messwerte als JSON
curl --request POST \
  --url https://ckan-service.odi-stage.schleswig-holstein.de/distribution/opendata-sh/luftqualitaet-sh-2025 \
  --header 'authorization: Bearer <access_token>' \
  --form 'file=@./luftqualitaet-messwerte-2025.json' \
  --form 'format=JSON' \
  --form 'licenseId=http://dcat-ap.de/def/licenses/cc-zero' \
  --form 'title={"de": "Jahresmittelwerte 2025 (JSON)"}'

# Rohdaten als ZIP
curl --request POST \
  --url https://ckan-service.odi-stage.schleswig-holstein.de/distribution/opendata-sh/luftqualitaet-sh-2025 \
  --header 'authorization: Bearer <access_token>' \
  --form 'file=@./luftqualitaet-rohdaten-2025.zip' \
  --form 'format=ZIP' \
  --form 'licenseId=http://dcat-ap.de/def/licenses/cc-zero' \
  --form 'title={"de": "Rohdaten der Messstationen 2025"}' \
  --form 'description={"de": "Halbstündliche Messwerte aller Stationen, ein Ordner je Station."}'

Formate

Der Wert in format muss aus der File-Type-Liste des EU-Amts für Veröffentlichungen stammen. Groß- und Kleinschreibung spielt keine Rolle, der Service schreibt den Wert ohnehin groß. Ein unbekanntes Format wird mit 400 abgelehnt.

Für Dateien, die typischerweise über diesen Weg hochgeladen werden:

Datei format
PDF-Dokument PDF
JSON JSON
GeoJSON GEOJSON
ZIP-Archiv ZIP
Excel XLSX bzw. XLS
XML XML
Word-Dokument DOCX

ZIP-Archive

Ein ZIP ist praktisch, um viele kleine Dateien zu bündeln. Für Nutzende ist aber nicht erkennbar, was drinsteckt. Deshalb lohnt es sich, den Inhalt kurz in der description zu beschreiben – wie im Beispiel oben.

6. Ergebnis prüfen

Ob alle Dateien angekommen sind, lässt sich über die Distributionen des Datensatzes abfragen:

curl --request GET \
  --url https://ckan-service.odi.schleswig-holstein.de/dataset/luftqualitaet-sh-2025/distributions \
  --header 'authorization: Bearer <access_token>'
curl --request GET \
  --url https://ckan-service.odi-stage.schleswig-holstein.de/dataset/luftqualitaet-sh-2025/distributions \
  --header 'authorization: Bearer <access_token>'

Die Antwort sollte alle drei Distributionen enthalten. Fehlt eine, einfach den Upload aus Schritt 4 bzw. 5 wiederholen – solange der Datensatz noch ein Entwurf ist, bekommt davon niemand etwas mit.

7. Datensatz veröffentlichen

Sind alle Dateien da, wird der Datensatz öffentlich geschaltet. Dafür gibt es den eigenen Endpunkt publish, der keinen Request-Body braucht – Katalog und datasetId im Pfad reichen.

Endpunkt

PUT https://ckan-service.odi.schleswig-holstein.de/dataset/publish/{catalogue}/{datasetId}

PUT https://ckan-service.odi-stage.schleswig-holstein.de/dataset/publish/{catalogue}/{datasetId}

Beispiel-Request

curl --request PUT \
  --url https://ckan-service.odi.schleswig-holstein.de/dataset/publish/opendata-sh/luftqualitaet-sh-2025 \
  --header 'authorization: Bearer <access_token>'
curl --request PUT \
  --url https://ckan-service.odi-stage.schleswig-holstein.de/dataset/publish/opendata-sh/luftqualitaet-sh-2025 \
  --header 'authorization: Bearer <access_token>'

Beispiel-Response (Erfolg)

Zurück kommt der vollständige Datensatz. An den distributionIds sieht man noch einmal, dass alle drei Dateien dranhängen; unter datasetUrl ist er jetzt im Portal zu finden.

{
  "datasetId": "luftqualitaet-sh-2025",
  "catalogue": "opendata-sh",
  "title": {
    "de": "Luftqualität in Schleswig-Holstein 2025"
  },
  "description": {
    "de": "Jahresbericht zur Luftqualität in Schleswig-Holstein mit den Messergebnissen der Stationen des Lufthygienischen Überwachungssystems."
  },
  "publisherId": "lfu",
  "categoryIds": ["envi"],
  "licenseId": "http://dcat-ap.de/def/licenses/cc-zero",
  "spatialReferenceId": "http://dcat-ap.de/def/politicalGeocoding/stateKey/01",
  "startDate": "2025-01-01T00:00:00",
  "endDate": "2025-12-31T23:59:59",
  "distributionIds": [
    "3f6c2a1e-8d4b-4c57-9a0e-51b7d2e9c8f4",
    "a81d05c9-2e7f-4b3a-b6d1-0c94e7f25a38",
    "d27b9e40-5c1a-4f86-8e3d-7a6f0b19c2e5"
  ],
  "modifiedDate": "2026-09-24T09:21:08.604215",
  "issuedDate": "2026-09-24T09:14:01.873000+00:00",
  "datasetUrl": "https://opendata.schleswig-holstein.de/dataset/luftqualitaet-sh-2025"
}

Fehlercodes

HTTP-Status Bedeutung Mögliche Ursache / Lösung
401 Nicht authentifiziert Access-Token fehlt oder ist abgelaufen → neuen Token abrufen. Oder dem Service Account ist kein gültiger ckan-token zugeordnet → Zugriff prüfen
403 Keine Berechtigung Service Account darf diesen Datensatz nicht veröffentlichen
404 Datensatz nicht gefunden datasetId oder catalogue prüfen
500 Interner Serverfehler Kontakt zum ODI-Support aufnehmen

8. Datensatz löschen (optional)

Ein Datensatz kann per API gelöscht werden. Ohne den Parameter purge wird ein Datensatz nur als gelöscht markiert (Soft Delete) und kann wiederhergestellt werden. Mit purge=true wird er dauerhaft gelöscht – samt aller hochgeladenen Dateien – und die datasetId ist wieder frei.

Endpunkt

DELETE https://ckan-service.odi.schleswig-holstein.de/dataset/{datasetId}

DELETE https://ckan-service.odi-stage.schleswig-holstein.de/dataset/{datasetId}

Beispiel-Request

curl --request DELETE \
  --url 'https://ckan-service.odi.schleswig-holstein.de/dataset/luftqualitaet-sh-2025?purge=true' \
  --header 'authorization: Bearer <access_token>'
curl --request DELETE \
  --url 'https://ckan-service.odi-stage.schleswig-holstein.de/dataset/luftqualitaet-sh-2025?purge=true' \
  --header 'authorization: Bearer <access_token>'

Nächste Schritte

Kommt im nächsten Jahr eine neue Version des Berichts, muss keine neue Distribution angelegt werden: Mit PUT /distribution/{catalogue}/{distributionId}/file lässt sich die Datei einer bestehenden Distribution ersetzen. Die Details dazu und alle weiteren Endpunkte stehen in der API-Referenz des CKAN-Service:

CKAN-Adapter API → API Referenz