Zum Hauptinhalt springen

Feed API — Technische Referenz

[api] [feed]

REST-Endpoints für Produktfeed-Export. Token-basierte Authentifizierung für Server-zu-Server-Imports oder Session-basiert im Browser.

Basis-URL

https://<tenant>.licensio.io/api/portal/feed

oder mit Custom-Domain:

https://portal.deinedomain.de/api/portal/feed

Endpoints

GET /api/portal/feed

Exportiert den Produktkatalog im gewählten Format.

Query-Parameter:

ParamWertePflichtDefault
formatcsv | shopify | woocommerce | jsonneincsv
tokenstringnein*
fieldskommagetrennte Listeneingespeicherte Auswahl, sonst Default

*Pflicht wenn keine Session-Auth. Token gilt als Auth (siehe Authentifizierung).

Verfügbare fields: name, description, category, compatibilities, preview_image, additional_images, price, tags, file_formats, created_at, updated_at

Response Headers:

  • Content-Type: text/csv; charset=utf-8 / application/json / application/xml
  • Content-Disposition: attachment; filename="produkte.{ext}"
  • Cache-Control: private, no-store

Response 401 Unauthorized:

  • Kein Token UND keine Session
  • Token ungültig (kein Match in DB)
  • Customer-Status ≠ active

GET /api/portal/feed/token

Status des Feed-Tokens.

Auth: Session-Cookie nötig.

Response 200 OK:

{
"has_token": true,
"created_at": "2026-04-25T14:30:00Z"
}

Token-Klartext wird nicht zurückgegeben (Security).

POST /api/portal/feed/token

Generiert oder erneuert Feed-Token.

Auth: Session-Cookie nötig.

Response 200 OK:

{
"token": "f9a1b2c3...64-char-hex"
}

Token = crypto.randomBytes(32).toString('hex'). Wird customers.feed_token zugeordnet (UNIQUE, alter Token wird überschrieben).

DELETE /api/portal/feed/token

Token invalidieren.

Response 200 OK:

{ "success": true }

feed_token und feed_token_created_at werden auf NULL gesetzt.

GET /api/portal/feed/settings

Auth: Session-Cookie nötig.

Response 200 OK:

{
"fields": ["name", "description", "category", "price"],
"mapping": {
"name": "Title",
"price": "Variant Price"
}
}

POST /api/portal/feed/settings

Speichert Felder + Mapping.

Body:

{
"fields": ["name", "description"],
"mapping": { "name": "Title" }
}

Beide Felder optional — können einzeln upgedatet werden.

Authentifizierung

Token (für Server-zu-Server)

Query-Parameter ?token=<64-char-hex>. Token muss in customers.feed_token existieren UND customers.status = 'active'.

Vorteil: keine Cookies nötig, ideal für Cron-Jobs / Shop-Importer.

Risk: Wer den Token hat, sieht den Katalog. Read-only, keine personenbezogenen Daten.

Session (für Browser)

Standard-Supabase-Auth-Cookies. Funktioniert wenn Endpoint Same-Origin aufgerufen wird.

Format-Spezifikationen

CSV (default)

Name,Beschreibung,Kategorie,Kompatibilitaeten,Vorschaubild,Preis,Tags
"Produkt 1","Beschreibung...",Werkzeug,"Bambu Lab, AC Infinity",https://...,49.99,"Bambu Lab, AC Infinity, Werkzeug"
  • Header-Zeile mit Spaltennamen (durch mapping überschreibbar)
  • CSV-Escape: Werte mit ,, ", \n werden in Anführungszeichen gewrapped, interne " verdoppelt

Shopify CSV

Feste Spalten (Shopify Product Import Format):

Handle,Title,Body (HTML),Vendor,Type,Tags,Published,Image Src,Image Position,Variant Price,Variant SKU,Variant Inventory Policy
licensio-<uuid>,"Produkt 1","Beschreibung...","Plant Technology",Werkzeug,"Bambu Lab",true,https://...,1,49.99,licensio-<uuid>,continue

Spalten-Mapping wird hier ignoriert (Shopify-spezifisch).

WooCommerce XML

<?xml version="1.0" encoding="UTF-8"?>
<products>
<product>
<id>uuid</id>
<name>Produkt 1</name>
<description>Beschreibung...</description>
<categories>Werkzeug</categories>
<tags>Bambu Lab, AC Infinity</tags>
<images>
<image>https://...</image>
</images>
<price>49.99</price>
</product>
</products>

XML-Escape: &, <, >, ", ' korrekt encodiert.

JSON

[
{
"name": "Produkt 1",
"description": "Beschreibung...",
"category": "Werkzeug",
"compatibilities": "Bambu Lab, AC Infinity",
"preview_image": "https://...",
"price": "49.99",
"tags": "Bambu Lab, AC Infinity, Werkzeug"
}
]

Pretty-printed (2-Space-Indent). Mapping wird auf JSON-Keys angewandt.

Paket-Einschränkungen

Der Feed wendet die identischen Filter wie das Portal-/dashboard/products an:

items.filter(item => {
// excluded_categories gewinnt
if (excludedCats.includes(category)) return false
// allowed_categories Whitelist
if (allowedCats.length && !allowedCats.includes(category)) return false
// excluded_compatibilities gewinnt
if (excludedCompats.some(c => item.compatibility.includes(c))) return false
// allowed_compatibilities (Items ohne Compats sind sichtbar)
if (allowedCompats.length && item.compatibility.length &&
!item.compatibility.some(c => allowedCompats.includes(c))) return false
return true
})

Quelle: apps/portal/app/api/portal/feed/route.ts.

Felder-Mapping zur DB

Feed-FeldDB-Pfad
namecatalog_items.name
descriptioncatalog_items.metadata.description (JSONB)
categorycatalog_items.metadata.category (JSONB)
compatibilitiescatalog_items.compatibility[] joined ,
preview_imagecatalog_items.images[0]
additional_imagescatalog_items.images[1..] joined ,
pricecatalog_items.recommended_price (text)
tagscompatibility + metadata.category joined
file_formatsdistinct production_files.file_type joined
created_atcatalog_items.created_at
updated_atcatalog_items.updated_at

Beispiel-Aufrufe

# Standard-CSV mit Token
curl "https://planttechnology.licensio.io/api/portal/feed?format=csv&token=abc..."

# JSON mit nur 3 Feldern
curl "https://planttechnology.licensio.io/api/portal/feed?format=json&token=abc...&fields=name,price,preview_image"

# Shopify-Direkt-Import
curl -O -J "https://planttechnology.licensio.io/api/portal/feed?format=shopify&token=abc..."

# WooCommerce XML
curl "https://planttechnology.licensio.io/api/portal/feed?format=woocommerce&token=abc..."

Verwandt