Skip to main content

Feed API: Technical Reference

[api] [feed]

REST endpoints for product feed export. Token-based authentication for server-to-server imports or session-based in the browser.

Base URL​

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

(Own-domain hosting is in preparation; until then use the <tenant>.licensio.io subdomain.)

Endpoints​

GET /api/portal/feed​

Exports the product catalog in the selected format.

Query parameters:

ParamValuesRequiredDefault
formatcsv | shopify | woocommerce | jsonnocsv
tokenstringno*(none)
fieldscomma-separated listnosaved selection, otherwise default

*Required when there is no session auth. A token counts as auth (see Authentication).

Available 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:

  • No token AND no session
  • Token invalid (no match in DB)
  • Customer status != active

GET /api/portal/feed/token​

Status of the feed token.

Auth: session cookie required.

Response 200 OK:

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

The token plaintext is not returned (security).

POST /api/portal/feed/token​

Generates or renews the feed token.

Auth: session cookie required.

Response 200 OK:

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

Token = crypto.randomBytes(32).toString('hex'). It is assigned to customers.feed_token (UNIQUE, the old token is overwritten).

DELETE /api/portal/feed/token​

Invalidate the token.

Response 200 OK:

{ "success": true }

feed_token and feed_token_created_at are set to NULL.

GET /api/portal/feed/settings​

Auth: session cookie required.

Response 200 OK:

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

POST /api/portal/feed/settings​

Saves fields + mapping.

Body:

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

Both fields optional: they can be updated individually.

Authentication​

Token (for server-to-server)​

Query parameter ?token=<64-char-hex>. The token must exist in customers.feed_token AND customers.status = 'active'.

Advantage: no cookies needed, ideal for cron jobs / shop importers.

Risk: whoever has the token can see the catalog. Read-only, no personal data.

Session (for the browser)​

Standard Supabase auth cookies. Works when the endpoint is called same-origin.

Format specifications​

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 row with column names (overridable via mapping)
  • CSV escaping: values containing ,, ", \n are wrapped in quotes, internal " is doubled

Shopify CSV​

Fixed columns (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

Column mapping is ignored here (Shopify-specific).

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 escaping: &, <, >, ", ' encoded correctly.

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 is applied to the JSON keys.

Package restrictions​

The feed applies the identical filters as the portal /dashboard/products:

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
})

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

Field mapping to the DB​

Feed fieldDB path
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

Example calls​

# 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..."