Übersicht
Die BISpicy POS REST API ermöglicht die Integration externer Systeme mit Ihrem Kassensystem. Synchronisieren Sie Produkte, Bestände, Verkaufsdaten und Kundendaten mit Ihrer bestehenden Infrastruktur.
Bidirektionaler Sync
Produkte, Kunden, Bestand
Echtzeit-Webhooks
Sofortige Benachrichtigungen
HMAC-SHA256
Signierte Schreibzugriffe
Für wen ist die API?
| System | Typischer Einsatz |
| JTL-Wawi | Artikel- und Bestandssync, Verkäufe als Aufträge importieren |
| Lexware | Buchhaltungsexport, Kundenstammdaten |
| Shopify / WooCommerce | Omnichannel-Bestand, Online-Bestellungen |
| DATEV | Täglicher Kassenbericht, Buchungsdaten |
| Custom ERP | Eigene Integrationen, Automatisierungen |
Technische Eckdaten
| Parameter | Wert |
| Base URL | https://bispicy.com/api/v1/ |
| Protokoll | HTTPS (TLS 1.3) |
| Format | JSON (UTF-8) |
| Authentifizierung | Bearer Token + HMAC-SHA256 |
| Rate Limits | 60 / 300 / 1.000 Requests/Min |
| Idempotency | 24h Cache für POST/PUT/PATCH |
Schnellstart
In 3 Schritten zur ersten API-Abfrage:
1. API-Key erstellen
Erstellen Sie einen API-Key im BISpicy Kundencenter unter API & Integrationen.
Credentials
API-Key: bsk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
API-Secret: bss_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
Wichtig: Key und Secret werden nur einmal angezeigt. Speichern Sie diese sicher.
2. Erster API-Call
cURL
curl -X GET "https://bispicy.com/api/v1/products" \
-H "Authorization: Bearer bsk_live_a1b2c3d4..." \
-H "Content-Type: application/json"
Response 200
{
"success": true,
"data": [
{
"id": "art_abc123",
"sku": "BIS-001",
"name": "Pad Thai",
"selling_price": 12.90,
"stock_quantity": 25
}
],
"meta": { "page": 1, "per_page": 50, "total": 142 }
}
3. Webhook registrieren
cURL
curl -X POST "https://bispicy.com/api/v1/webhooks" \
-H "Authorization: Bearer bsk_live_a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{
"target_url": "https://eure-wawi.de/api/bispicy-webhook",
"events": ["sale.completed", "inventory.updated"],
"description": "JTL-Wawi Integration"
}'
Authentifizierung
Lesende Zugriffe: Bearer Token
Für GET-Requests reicht der API-Key als Bearer Token:
HTTP
GET /api/v1/products HTTP/1.1
Host: bispicy.com
Authorization: Bearer bsk_live_a1b2c3d4...
Accept: application/json
Schreibende Zugriffe: HMAC-SHA256 Signierung
Für POST, PUT, PATCH, DELETE ist zusätzlich eine HMAC-Signatur erforderlich.
Pflicht-Header
| Header | Beschreibung |
Authorization | Bearer {api_key} |
X-BISpicy-Timestamp | Unix-Timestamp (Sekunden) |
X-BISpicy-Signature | v1={hmac_hex} |
Idempotency-Key | Eindeutige ID pro Request (UUID empfohlen) |
Content-Type | application/json |
Signatur berechnen
Python
import hmac, hashlib, time, json, uuid
timestamp = str(int(time.time()))
body = json.dumps(payload, separators=(',', ':'))
signed_content = f"{timestamp}.{body}"
signature = hmac.new(
api_secret.encode('utf-8'),
signed_content.encode('utf-8'),
hashlib.sha256
).hexdigest()
headers = {
"Authorization": f"Bearer {api_key}",
"X-BISpicy-Timestamp": timestamp,
"X-BISpicy-Signature": f"v1={signature}",
"Idempotency-Key": str(uuid.uuid4())
}
Sicherheit: API-Secret niemals im Frontend oder Client-Code verwenden. Nur serverseitig signieren.
Server-seitige Verifizierung
Der Server prüft bei jedem Request:
- API-Key vorhanden und aktiv? →
401
- API-Key nicht abgelaufen? →
401
- IP in Whitelist (wenn konfiguriert)? →
403
- Scope ausreichend? →
403
- Timestamp max. 5 Minuten alt? →
401 (Replay-Schutz)
- HMAC-Signatur korrekt? (nur Schreibzugriffe) →
401
- Rate Limit eingehalten? →
429
Scopes & Berechtigungen
Bei der API-Key-Erstellung werden Scopes festgelegt. Vergeben Sie nur die Berechtigungen, die tatsächlich benötigt werden (Principle of Least Privilege).
| Scope | Erlaubt |
products:read | Artikel und Kategorien lesen |
products:write | Artikel erstellen, ändern, löschen |
orders:read | Verkäufe und Bestellungen lesen |
inventory:read | Bestände lesen |
inventory:write | Bestände anpassen |
customers:read | Kundendaten lesen |
customers:write | Kunden erstellen, ändern, löschen |
reports:read | Kassenschlüsse, Z-Bons, Steuerberichte |
webhooks:manage | Webhooks erstellen, ändern, löschen |
settings:read | Geschäftseinstellungen lesen |
Beispiel: Ein reiner Bestands-Sync braucht nur products:read + inventory:read + inventory:write.
Endpoints-Übersicht
| Methode | Pfad | Scope | Beschreibung |
| GET | /products | products:read | Produktliste (paginiert, Filter) |
| GET | /products/{id} | products:read | Einzelnes Produkt |
| POST | /products | products:write | Produkt erstellen |
| PUT | /products/{id} | products:write | Produkt aktualisieren |
| DEL | /products/{id} | products:write | Produkt löschen (Soft-Delete) |
| POST | /products/batch | products:write | Batch-Upsert (max. 500) |
| Kategorien |
| GET | /categories | products:read | Kategorien auflisten (hierarchisch) |
| GET | /categories/{id} | products:read | Einzelne Kategorie |
| POST | /categories | products:write | Kategorie erstellen |
| PUT | /categories/{id} | products:write | Kategorie aktualisieren |
| DEL | /categories/{id} | products:write | Kategorie löschen |
| Bestand |
| GET | /inventory | inventory:read | Bestandslevel (low_stock Filter) |
| POST | /inventory/adjustments | inventory:write | Bestandskorrektur (set/add/subtract) |
| Verkäufe (Read-Only) |
| GET | /orders | orders:read | Verkaufsliste |
| GET | /orders/{id} | orders:read | Einzelner Verkauf mit Positionen |
| Kunden |
| GET | /customers | customers:read | Kundenliste |
| GET | /customers/{id} | customers:read | Einzelner Kunde |
| POST | /customers | customers:write | Kunde erstellen |
| PUT | /customers/{id} | customers:write | Kunde aktualisieren |
| DEL | /customers/{id} | customers:write | DSGVO-Anonymisierung |
| Webhooks |
| GET | /webhooks | webhooks:manage | Webhook-Liste |
| POST | /webhooks | webhooks:manage | Webhook erstellen |
| PUT | /webhooks/{id} | webhooks:manage | Webhook aktualisieren |
| DEL | /webhooks/{id} | webhooks:manage | Webhook löschen |
| POST | /webhooks/{id}/test | webhooks:manage | Test-Event senden |
Produkte
GET /products
Produktliste mit Pagination und Filtern.
| Parameter | Typ | Beschreibung |
page | int | Seitennummer (Default: 1) |
per_page | int | Ergebnisse pro Seite (Default: 50, Max: 200) |
since | ISO 8601 | Nur Änderungen seit Zeitstempel (Delta-Sync) |
active | bool | Nur aktive Produkte filtern |
category_id | string | Nach Kategorie filtern |
sku | string | Exakte Suche nach Artikelnummer (SKU) |
ean | string | Exakte Suche nach EAN/GTIN-Barcode |
search | string | Volltextsuche in Name, SKU, EAN |
POST /products
Neues Produkt erstellen.
Request
{
"sku": "BIS-042",
"name": "Tom Yum Suppe",
"selling_price": 8.50,
"tax_rate": 19,
"category_id": "cat_abc123",
"stock_quantity": 50
}
POST /products/batch
Bis zu 500 Produkte in einem Request erstellen oder aktualisieren.
Request
{
"items": [
{ "action": "upsert", "sku": "BIS-001", "name": "Pad Thai", "selling_price": 12.90 },
{ "action": "upsert", "sku": "BIS-002", "name": "Green Curry", "selling_price": 11.50 }
]
}
Kategorien
Kategorien verwalten die hierarchische Produktstruktur. Kompatibel mit JTL-Wawi, Lexware und anderen ERP-Systemen.
GET /categories
Kategorien auflisten mit optionalen Filtern.
| Parameter | Typ | Beschreibung |
since | ISO 8601 | Nur geänderte Kategorien |
active | bool | Nur aktive/inaktive Kategorien |
parent_id | UUID | Unterkategorien einer bestimmten Kategorie |
root | bool | Nur Hauptkategorien (ohne Elternkategorie) |
POST /categories
Neue Kategorie erstellen.
Request
{
"name": "Getränke",
"parent_id": null,
"sort_order": 1,
"description": "Alle Getränkeprodukte"
}
PUT /categories/{id}
Kategorie aktualisieren. Nur übergebene Felder werden geändert.
DEL /categories/{id}
Kategorie löschen. Produkte werden automatisch aus der Kategorie entfernt. Kategorien mit Unterkategorien können nicht gelöscht werden.
Bestandsverwaltung
GET /inventory
Aktuelle Bestandslevel aller Produkte. Mit low_stock=true nur Artikel unter Mindestbestand.
POST /inventory/adjustments
Bestandskorrekturen durchführen.
| Typ | Beschreibung |
set | Absolut setzen (Inventur) |
add | Hinzufügen (Wareneingang) |
subtract | Abziehen (Schwund, Korrektur) |
Request
{
"adjustments": [
{ "product_id": "art_abc123", "type": "add", "quantity": 50, "reason": "Wareneingang" },
{ "product_id": "art_def456", "type": "set", "quantity": 0, "reason": "Inventur" }
]
}
Verkäufe
Nur lesend: Verkäufe können über die API nicht erstellt oder geändert werden (KassenSichV-Konformität). Verkäufe werden ausschließlich über die POS-App erstellt.
GET /orders
Verkaufsliste mit Filtern: since (Delta-Sync), until, device_id.
GET /orders/{id}
Einzelner Verkauf mit allen Positionen, MwSt-Aufschlüsselung, Zahlungsart und TSE-Daten.
Response 200
{
"id": "sale_xyz789",
"receipt_number": "2026-00142",
"total_gross": 25.80,
"payment_method": "CASH",
"items": [
{ "name": "Pad Thai", "quantity": 2, "unit_price": 12.90 }
],
"tse": {
"transaction_number": 4217,
"signature": "MEUCIQDx...",
"serial_number": "abc123def..."
}
}
Kunden
Vollständiges CRUD für Kundenstammdaten.
POST /customers
Request
{
"first_name":
"Max",
"last_name":
"Mustermann",
"email":
"[email protected]",
"type":
"business",
"company":
"Mustermann GmbH",
"vat_id":
"DE123456789"
}
DEL /customers/{id}
DSGVO Art. 17: DELETE anonymisiert die Kundendaten (Name, E-Mail, Adresse werden überschrieben). Verkaufshistorie bleibt erhalten, aber ohne Personenbezug.
Webhooks
Echtzeit-Benachrichtigungen per HTTP POST wenn auf der Kasse etwas passiert.
Verfügbare Events
| Event | Auslöser |
sale.completed | Verkauf abgeschlossen |
sale.cancelled | Stornierung durchgeführt |
inventory.adjusted | Bestandsänderung (Verkauf, Import, Korrektur) |
product.created | Neues Produkt angelegt |
product.updated | Produkt geändert |
product.deleted | Produkt gelöscht |
customer.created | Neuer Kunde angelegt |
customer.updated | Kundendaten geändert |
customer.deleted | Kunde anonymisiert (DSGVO) |
cash_closing.completed | Kassenschluss (Z-Bon) erstellt |
Signatur-Verifizierung
Jeder Webhook wird mit HMAC-SHA256 signiert. Verifizieren Sie die Signatur im Header X-BISpicy-Webhook-Signature:
Python
import hmac, hashlib
def verify_webhook(payload_body, signature_header, secret):
timestamp, received_sig = signature_header.split(",")
ts = timestamp.split("=")[1]
sig = received_sig.split("=", 1)[1]
expected = hmac.new(
secret.encode('utf-8'),
f"{ts}.{payload_body}".encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, sig)
Retry-Verhalten
Bei Fehlern (kein 2xx-Response) wird die Zustellung mit exponentiellem Backoff wiederholt:
| Versuch | Wartezeit |
| 1 | 1 Minute |
| 2 | 5 Minuten |
| 3 | 30 Minuten |
| 4 | 2 Stunden |
| 5 | 6 Stunden |
| 6 | 12 Stunden |
| 7 | 24 Stunden |
| 8 (letzter) | Dead Letter Queue |
Circuit Breaker: Nach 5 aufeinanderfolgenden Fehlern wird die Zustellung für 1 Stunde pausiert. Fehlgeschlagene Deliveries landen in der Dead-Letter-Queue und können manuell wiederholt werden.
Rate Limiting
| Tier | Requests/Minute | Requests/Tag |
| Basic | 60 | 5.000 |
| Standard | 300 | 50.000 |
| Premium | 1.000 | 200.000 |
Jede Response enthält Rate-Limit-Header:
Response Headers
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 247
X-RateLimit-Reset: 1709390460
Bei Überschreitung: 429 Too Many Requests mit Retry-After Header.
Fehlerbehandlung
HTTP-Statuscodes
| Code | Bedeutung |
| 200 | OK - Erfolgreicher GET, PUT, PATCH, DELETE |
| 201 | Created - Erfolgreicher POST |
| 400 | Bad Request - Ungültiges JSON, fehlende Pflichtfelder |
| 401 | Unauthorized - API-Key ungültig/abgelaufen, HMAC falsch |
| 403 | Forbidden - Fehlender Scope, IP nicht erlaubt |
| 404 | Not Found - Ressource existiert nicht |
| 409 | Conflict - Duplikat (SKU/EAN), Idempotency-Conflict |
| 422 | Unprocessable - Validierungsfehler |
| 429 | Too Many Requests - Rate Limit überschritten |
| 500 | Server Error - Bitte mit request_id melden |
Error Response Format
JSON
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Ungültige Eingabedaten",
"details": [
{ "field": "selling_price", "message": "Muss eine positive Zahl sein" }
]
},
"request_id": "req_a1b2c3d4e5f6"
}
Error Codes
| Code | HTTP | Beschreibung |
AUTHENTICATION_REQUIRED | 401 | Kein Authorization-Header |
INVALID_API_KEY | 401 | API-Key ungültig oder deaktiviert |
INVALID_SIGNATURE | 401 | HMAC-Signatur stimmt nicht |
TIMESTAMP_EXPIRED | 401 | Timestamp älter als 5 Minuten |
INSUFFICIENT_SCOPE | 403 | API-Key hat nicht den nötigen Scope |
IP_NOT_ALLOWED | 403 | IP nicht in Whitelist |
VALIDATION_ERROR | 422 | Feldvalidierung fehlgeschlagen |
RATE_LIMIT_EXCEEDED | 429 | Rate Limit überschritten |
Code-Beispiele
Python
Python
import hmac, hashlib, time, json, uuid, requests
API_KEY = "bsk_live_a1b2c3d4..."
API_SECRET = "bss_a1b2c3d4..."
BASE_URL = "https://bispicy.com/api/v1"
def api_request(method, endpoint, payload=None):
timestamp = str(int(time.time()))
body = json.dumps(payload, separators=(',',':')) if payload else ""
signed = f"{timestamp}.{body}"
sig = hmac.new(API_SECRET.encode(), signed.encode(), hashlib.sha256).hexdigest()
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"X-BISpicy-Timestamp": timestamp,
"X-BISpicy-Signature": f"v1={sig}",
"Idempotency-Key": str(uuid.uuid4())
}
resp = requests.request(method, f"{BASE_URL}{endpoint}", headers=headers, json=payload)
return resp.json()
# Produkte abrufen
products = api_request("GET", "/products?per_page=100")
# Bestand korrigieren
api_request("POST", "/inventory/adjustments", {
"adjustments": [{"product_id": "art_abc", "type": "add", "quantity": 50}]
})
PHP
PHP
<?php
$apiKey = 'bsk_live_a1b2c3d4...';
$apiSecret = 'bss_a1b2c3d4...';
$baseUrl = 'https://bispicy.com/api/v1';
function apiRequest($method, $endpoint, $payload = null) {
global $apiKey, $apiSecret, $baseUrl;
$timestamp = (string) time();
$body = $payload ? json_encode($payload, JSON_UNESCAPED_UNICODE) : '';
$signature = hash_hmac('sha256', "{$timestamp}.{$body}", $apiSecret);
$ch = curl_init("{$baseUrl}{$endpoint}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$apiKey}",
"Content-Type: application/json",
"X-BISpicy-Timestamp: {$timestamp}",
"X-BISpicy-Signature: v1={$signature}",
"Idempotency-Key: " . bin2hex(random_bytes(16))
],
CURLOPT_POSTFIELDS => $body ?: null
]);
return json_decode(curl_exec($ch), true);
}
// Produkte abrufen
$products = apiRequest('GET', '/products');
// Neues Produkt erstellen
apiRequest('POST', '/products', [
'sku' => 'MEIN-001', 'name' => 'Neuer Artikel', 'selling_price' => 9.99
]);
Node.js
JavaScript
import crypto from 'crypto';
const API_KEY = 'bsk_live_a1b2c3d4...';
const API_SECRET = 'bss_a1b2c3d4...';
const BASE_URL = 'https://bispicy.com/api/v1';
async function apiRequest(method, endpoint, payload) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = payload ? JSON.stringify(payload) : '';
const sig = crypto.createHmac('sha256', API_SECRET)
.update(`${timestamp}.${body}`).digest('hex');
const res = await fetch(`${BASE_URL}${endpoint}`, {
method,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'X-BISpicy-Timestamp': timestamp,
'X-BISpicy-Signature': `v1=${sig}`,
'Idempotency-Key': crypto.randomUUID()
},
body: body || undefined
});
return res.json();
}
// Produkte abrufen
const products = await apiRequest('GET', '/products');
// Webhook registrieren
await apiRequest('POST', '/webhooks', {
target_url: 'https://mein-server.de/webhook',
events: ['sale.completed', 'inventory.adjusted']
});
cURL
Bash
# Produkte auflisten
curl -X GET "https://bispicy.com/api/v1/products?per_page=100" \
-H "Authorization: Bearer bsk_live_a1b2c3d4..."
# Einzelnes Produkt abrufen
curl -X GET "https://bispicy.com/api/v1/products/art_abc123" \
-H "Authorization: Bearer bsk_live_a1b2c3d4..."
# Verkäufe seit gestern abrufen
curl -X GET "https://bispicy.com/api/v1/orders?since=2026-03-01T00:00:00Z" \
-H "Authorization: Bearer bsk_live_a1b2c3d4..."