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:
| Param | Values | Required | Default |
|---|---|---|---|
format | csv | shopify | woocommerce | json | no | csv |
token | string | no* | (none) |
fields | comma-separated list | no | saved 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/xmlContent-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
,,",\nare 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 field | DB path |
|---|---|
name | catalog_items.name |
description | catalog_items.metadata.description (JSONB) |
category | catalog_items.metadata.category (JSONB) |
compatibilities | catalog_items.compatibility[] joined , |
preview_image | catalog_items.images[0] |
additional_images | catalog_items.images[1..] joined , |
price | catalog_items.recommended_price (text) |
tags | compatibility + metadata.category joined |
file_formats | distinct production_files.file_type joined |
created_at | catalog_items.created_at |
updated_at | catalog_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..."
Related
- Feed export feature: provider-focused guide
- Package restrictions: filter logic