Schnellstart
- Im Partnerportal → Entwickler einen Testschlüssel erzeugen. Du bekommst einmalig den API-Schlüssel und das Signatur-Geheimnis.
- Eine Testbuchung senden (Beispiel unten). Testbuchungen vergeben keine echten Punkte und kosten nichts.
- Nach der Freischaltung einen Live-Schlüssel pro Kasse oder Filiale erzeugen und einbauen.
# Testbuchung: 25,40 € Einkauf auf Kundenkarte 9990 1234 5678 9018
BODY='{"card":"9990123456789018","receipt_id":"K1-000481","amount":25.40}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$BACKPERK_SECRET" | awk '{print $NF}')
curl https://backperk.com/api/partner/v1/transactions \
-H "Authorization: Bearer $BACKPERK_KEY" \
-H "Content-Type: application/json" \
-H "BackPerk-Signature: t=$T,v1=$SIG" \
-d "$BODY"
Grundlagen
| Basis-Adresse | https://www.backperk.com/api/partner/v1 |
|---|---|
| Format | JSON in UTF-8, Header Content-Type: application/json und Accept: application/json |
| Verschlüsselung | nur HTTPS (TLS 1.2+). Aufrufe über HTTP werden umgeleitet und dürfen nicht genutzt werden. |
| Beträge | in Euro als Zahl mit höchstens 2 Nachkommastellen, z. B. 25.40. Antworten enthalten Euro-Werte als Zahl (cost_eur, balance_eur). |
| Punkte | ganze Zahlen. 100 Punkte = 1 €. |
| Zeitangaben | ISO 8601 mit Zeitzone, z. B. 2026-11-07T15:42:10+01:00 |
| IDs | Buchungen tx_…, Einlösungen rd_…, Coupons cp_… – als Text behandeln, Länge kann sich ändern. |
| Versionierung | Neue Felder in Antworten können jederzeit dazukommen – unbekannte Felder bitte ignorieren. Inkompatible Änderungen gibt es nur mit neuer Version (/v2). |
Anmeldung & Schlüssel
Jeder Aufruf trägt den API-Schlüssel als Bearer-Token:
Authorization: Bearer bp_live_Ab3dE6gH_9f8e7d6c5b4a39281706f5e4d3c2b1a0
bp_test_…= Testumgebung,bp_live_…= echte Buchungen (erst nach Freischaltung).- Ein Schlüssel kann an eine Filiale gebunden werden – dann gelten alle Buchungen automatisch für diese Filiale und
store_idwird ignoriert. Empfehlung: ein Schlüssel pro Kasse. - Optional eine IP-Freigabeliste (kommagetrennt). Aufrufe von anderen IPs:
403 ip_not_allowed. - Schlüssel und Geheimnis werden nur einmal angezeigt; BackPerk speichert den Schlüssel nur als Hash. Bei Verlust: widerrufen und neu erzeugen.
- Schlüssel gehören auf den Server bzw. in die Kasse, nie in eine Website, App oder ein öffentliches Repository.
Signatur
Alle schreibenden Aufrufe (POST) müssen zusätzlich signiert werden. So kann ein abgefangener Aufruf nicht verändert oder später wiederholt werden.
BackPerk-Signature: t=<Unix-Zeit in Sekunden>,v1=<Signatur>
Signatur = HEX( HMAC-SHA256( Signatur-Geheimnis, t + "." + Rohdaten ) )
- Rohdaten = exakt die Bytes, die du als Body sendest. Erzeuge den JSON-Text einmal, signiere ihn und sende genau diesen Text – nicht erneut serialisieren.
- Der Zeitstempel darf höchstens 5 Minuten von der Serverzeit abweichen → Uhr der Kasse per NTP synchron halten.
- Hex in Kleinbuchstaben (64 Zeichen). GET-Aufrufe brauchen keine Signatur.
- Fehlt die Signatur, ist sie falsch oder zu alt:
401 invalid_signature.
Doppelte Aufrufe (Idempotenz)
Netzwerke sind unzuverlässig – eine Kasse soll einen Aufruf nach einem Timeout gefahrlos wiederholen können. Deshalb gilt:
- Jede Buchung trägt deine Beleg-ID (
receipt_id), eindeutig je Partner, z. B. Kassen-Nr. + Bon-Nr. - Gleiche
receipt_idmit gleichen Daten nochmal gesendet → keine zweite Buchung, Antwort200mit der ursprünglichen Buchung (statt201). - Gleiche
receipt_idmit anderer Karte oder anderem Betrag →409 receipt_conflict. - Beim Einlösen gilt dasselbe:
capturemit gleichem Code und gleicherreceipt_idliefert dieselbe Antwort; ein anderer Beleg bekommt409 already_used.
5xx denselben Aufruf mit derselben receipt_id bis zu 3-mal wiederholen (nach 1 s, 5 s, 30 s). Bei 4xx nicht wiederholen, sondern die Meldung anzeigen.Karten & Codes erkennen
Kunden zeigen zwei verschiedene Dinge vor. Beide sind reine Ziffernfolgen mit Luhn-Prüfziffer (letzte Stelle) und als QR-Code, Strichcode (Code 128) oder zum Eintippen verfügbar:
| Beginnt mit | Länge | Was | Verwenden für |
|---|---|---|---|
999 | 16 Ziffern | Kundenkarte (fest, ändert sich nicht) | /cards/lookup, /transactions – nur Punkte sammeln |
998 | 13 Ziffern | Einlöse-Code (Einmal-Code, 10 Minuten gültig) | /redemptions/lookup, /redemptions/capture – Punkte einlösen |
Leerzeichen und Bindestriche werden ignoriert (9990 1234 5678 9018 ist gültig). So kann deine Kasse am Präfix automatisch entscheiden, welcher Ablauf startet. Eine Kundenkarte kann nie Punkte abbuchen.
function backperkType(string $scan): ?string {
$d = preg_replace('/\D/', '', $scan);
if (strlen($d) === 16 && str_starts_with($d, '999')) return 'card'; // Punkte sammeln
if (strlen($d) === 13 && str_starts_with($d, '998')) return 'redemption'; // Punkte einlösen
return null; // kein BackPerk-Code
}
Ablauf: Punkte sammeln
- Kassierer scannt die Kundenkarte (
999…). - Optional:
POST /cards/lookup→ Kürzel „Julia M.“ und aktive Coupons anzeigen. - Nach dem Bezahlen:
POST /transactionsmit Betrag, Beleg-ID und – empfohlen – dem Warenkorb (items). receipt_textaus der Antwort auf den Bon drucken, z. B. „Sie erhalten 26 BackPerk-Punkte“.
Status der Buchung: available = Punkte sofort beim Kunden. held = dein Guthaben reicht nicht, die Punkte stehen beim Kunden „in Prüfung“ und werden nach dem Aufladen automatisch gutgeschrieben (Bon-Text: „Punkte werden nachgebucht“). Die Kasse muss in beiden Fällen nichts weiter tun.
Berechnung: Grundpunkte = abgerundet(punkteberechtigter Betrag × Grundvergütung); dazu kommt der beste aktivierte Coupon. Kosten = Punktwert + Servicegebühr (Standard 20 % auf den Punktwert) und werden sofort vom Guthaben abgezogen. Waren ohne Punkte (Tabak, Pfand, Gutscheine) über eligible_amount herausrechnen.
Ablauf: Punkte einlösen
- Kunde erzeugt in seinem Konto einen Einlöse-Code (
998…, mit PIN bestätigt) und zeigt ihn vor. POST /redemptions/lookup→ „200 Punkte = 2,00 € einlösbar“ anzeigen. Bucht noch nichts.POST /redemptions/capturemit Rechnungsbetrag und Beleg-ID. Abgezogen wird höchstens der reservierte Wert und höchstens die Rechnungssumme; ein Rest geht automatisch zurück zum Kunden.- Den Wert aus
value_eurals Zahlungsart „BackPerk-Punkte“ von der Rechnung abziehen. Kein Bargeld, kein Wechselgeld.
Der Wert wird deinem Guthaben ohne Gebühr gutgeschrieben. Einlösen geht nur online und nicht mit Testschlüsseln. Auf den mit Punkten bezahlten Teil vergibst du keine neuen Punkte – sende bei /transactions als eligible_amount nur den Restbetrag.
Warenkorb & Produkt-Coupons
Sendet deine Kasse bei /transactions den Warenkorb (items) mit, kann BackPerk Coupons auf einzelne Kategorien, Marken oder Artikel selbst ausrechnen – z. B. „10-fach Punkte auf Getränke > Wasser“ oder „+50 Punkte pro Kiste Gerolsteiner“. Die Kasse muss Coupons dafür nicht kennen.
- Kategorie = deine Kassen-Kategorie als Text, gern als Pfad mit
>, z. B.Getränke > Wasser. Ein Coupon auf „Getränke“ gilt auch für alle Unterkategorien; ein Coupon auf „Wasser“ passt auf die letzte Stufe. Groß-/Kleinschreibung ist egal. - Marke = Herstellername, z. B.
Gerolsteiner. - Artikel = deine Artikelnummer (
sku) oder die EAN (gtin). - Positionen mit
"eligible": false(Tabak, Pfand, Gutscheine, mit Punkten bezahlter Teil) bekommen weder Grund- noch Coupon-Punkte. Mit Warenkorb wird der punkteberechtigte Betrag aus den Positionen berechnet;eligible_amountwird dann ignoriert. - Die Summe aller
totalmuss zumamountpassen (±5 Cent), sonst422 items_mismatch. - Unbekannte Artikel zählen ganz normal für die Grundpunkte.
items – gelten nur Coupons auf den ganzen Einkauf. Coupons auf Kategorien, Marken oder Artikel und „pro Stück“ werden dann nicht eingerechnet. Ob ein Coupon den Warenkorb braucht, steht bei GET /coupons im Feld requires_basket.Berechnung (abgerundet auf ganze Punkte, Grundvergütung im Beispiel 1 P/€):
| Coupon-Art | Extrapunkte |
|---|---|
| x-fach auf Kategorie/Marke/Artikel | passender Betrag × Grundvergütung × (Faktor − 1) |
| Extrapunkte pro € | passender Betrag × Wert |
| Extrapunkte pro Stück | Menge der passenden Positionen × Wert |
| Extrapunkte fest | Wert, sobald mindestens ein passender Artikel im Warenkorb ist |
Es zählt immer nur der beste Coupon je Einkauf. Beispiel „10-fach auf Getränke > Wasser“: 3 × Wasser à 0,79 € und Brot 3,20 € → Grundpunkte auf 5,57 € = 5 P, Extrapunkte auf das Wasser 2,37 € × 1 × 9 = 21 P. Der Kunde bekommt 26 Punkte; vom Guthaben gehen 0,26 € + 20 % = 0,31 € ab.
Endpunkte
POST/cards/lookup | Kundenkarte prüfen |
POST/transactions | Einkauf buchen (Punkte vergeben) |
GET/transactions | Buchungen abrufen |
POST/transactions/{id}/refunds | Retoure |
POST/redemptions/lookup | Einlöse-Code prüfen |
POST/redemptions/capture | Einlöse-Code verbrauchen |
POST/redemptions/{id}/refunds | Einlösung zurückgeben |
GET/balance | Guthaben |
GET/stores | Filialen |
GET/coupons | Coupons |
POST/cards/lookup
Prüft eine Kundenkarte. Liefert nur ein Kürzel zur Bestätigung – nie Name, E-Mail oder Punktestand.
| Feld | Typ | Beschreibung |
|---|---|---|
card | Text, Pflicht | Kartennummer (16 Ziffern, beginnt mit 999) |
200 OK
{
"valid": true,
"customer_token": "4be2a91c07de55f1a0c38e7d", // stabil je Karte und Partner, z. B. für eigene Statistik
"display_name": "Julia M.",
"coupons": [ { "id": "cp_12", "label": "5-fach Punkte", "title": "Samstags-Aktion" } ]
}
404 Not Found
{ "valid": false, "error": "card_invalid", "message": "Karte unbekannt oder gesperrt." }
POST/transactions
Bucht einen Einkauf und vergibt Punkte. Idempotent über receipt_id.
| Feld | Typ | Beschreibung |
|---|---|---|
card | Text, Pflicht | Kartennummer |
receipt_id | Text, Pflicht, max. 64 | deine eindeutige Beleg-ID |
amount | Zahl, Pflicht | Gesamtbetrag in €, 0,01–10.000 |
eligible_amount | Zahl, optional | punkteberechtigter Betrag (Standard = amount), höchstens amount |
currency | Text, optional | nur EUR |
occurred_at | Zeit, optional | Zeitpunkt des Einkaufs (Standard: jetzt); bis 72 Stunden rückwirkend, z. B. nach Offline-Betrieb |
store_id | Zahl, optional | Filiale per BackPerk-ID (siehe /stores); entfällt bei filialgebundenem Schlüssel |
store_ref | Text, optional | … oder per deiner eigenen Filialnummer (im Portal bei der Filiale hinterlegt, z. B. 4711). Unbekannt → 422 invalid_store |
items | Liste, optional | Warenkorb, bis 500 Positionen – siehe Warenkorb |
items[].total | Zahl, Pflicht je Position | Gesamtpreis der Position in € (nach Rabatt) |
items[].quantity | Zahl, optional | Menge (Standard 1), auch Kommazahlen für Gewicht |
items[].unit_price | Zahl, optional | Einzelpreis in € |
items[].sku / gtin | Text, optional | deine Artikelnummer / EAN |
items[].name | Text, optional | Artikelbezeichnung (für den digitalen Bon) |
items[].category | Text, optional | Kassen-Kategorie, z. B. Getränke > Wasser |
items[].brand | Text, optional | Marke |
items[].eligible | true/false, optional | false = keine Punkte für diese Position (Standard true) |
POST /api/partner/v1/transactions
{ "card": "9990123456789018", "receipt_id": "K3-2026-000481", "amount": 80.00, "eligible_amount": 76.50 }
201 Created (bei Wiederholung derselben receipt_id: 200 OK mit identischem Inhalt)
{
"id": "tx_5hd91k2m8qzc",
"receipt_id": "K3-2026-000481",
"status": "available", // oder "held" = in Prüfung bis zum Aufladen
"points": 380,
"refunded_points": 0,
"breakdown": [
{ "type": "base", "points": 76 },
{ "type": "coupon", "coupon_id": "cp_12", "label": "5-fach Punkte", "points": 304 }
],
"items": [], // Positionen mit Coupon-Punkten (nur mit Warenkorb)
"cost_eur": 4.56, // vom Guthaben abgezogen (Punktwert + Gebühr)
"balance_eur": 195.44, // Guthaben danach (null bei Testschlüssel)
"test": false,
"receipt_text": "Sie erhalten 380 BackPerk-Punkte",
"occurred_at": "2026-11-07T15:42:10+01:00"
}
Mit Warenkorb:
POST /api/partner/v1/transactions
{
"card": "9990123456789018",
"receipt_id": "K3-2026-000483",
"amount": 5.57,
"items": [
{ "sku": "4711-05", "gtin": "4008501011009", "name": "Mineralwasser still 1,5 l",
"category": "Getränke > Wasser", "brand": "Gerolsteiner", "quantity": 3, "unit_price": 0.79, "total": 2.37 },
{ "sku": "BR-002", "name": "Bauernbrot", "category": "Backwaren", "quantity": 1, "total": 3.20 }
]
}
201 Created
{
"id": "tx_8m2q0a7c4kpe", "status": "available", "points": 26,
"breakdown": [
{ "type": "base", "points": 5 },
{ "type": "coupon", "coupon_id": "cp_31", "label": "10-fach Punkte auf Getränke > Wasser", "points": 21 }
],
"items": [ { "sku": "4711-05", "gtin": "4008501011009", "name": "Mineralwasser still 1,5 l", "coupon_points": 21 } ],
"cost_eur": 0.31, "receipt_text": "Sie erhalten 26 BackPerk-Punkte", …
}
GET/transactions
Buchungen ab einem Zeitpunkt, z. B. für den Kassenabschluss. Parameter from (Datum/Zeit, Standard: heute 0 Uhr). Höchstens 500 Einträge, älteste zuerst. Testschlüssel sehen nur Testbuchungen, Live-Schlüssel nur echte.
GET /api/partner/v1/transactions?from=2026-11-07
200 OK
{ "data": [ { "id": "tx_…", "receipt_id": "…", "status": "available", "points": 380, … } ] }
POST/transactions/{id}/refunds
Retoure innerhalb von 30 Tagen. Ohne amount = ganze Buchung; mit amount (in €) anteilig. Die Punkte werden beim Kunden abgezogen, du bekommst Punktwert und Gebühr anteilig zurück. Buchungen in Prüfung (held) können nur ganz storniert werden.
POST /api/partner/v1/transactions/tx_5hd91k2m8qzc/refunds
{ "amount": 40.00 }
200 OK
{ "refunded_points": 190, "transaction": { "id": "tx_5hd91k2m8qzc", "refunded_points": 190, "status": "available", … } }
Nach vollständiger Retoure hat die Buchung den Status cancelled.
POST/redemptions/lookup
Prüft einen Einlöse-Code, ohne etwas zu buchen.
POST /api/partner/v1/redemptions/lookup
{ "code": "9981234567891" }
200 OK
{ "id": "rd_7k2p0m4x9qa1", "status": "reserved", "points": 200, "value_eur": 2.0, "expires_at": "2026-11-07T15:52:10+01:00" }
POST/redemptions/capture
| Feld | Typ | Beschreibung |
|---|---|---|
code | Text, Pflicht | Einlöse-Code (13 Ziffern, beginnt mit 998) |
invoice_amount | Zahl, Pflicht | Rechnungsbetrag in € (vor Abzug der Punkte) |
receipt_id | Text, Pflicht | deine Beleg-ID |
store_id / store_ref | optional | Filiale per BackPerk-ID oder eigener Filialnummer |
POST /api/partner/v1/redemptions/capture
{ "code": "9981234567891", "invoice_amount": 1.50, "receipt_id": "K3-2026-000482" }
200 OK
{ "id": "rd_7k2p0m4x9qa1", "status": "captured", "points": 150, "value_eur": 1.5,
"expires_at": "…", "receipt_text": "Mit BackPerk-Punkten bezahlt: 1,50 €" }
// 200 reservierte Punkte, Rechnung nur 1,50 € → 150 Punkte verbraucht, 50 gehen zurück an den Kunden
POST/redemptions/{id}/refunds
Wird ein mit Punkten bezahlter Einkauf zurückgegeben, gehen die Punkte zurück aufs Kundenkonto (nie bar) und der Wert wird von deinem Guthaben abgezogen. Body leer ({}), Signatur wie bei allen POST-Aufrufen.
200 OK
{ "refunded_points": 150 }
GET/balance
200 OK
{
"balance_eur": 195.44,
"low_balance_eur": 25.0, // Warnschwelle aus dem Portal
"held_count": 0, // Buchungen in Prüfung
"held_cost_eur": 0.0, // so viel fehlt, um sie freizugeben
"bookings_blocked": false, // true = neue Buchungen werden mit 402 abgelehnt
"topup_reference": "BP-H-48213QX" // Verwendungszweck zum Aufladen
}
Tipp: Bei Kassenstart /balance abfragen und bei bookings_blocked oder niedrigem Guthaben einen Hinweis anzeigen.
GET/stores
200 OK
{ "data": [ { "id": 3, "ref": "4711", "name": "Hauptgeschäft", "address": "Markt 1, 10115 Berlin", "base_rate": 1.0, "active": true, "open_now": true } ] }
ref = deine interne Filialnummer (Kunden sehen sie nie). open_now = laut hinterlegten Öffnungszeiten (inkl. Feiertagen und Sondertagen) geöffnet (null, wenn keine hinterlegt sind). Viele Filialen legst du im Portal per CSV-Import an.
GET/coupons
Deine Coupons (anlegen im Portal). BackPerk rechnet Coupons bei der Buchung selbst ein – die Kasse muss sie nicht kennen.
200 OK
{ "data": [
{ "id": "cp_12", "title": "Samstags-Aktion", "label": "5-fach Punkte", "type": "multiplier", "value": 5.0,
"target_type": "purchase", "target_values": [], "requires_basket": false, "stores": [], "status": "active", "starts_at": "…", "ends_at": "…" },
{ "id": "cp_31", "title": "Wasser-Woche", "label": "10-fach Punkte auf Getränke > Wasser", "type": "multiplier", "value": 10.0,
"target_type": "category", "target_values": ["Getränke > Wasser"], "requires_basket": true,
"stores": [ { "id": 3, "ref": "4711" } ], "status": "active", "starts_at": "…", "ends_at": "…" }
] }
recurrence + per_customer: wie oft ein Kunde den Coupon nutzen darf – none = insgesamt, daily/weekly/monthly = je Tag/Woche/Monat wieder neu. weekdays (mon…sun, leer = alle) und time_from/time_to (auch über Mitternacht, z. B. 22:00–02:00) begrenzen, wann er gilt; maßgeblich ist occurred_at der Buchung.
stores leer = Coupon gilt in allen Filialen; sonst nur in den genannten. Coupons mit Filialauswahl werden bei Buchungen ohne Filiale nicht eingerechnet – sende deshalb immer store_id oder store_ref (oder nutze filialgebundene Schlüssel).
type: multiplier, fixed, per_euro, per_item, welcome · target_type: purchase (ganzer Einkauf), category, brand, product.
Fehlercodes
Fachliche Fehler haben immer dieses Format – werte error aus und zeige message (Deutsch) dem Kassierer:
{ "error": "insufficient_balance", "message": "Dieser Partner vergibt derzeit keine Punkte (Guthaben aufladen)." }
Fehlerhafte Eingaben liefern 422 im Laravel-Format: { "message": "…", "errors": { "amount": ["…"] } }.
| HTTP | error | Bedeutung · was tun |
|---|---|---|
401 | unauthorized | Schlüssel fehlt, falsch oder widerrufen. |
401 | invalid_signature | Signatur fehlt, falsch oder älter als 5 Minuten – Uhrzeit und Rohdaten prüfen. |
402 | insufficient_balance | Neue Buchungen gesperrt, weil seit 7 Tagen Punkte auf Guthaben warten. Guthaben aufladen. |
403 | ip_not_allowed | Aufruf von nicht freigegebener IP. |
403 | merchant_inactive | Partnerkonto nicht aktiv (in Prüfung, pausiert oder gesperrt). |
403 | own_card | Karte gehört einem Mitarbeiter deines Teams – nicht erlaubt. |
403 | merchant_no_redemptions | Einlösungen sind bei dir nicht aktiviert (Portal → Konditionen). |
403 | wrong_merchant | Der Kunde hat den Code für einen anderen Partner erstellt. |
404 | card_invalid | Karte unbekannt oder gesperrt. |
404 | code_invalid / code_not_found | Einlöse-Code ungültig (Tippfehler, falsches Präfix). |
409 | receipt_conflict | Beleg-ID schon mit anderen Daten gebucht. |
409 | possible_duplicate | Sieht nach einer doppelten Buchung aus (Doppel-Scan?). Nur nach Rückfrage beim Kunden mit neuer Beleg-ID erneut senden. |
409 | already_used | Einlöse-Code wurde bereits verwendet. |
409 | already_refunded / not_refundable | Retoure bereits erfolgt oder nicht möglich. |
410 | code_expired | Einlöse-Code abgelaufen oder vom Kunden storniert – Kunde soll einen neuen erstellen. |
422 | invalid_amount / invalid_time / invalid_store | Betrag, Zeitpunkt (max. 72 h zurück) oder Filiale ungültig. |
422 | items_mismatch | Summe der Warenkorb-Positionen passt nicht zum Gesamtbetrag (±5 Cent). |
422 | monthly_limit | Monatslimit für vergebene Punkte erreicht – BackPerk kontaktieren. |
422 | refund_window_closed / held_partial_refund | Retoure nach 30 Tagen bzw. Teilretoure einer Buchung in Prüfung. |
422 | test_mode | Einlösen ist mit Testschlüsseln nicht möglich. |
429 | card_limit | Für diese Karte ist heute keine weitere Buchung möglich. Meldung anzeigen, nicht wiederholen. |
429 | – | Zu viele Aufrufe – Header Retry-After beachten. |
500 | – | Fehler bei BackPerk – mit derselben receipt_id wiederholen. |
409 possible_duplicate, 429 card_limit), zeige die Meldung an und buche nicht automatisch erneut. Einzelheiten dieser Prüfungen veröffentlichen wir nicht.Testumgebung
- Gleiche Adressen, aber mit
bp_test_…-Schlüssel. Testschlüssel gibt es schon vor der Freischaltung. - Testbuchungen verwenden echte Kundenkarten (z. B. deine eigene von einem Kundenkonto, das nicht in deinem Team ist), vergeben aber keine Punkte und kosten kein Guthaben. In Antworten steht
"test": true. - Retouren und
GET /transactionsfunktionieren auch im Test. Einlösen (capture) ist im Test gesperrt (422 test_mode) –lookupgeht.
Code-Beispiele
PHP
function backperk(string $method, string $path, array $data = []): array
{
$key = getenv('BACKPERK_KEY'); // bp_live_…
$secret = getenv('BACKPERK_SECRET'); // Signatur-Geheimnis
$body = $method === 'GET' ? '' : json_encode($data, JSON_UNESCAPED_UNICODE);
$t = time();
$headers = ["Authorization: Bearer $key", 'Accept: application/json', 'Content-Type: application/json'];
if ($method !== 'GET') {
$headers[] = 'BackPerk-Signature: t='.$t.',v1='.hash_hmac('sha256', $t.'.'.$body, $secret);
}
$ch = curl_init('https://backperk.com/api/partner/v1'.$path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10, CURLOPT_POSTFIELDS => $method === 'GET' ? null : $body,
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
return ['status' => $status, 'data' => $response];
}
$r = backperk('POST', '/transactions', ['card' => '9990123456789018', 'receipt_id' => 'K1-481', 'amount' => 25.40]);
echo $r['data']['receipt_text'] ?? $r['data']['message'];
Python
import hashlib, hmac, json, os, time, requests
KEY, SECRET = os.environ["BACKPERK_KEY"], os.environ["BACKPERK_SECRET"]
BASE = "https://backperk.com/api/partner/v1"
def backperk_post(path, data):
body = json.dumps(data, separators=(",", ":")) # genau diesen Text senden
t = str(int(time.time()))
sig = hmac.new(SECRET.encode(), f"{t}.{body}".encode(), hashlib.sha256).hexdigest()
r = requests.post(BASE + path, data=body.encode(), timeout=10, headers={
"Authorization": f"Bearer {KEY}", "Content-Type": "application/json",
"Accept": "application/json", "BackPerk-Signature": f"t={t},v1={sig}",
})
return r.status_code, r.json()
status, res = backperk_post("/transactions", {"card": "9990123456789018", "receipt_id": "K1-481", "amount": 25.40})
print(res.get("receipt_text") or res.get("message"))
Node.js
import crypto from 'node:crypto';
const BASE = 'https://backperk.com/api/partner/v1';
export async function backperkPost(path, data) {
const body = JSON.stringify(data); // genau diesen Text senden
const t = Math.floor(Date.now() / 1000);
const sig = crypto.createHmac('sha256', process.env.BACKPERK_SECRET).update(`${t}.${body}`).digest('hex');
const res = await fetch(BASE + path, {
method: 'POST', body, signal: AbortSignal.timeout(10_000),
headers: {
Authorization: `Bearer ${process.env.BACKPERK_KEY}`, 'Content-Type': 'application/json',
Accept: 'application/json', 'BackPerk-Signature': `t=${t},v1=${sig}`,
},
});
return { status: res.status, data: await res.json() };
}
C# (.NET)
var body = JsonSerializer.Serialize(new { card = "9990123456789018", receipt_id = "K1-481", amount = 25.40m });
var t = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var sig = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{t}.{body}"))).ToLowerInvariant();
var req = new HttpRequestMessage(HttpMethod.Post, "https://backperk.com/api/partner/v1/transactions")
{
Content = new StringContent(body, Encoding.UTF8, "application/json")
};
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
req.Headers.Add("BackPerk-Signature", $"t={t},v1={sig}");
var res = await http.SendAsync(req);
Checkliste Livegang
- Uhr der Kasse per NTP synchron (Signatur ±5 Minuten).
receipt_idist je Bon eindeutig und wird bei Wiederholungen beibehalten.- Timeout ca. 10 Sekunden, Wiederholung nur bei Timeout/5xx mit derselben
receipt_id. - Präfix-Erkennung:
999…= sammeln,998…= einlösen. eligible_amountohne Tabak, Pfand, Gutscheine und ohne den mit Punkten bezahlten Teil – oder besser den Warenkorb miteligible: falsesenden.- Warenkorb mit Kategorie und Marke senden, damit Produkt-Coupons funktionieren; Summe der Positionen = Gesamtbetrag.
receipt_textwird auf den Bon gedruckt;heldführt nicht zum Abbruch.- Fehlermeldungen (
message) werden dem Kassierer angezeigt. - Retouren laufen über
/transactions/{id}/refundsbzw./redemptions/{id}/refunds. - Live-Schlüssel je Kasse, IP-Freigabe gesetzt, Schlüssel nur auf dem Server.
- Filiale bei jeder Buchung mitsenden (
store_refmit deiner Filialnummer genügt) – sonst greifen filialbezogene Coupons nicht.
Fragen zur Anbindung? Schreib uns über die Hilfe im Kundenkonto oder an mail@backperk.com.