Base-URL: https://<host>/admin/api/
EIGENES System
Dies ist ein eigenes REST-System — Routen, Felder und Verhalten gelten ausschließlich für dieses Admin und folgen keiner fremden Schnittstelle.
Jeder Request braucht einen gültigen API-Key (64 Hex-Zeichen):
X-API-Key: <64-hex-key>?api_key=<key>Keys werden im Admin unter API-Keys erzeugt. Der Klartext-Key wird nur einmal direkt nach dem Anlegen angezeigt — danach nie wieder.
Fehlt der Key oder ist er inaktiv/ungültig:
401 {"success":false,"error":"unauthorized"}
| Methode | Route | Zweck |
|---|---|---|
| GET | /admin/api/<entity>?limit=&offset= | Liste (default limit=50, max 200, offset=0) |
| GET | /admin/api/<entity>?format=csv | Export: ALLE Datensätze als CSV, uuid als erste Spalte |
| GET | /admin/api/<entity>/<uuid> | Einzelner Datensatz per UUID |
| POST | /admin/api/<entity> | Upsert; Body JSON-Objekt, uuid Pflicht |
| POST | /admin/api/<entity> | Massen-Upsert: JSON-Array bzw. {"data":[…]} |
| POST | /admin/api/<entity> (Content-Type: text/csv) | Massen-Upsert per CSV — die exportierte Datei unverändert zurückschicken |
Der Export liefert standardmäßig alle Zeilen (mit limit/offset
lässt es sich eingrenzen), sortiert aufsteigend nach interner id — so stehen übergeordnete Einträge
vor ihren untergeordneten, was beim Zurückspielen von Kategorien zählt.
Format: Trennzeichen ;, UTF-8 mit BOM (öffnet direkt in Excel).
Dieselbe Datei geht unverändert wieder rein. Leere Zellen werden zu NULL,
nicht zu "" — sonst scheitert jede Zahlenspalte, weil der Export für NULL
eine leere Zelle schreibt. Spalten, die nicht in der write-Liste stehen
(created_at u. a.), werden beim Rückspielen ignoriert, nicht abgelehnt.
# Export
curl -H "X-API-Key: $KEY" "https://host/admin/api/articles?format=csv" -o artikel.csv
# unverändert zurückspielen (Upsert per uuid)
curl -H "X-API-Key: $KEY" -H "Content-Type: text/csv" \
--data-binary @artikel.csv "https://host/admin/api/articles"
Standard ist zeilenweise: jede Zeile zählt für sich, am Ende kommt ein Bericht.
Gibt es Fehler, ist die Antwort 207 mit success:false, und
results nennt Zeilennummer, uuid und Grund.
{"success":true,"total":13,"created":0,"updated":13,"failed":0,
"results":[{"row":1,"uuid":"…","action":"updated"}, …]}
Mit ?atomic=1 gilt alles oder nichts: die erste fehlerhafte Zeile
rollt den ganzen Vorgang zurück, Antwort 422.
Obergrenze: 5000 Zeilen pro Aufruf (darüber 413). Ein einzelnes
JSON-Objekt verhält sich unverändert wie bisher (201 created / 200 updated).
UUID = einziger externer Identifier. POST ist ein Upsert per UUID:
existiert die UUID → UPDATE (nur write-Felder), sonst INSERT
mit der übergebenen Client-UUID. Die interne id wird nie als Schlüssel verlangt
(nur informativ bei Create zurückgegeben).
Ohne mod_rewrite funktioniert alles identisch über den Fallback
index.php?entity=<e>&uuid=<u>.
Pro Entität sind nur die gelisteten Felder les- bzw. schreibbar (Whitelist).
manufacturer_id, parent_id, unit_id, group_id,
shop_id). Die sind pro Shop verschieden — unverändert übernommen zeigen
sie im Zielshop auf etwas anderes oder ins Leere. Vor dem Rückspielen umschlüsseln.created_at füllt sich bei articles, categories und
orders automatisch; bei customers und units gibt es keinen
Standardwert — neu angelegte Datensätze bleiben dort ohne Datum.
articles)read: uuid, name, variant_name, article_number, ean, net_price, price, other_price, tax_class, status, type, typ, root, parent_id, stock, manufacturer_id, unit_id, unit, product_unit, sales_unit, description_short, description_long, weight, buy_active, book_active, rent_active, rent_price, rent_interval, book_price, abo_active, abo_weekly, abo_monthly, abo_yearly, abo_discount_weekly, abo_discount_monthly, abo_discount_yearly, product_interval, interval_unit, requires_appointment, appointment_duration, appointment_type, images, created_at, updated_at
write: wie read, ohne uuid, images, created_at, updated_at
Hinweis: article_number u.ä. sind NOT-NULL ohne Default — beim ersten INSERT mitschicken, sonst kommt ein 400 db: ….
images ist nur lesbar (JSON mit Medien-Verweisen). stock ist der GESAMT-Bestand; die Aufteilung auf einzelne Lager lässt sich hierüber nicht setzen.
categories)read: uuid, name, parent_id, type, status, sort, groups, system, layout_id, description_short, description_long, images, created_at, updated_at
write: name, parent_id, type, status, sort, groups, layout_id, description_short, description_long
Hinweis: type ist NOT-NULL ohne Default — beim ersten INSERT mitschicken. system und images sind nur lesbar.
Diese Entität deckt drei Arten ab, unterschieden über type:
category (Kategorie), page (Seite) und manufacturer (Hersteller).
Hersteller lagen früher in einer eigenen Tabelle; sie liegen jetzt hier, damit alle drei dieselben Felder,
dieselben Layouts und dieselben Platzhalter haben. Zum Export nur der Hersteller nach type filtern.
layout_id verweist auf ein Layout (Menü Inhalte → Layouts).
Leer bedeutet: es gilt das Standard-Layout.
orders)read: uuid, number, email, status, payment_status, shipping_status, sub_total, tax_total, total, shipping_cost, billing_company, billing_firstname, billing_lastname, billing_street, billing_zipcode, billing_city, billing_country, shipping_company, shipping_firstname, shipping_lastname, shipping_street, shipping_zipcode, shipping_city, shipping_country, shipping_method_id, payment_method, transaction, tracking_numbers, comment, confirmation_sent, group_calendar_id, group_appointment, group_appointment_end, customer_account_id, customer_group_id, partner_id, shop_id, created_at, updated_at
write: status, payment_status, shipping_status, tracking_numbers, comment — Stammdaten der Bestellung bleiben schreibgeschützt
Nicht enthalten: stripe_* (Zahlungs-Identifikatoren) und die neofire_*-Sync-Marker der Plugins.
customer_accounts_customers)read: uuid, email, salutation, firstname, lastname, company, phone, group_id, shop_id, status, last_login_at, created_at, updated_at
write: email, salutation, firstname, lastname, company, phone, group_id, shop_id, status — kein password_hash in read/write
units)read: uuid, name, short_name, sort, created_at
write: name, short_name, sort
Die Registry bildet je Entität genau EINE Tabelle ab, adressiert über uuid.
Verknüpfte Daten ohne eigene uuid brauchen eigene Endpunkte und sind derzeit nicht übertragbar:
Lagerbestände je Lager (article_warehouse_stock), Preise je Preisliste
(article_price_list_prices), Shop-Zuweisungen (shop_articles),
Kategorie- und Eigenschafts-Zuordnungen, Medien und Bestellpositionen.
| Code | Bedeutung |
|---|---|
400 | invalid json body / db: <fehler> |
401 | unauthorized (Key fehlt/ungültig/inaktiv) |
404 | unknown entity / not found |
405 | method not allowed |
413 | too many rows (Massen-Upsert > 5000 Zeilen) |
422 | uuid is required / no writable fields / atomic abgebrochen |
207 | Massen-Upsert teilweise fehlgeschlagen — Details in results |
# Liste
curl -H "X-API-Key: $KEY" https://host/admin/api/articles?limit=10
# Einzeln per UUID
curl -H "X-API-Key: $KEY" https://host/admin/api/articles/2b1c...e9
# Anlegen/Aktualisieren (uuid PFLICHT)
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"uuid":"2b1c...e9","name":"Neuer Artikel","article_number":"A-100","price":19.9}' \
https://host/admin/api/articles
# Liste
{"success":true,"total":42,"limit":10,"offset":0,"data":[ ... ]}
# Einzeln
{"success":true,"data":{ "uuid":"2b1c...e9", ... }}
# Create
{"success":true,"action":"created","uuid":"2b1c...e9","id":123}
# Update
{"success":true,"action":"updated","uuid":"2b1c...e9"}
# Fehler (uuid fehlt)
{"success":false,"error":"uuid is required"}