REST · JSON · versioniert

Provenia API

Mit der Provenia-API liest und pflegst du Produkte, Produktpässe, Datenträger und Scan-Statistiken deines Betriebs — z. B. aus deiner Warenwirtschaft heraus. Alle Antworten sind JSON, alle Endpunkte sind strikt auf deinen Betrieb (Tenant) begrenzt und unter /api/v1 versioniert.

Basis-URL   https://pass.provenia.at/api/v1
Format      JSON (UTF-8) · Content-Type: application/json
Auth        Authorization: Bearer pv_…  (API-Schlüssel)
Limits      max. 200 Einträge pro Seite (limit/offset-Paging)

Authentifizierung

Jeder Request braucht einen API-Schlüssel im Authorization-Header. Schlüssel erstellst du in der App unter Einstellungen → API (nur Inhaber:innen). Der Schlüssel wird genau einmal angezeigt — wir speichern nur seinen SHA-256-Hash.

curl https://pass.provenia.at/api/v1/me \
  -H "Authorization: Bearer pv_DEIN_SCHLUESSEL"

Widerrufene Schlüssel werden sofort ungültig. Behandle Schlüssel wie Passwörter: nie im Frontend einbetten, nie committen.

Fehler & Statuscodes

Fehler kommen immer in derselben Struktur:

{
  "error": {
    "code": "invalid_api_key",
    "message": "API-Schlüssel fehlt, ist ungültig oder wurde widerrufen."
  }
}
ParameterTypBeschreibung
invalid_api_key401Schlüssel fehlt, ist ungültig oder widerrufen.
not_found404Produkt/Ressource existiert nicht in deinem Betrieb.
invalid_json400Request-Body ist kein gültiges JSON.
name_required400Pflichtfeld 'name' fehlt (POST /products).
unknown_product_group400product_group ist keiner Vorlage zugeordnet.
values_must_be_array400PUT …/passport erwartet ein Array.
field_key_required400Jeder Wert braucht ein field_key.
internal_error500Unerwarteter Fehler — bitte erneut versuchen.
GET/api/v1/me

Betrieb abfragen

Liefert den Betrieb, der zum Schlüssel gehört — ideal als Verbindungs-Test.

{
  "tenant": { "id": "…", "name": "Leinenweberei Hofer", "slug": "leinenweberei-hofer",
              "country": "AT", "plan": "trial" },
  "products_total": 12
}
GET/api/v1/products

Produkte auflisten

ParameterTypBeschreibung
statusstring, optionalFilter: draft · active · archived
searchstring, optionalSuche im Produktnamen (enthält, case-insensitiv)
limitint, optionalSeitengröße, Standard 50, max. 200
offsetint, optionalVersatz fürs Paging, Standard 0
curl "…/api/v1/products?status=active&limit=2" -H "Authorization: Bearer pv_…"

{
  "items": [
    { "id": "…", "name": "Leinenhemd Mühlviertel", "slug": "leinenhemd-muehlviertel",
      "status": "active", "gtin": "9012345678901", "completeness": 92,
      "registry_status": "none",
      "unique_product_identifier": "urn:provenia:leinenweberei-hofer:leinenhemd-muehlviertel",
      "created_at": "…", "updated_at": "…" }
  ],
  "total": 5, "limit": 2, "offset": 0
}
POST/api/v1/products

Produkt anlegen

ParameterTypBeschreibung
namestring, PflichtProduktname
slugstring, optionalURL-Slug — wird sonst aus dem Namen erzeugt; bei Kollision automatisch Suffix
product_groupstring, optionalVorlagen-Schlüssel, z. B. textile, battery, smartphone, computer, cable, motor, tyre, packaging, chemical, other — voller Katalog in der App
gtinstring, optionalGTIN (8/12/13/14 Ziffern) für GS1-konforme Datenträger
modelstring, optionalModell/Variante
curl -X POST …/api/v1/products \
  -H "Authorization: Bearer pv_…" -H "Content-Type: application/json" \
  -d '{ "name": "Tischläufer Leinpfad", "product_group": "textile", "gtin": "9012345678918" }'

→ 200: das angelegte Produkt (gleiche Struktur wie GET /products/{slug})

Das Produkt startet als draft mit stabilem unique_product_identifier — der spätere QR-Link bleibt dauerhaft gültig.

GET/api/v1/products/{slug}

Einzelnes Produkt

Vollständige Stammdaten inkl. registry_status/registry_id (EU-Register-Vorbereitung) und Produktgruppe.

curl …/api/v1/products/leinenhemd-muehlviertel -H "Authorization: Bearer pv_…"
GET/api/v1/products/{slug}/passport

Produktpass lesen

Alle Pass-Werte (auch restricted/authority — die API ist dein interner Zugriff), inkl. Quelle, Prüf-Status und der letzten Version mit content_hash (Integritätsnachweis).

{
  "product": { "id": "…", "name": "Leinenhemd Mühlviertel", "slug": "…",
               "status": "active", "completeness": 92 },
  "template_version": 1,
  "values": [
    { "field_key": "co2_footprint", "value": "2,4", "unit": "kg CO₂e",
      "source": "supplier", "is_unknown": false, "needs_review": true,
      "access_level": "public", "locale": "de", "updated_at": "…" }
  ],
  "latest_version": { "content_hash": "…", "created_at": "…" }
}
PUT/api/v1/products/{slug}/passport

Pass-Werte schreiben (Upsert)

Body ist ein Array von Werten. Bestehende Felder werden aktualisiert, neue angelegt. Die Vollständigkeit wird serverseitig nachgeführt und jede Änderung versioniert (mit content_hash).

ParameterTypBeschreibung
field_keystring, PflichtFeld-Schlüssel der Vorlage, z. B. co2_footprint
valuebeliebig, PflichtDer Wert (String empfohlen)
unitstring, optionalEinheit, z. B. kg CO₂e
sourcestring, optionalmanual · import · supplier — Standard: import
access_levelstring, optionalpublic · restricted · authority — Standard: public
is_unknown / needs_reviewbool, optionalUnbekannt-Markierung / Prüf-Flag
curl -X PUT …/api/v1/products/leinenhemd-muehlviertel/passport \
  -H "Authorization: Bearer pv_…" -H "Content-Type: application/json" \
  -d '[
    { "field_key": "co2_footprint", "value": "2,4", "unit": "kg CO₂e" },
    { "field_key": "main_material", "value": "100 % Bio-Leinen" }
  ]'

→ { "updated": 2, "completeness": 92 }
GET/api/v1/products/{slug}/carriers

Datenträger eines Produkts

{
  "items": [
    { "id": "…", "type": "qr",
      "gs1_digital_link": "https://…/01/09012345678901",
      "nfc_tag_uid": null, "status": "active", "created_at": "…" }
  ]
}
GET/api/v1/analytics/scans?days=30

Scan-Statistik (anonym)

ParameterTypBeschreibung
daysint, optionalZeitraum in Tagen, Standard 30, max. 365
{
  "days": 30, "total": 1248,
  "per_day": [ { "date": "2026-05-07", "count": 22 }, … ],
  "by_view_role": { "consumer": 1190, "inspector": 41, "recycler": 17 },
  "top_products": [ { "slug": "tischlaeufer-leinpfad", "name": "Tischläufer Leinpfad", "count": 412 }, … ]
}

Es werden keine personenbezogenen Daten erfasst — IPs sind nur als gesalzener Hash gespeichert.

Öffentliche Endpunkte (ohne Schlüssel)

Für aktivierte Pässe — das, was Datenträger und Dritt-Systeme nutzen:

ParameterTypBeschreibung
GET /p/{tenant}/{produkt}HTMLÖffentliche Pass-Seite; erweiterte Ansicht nur mit Zugangs-Token (?zugang=…)
GET /p/{tenant}/{produkt}/jsonldapplication/ld+jsonMaschinenlesbarer Pass (schema.org + GS1-Vokabular, content-Hash); ?zugang=… wie oben
GET /01/{gtin}302/307GS1-Digital-Link-Resolver → leitet zur Pass-Seite

Öffentlich erscheinen nur Felder mit access_level=public aktivierter Produkte. restricted-Felder (Lieferkette, Demontage, Stoffe) gibt es ausschließlich über Prüf-/Recycling-Links mit gültigem Token — die Rolle steckt im Token, ein bloßer ?view=-Parameter genügt nicht. Token erstellst und widerrufst du im Produktdetail unter „Vorschau“.