BackPerk.com
Für Entwickler · Version 1

Kassen-API

Verbinde dein Kassen- oder Shopsystem mit BackPerk: Kundenkarten erkennen, Punkte vergeben, Einlöse-Codes verbuchen und Retouren abwickeln – sicher signiert und ohne doppelte Buchungen.

Schnellstart

  1. Im Partnerportal → Entwickler einen Testschlüssel erzeugen. Du bekommst einmalig den API-Schlüssel und das Signatur-Geheimnis.
  2. Eine Testbuchung senden (Beispiel unten). Testbuchungen vergeben keine echten Punkte und kosten nichts.
  3. 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-Adressehttps://backperk.com/api/partner/v1
FormatJSON in UTF-8, Header Content-Type: application/json und Accept: application/json
Verschlüsselungnur HTTPS (TLS 1.2+). Aufrufe über HTTP werden umgeleitet und dürfen nicht genutzt werden.
Beträgein Euro als Zahl mit höchstens 2 Nachkommastellen, z. B. 25.40. Antworten enthalten Euro-Werte als Zahl (cost_eur, balance_eur).
Punkteganze Zahlen. 100 Punkte = 1 €.
ZeitangabenISO 8601 mit Zeitzone, z. B. 2026-11-07T15:42:10+01:00
IDsBuchungen tx_…, Einlösungen rd_…, Coupons cp_… – als Text behandeln, Länge kann sich ändern.
VersionierungNeue 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_id wird 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_id mit gleichen Daten nochmal gesendet → keine zweite Buchung, Antwort 200 mit der ursprünglichen Buchung (statt 201).
  • Gleiche receipt_id mit anderer Karte oder anderem Betrag → 409 receipt_conflict.
  • Beim Einlösen gilt dasselbe: capture mit gleichem Code und gleicher receipt_id liefert dieselbe Antwort; ein anderer Beleg bekommt 409 already_used.
Empfehlung: Bei Timeout oder 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 mitLängeWasVerwenden für
99916 ZiffernKundenkarte (fest, ändert sich nicht)/cards/lookup, /transactions – nur Punkte sammeln
99813 ZiffernEinlö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

  1. Kassierer scannt die Kundenkarte (999…).
  2. Optional: POST /cards/lookup → Kürzel „Julia M.“ und aktive Coupons anzeigen.
  3. Nach dem Bezahlen: POST /transactions mit Betrag, Beleg-ID und – empfohlen – dem Warenkorb (items).
  4. receipt_text aus 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

  1. Kunde erzeugt in seinem Konto einen Einlöse-Code (998…, mit PIN bestätigt) und zeigt ihn vor.
  2. POST /redemptions/lookup → „200 Punkte = 2,00 € einlösbar“ anzeigen. Bucht noch nichts.
  3. POST /redemptions/capture mit Rechnungsbetrag und Beleg-ID. Abgezogen wird höchstens der reservierte Wert und höchstens die Rechnungssumme; ein Rest geht automatisch zurück zum Kunden.
  4. Den Wert aus value_eur als 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_amount wird dann ignoriert.
  • Die Summe aller total muss zum amount passen (±5 Cent), sonst 422 items_mismatch.
  • Unbekannte Artikel zählen ganz normal für die Grundpunkte.
Ohne Warenkorb – also bei Buchungen über die Web-Kasse im Partnerportal, die Händler-App oder die API ohne 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-ArtExtrapunkte
x-fach auf Kategorie/Marke/Artikelpassender Betrag × Grundvergütung × (Faktor − 1)
Extrapunkte pro €passender Betrag × Wert
Extrapunkte pro StückMenge der passenden Positionen × Wert
Extrapunkte festWert, 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/lookupKundenkarte prüfen
POST/transactionsEinkauf buchen (Punkte vergeben)
GET/transactionsBuchungen abrufen
POST/transactions/{id}/refundsRetoure
POST/redemptions/lookupEinlöse-Code prüfen
POST/redemptions/captureEinlöse-Code verbrauchen
POST/redemptions/{id}/refundsEinlösung zurückgeben
GET/balanceGuthaben
GET/storesFilialen
GET/couponsCoupons

POST/cards/lookup

Prüft eine Kundenkarte. Liefert nur ein Kürzel zur Bestätigung – nie Name, E-Mail oder Punktestand.

FeldTypBeschreibung
cardText, PflichtKartennummer (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.

FeldTypBeschreibung
cardText, PflichtKartennummer
receipt_idText, Pflicht, max. 64deine eindeutige Beleg-ID
amountZahl, PflichtGesamtbetrag in €, 0,01–10.000
eligible_amountZahl, optionalpunkteberechtigter Betrag (Standard = amount), höchstens amount
currencyText, optionalnur EUR
occurred_atZeit, optionalZeitpunkt des Einkaufs (Standard: jetzt); bis 72 Stunden rückwirkend, z. B. nach Offline-Betrieb
store_idZahl, optionalFiliale per BackPerk-ID (siehe /stores); entfällt bei filialgebundenem Schlüssel
store_refText, optional… oder per deiner eigenen Filialnummer (im Portal bei der Filiale hinterlegt, z. B. 4711). Unbekannt → 422 invalid_store
itemsListe, optionalWarenkorb, bis 500 Positionen – siehe Warenkorb
items[].totalZahl, Pflicht je PositionGesamtpreis der Position in € (nach Rabatt)
items[].quantityZahl, optionalMenge (Standard 1), auch Kommazahlen für Gewicht
items[].unit_priceZahl, optionalEinzelpreis in €
items[].sku / gtinText, optionaldeine Artikelnummer / EAN
items[].nameText, optionalArtikelbezeichnung (für den digitalen Bon)
items[].categoryText, optionalKassen-Kategorie, z. B. Getränke > Wasser
items[].brandText, optionalMarke
items[].eligibletrue/false, optionalfalse = 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

FeldTypBeschreibung
codeText, PflichtEinlöse-Code (13 Ziffern, beginnt mit 998)
invoice_amountZahl, PflichtRechnungsbetrag in € (vor Abzug der Punkte)
receipt_idText, Pflichtdeine Beleg-ID
store_id / store_refoptionalFiliale 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.

Webhooks

Statt regelmäßig nachzufragen, kann dein System Ereignisse von BackPerk empfangen. Adressen (nur https) legst du im Partnerportal unter Entwickler → Webhooks an und wählst die Ereignisse aus.

EreignisWann
transaction.createdEinkauf gebucht (auch „in Prüfung“)
transaction.releasedPunkte aus „in Prüfung“ freigegeben
transaction.refundedRetoure gebucht
redemption.capturedPunkte bei dir eingelöst
redemption.refundedEinlösung zurückgegeben
balance.lowGuthaben unter Warnschwelle
merchant.bookings_blockedNeue Buchungen gesperrt (Guthaben)
ping„Test senden“ im Portal
POST https://meine-kasse.de/backperk/webhook
Content-Type: application/json
BackPerk-Event: transaction.released
BackPerk-Delivery: evt_k3j2h1g0f9e8d7c6
BackPerk-Signature: t=1794107400,v1=5f2b…

{ "id": "evt_k3j2h1g0f9e8d7c6", "type": "transaction.released", "created_at": "2026-11-12T09:30:00+01:00",
  "data": { "transaction_id": "tx_5hd91k2m8qzc", "receipt_id": "K3-2026-000481", "status": "available",
            "points": 380, "refunded_points": 0, "amount_eur": 80.0, "cost_eur": 4.56, "store_id": 3, "store_ref": "4711", "test": false } }
  • Signatur prüfen wie bei der API: HEX(HMAC-SHA256(Webhook-Geheimnis, t + "." + Body)) mit dem Geheimnis whsec_… aus dem Portal; Zeitstempel nicht älter als 5 Minuten.
  • Antworte innerhalb von 10 Sekunden mit 2xx. Sonst wiederholen wir nach 1 Min., 5 Min., 30 Min., 2 Std., 6 Std. und 12 Std.
  • Ereignisse können mehrfach ankommen – an id erkennen und doppelte ignorieren. Die Reihenfolge ist nicht garantiert.
  • Nach 50 Fehlschlägen in Folge wird die Adresse deaktiviert (im Portal sichtbar).
  • Weitere Daten: redemption.* enthält redemption_id, points, value_eur; balance.low enthält balance_eur und topup_reference.

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": ["…"] } }.

HTTPerrorBedeutung · was tun
401unauthorizedSchlüssel fehlt, falsch oder widerrufen.
401invalid_signatureSignatur fehlt, falsch oder älter als 5 Minuten – Uhrzeit und Rohdaten prüfen.
402insufficient_balanceNeue Buchungen gesperrt, weil seit 7 Tagen Punkte auf Guthaben warten. Guthaben aufladen.
403ip_not_allowedAufruf von nicht freigegebener IP.
403merchant_inactivePartnerkonto nicht aktiv (in Prüfung, pausiert oder gesperrt).
403own_cardKarte gehört einem Mitarbeiter deines Teams – nicht erlaubt.
403merchant_no_redemptionsEinlösungen sind bei dir nicht aktiviert (Portal → Konditionen).
403wrong_merchantDer Kunde hat den Code für einen anderen Partner erstellt.
404card_invalidKarte unbekannt oder gesperrt.
404code_invalid / code_not_foundEinlöse-Code ungültig (Tippfehler, falsches Präfix).
409receipt_conflictBeleg-ID schon mit anderen Daten gebucht.
409possible_duplicateSieht nach einer doppelten Buchung aus (Doppel-Scan?). Nur nach Rückfrage beim Kunden mit neuer Beleg-ID erneut senden.
409already_usedEinlöse-Code wurde bereits verwendet.
409already_refunded / not_refundableRetoure bereits erfolgt oder nicht möglich.
410code_expiredEinlöse-Code abgelaufen oder vom Kunden storniert – Kunde soll einen neuen erstellen.
422invalid_amount / invalid_time / invalid_storeBetrag, Zeitpunkt (max. 72 h zurück) oder Filiale ungültig.
422items_mismatchSumme der Warenkorb-Positionen passt nicht zum Gesamtbetrag (±5 Cent).
422monthly_limitMonatslimit für vergebene Punkte erreicht – BackPerk kontaktieren.
422refund_window_closed / held_partial_refundRetoure nach 30 Tagen bzw. Teilretoure einer Buchung in Prüfung.
422test_modeEinlösen ist mit Testschlüsseln nicht möglich.
429card_limitFü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.
Schutz vor Missbrauch: BackPerk prüft jede Buchung automatisch. Wird eine Buchung abgelehnt (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 /transactions funktionieren auch im Test. Einlösen (capture) ist im Test gesperrt (422 test_mode) – lookup geht.

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_id ist 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_amount ohne Tabak, Pfand, Gutscheine und ohne den mit Punkten bezahlten Teil – oder besser den Warenkorb mit eligible: false senden.
  • Warenkorb mit Kategorie und Marke senden, damit Produkt-Coupons funktionieren; Summe der Positionen = Gesamtbetrag.
  • receipt_text wird auf den Bon gedruckt; held führt nicht zum Abbruch.
  • Fehlermeldungen (message) werden dem Kassierer angezeigt.
  • Retouren laufen über /transactions/{id}/refunds bzw. /redemptions/{id}/refunds.
  • Live-Schlüssel je Kasse, IP-Freigabe gesetzt, Schlüssel nur auf dem Server.
  • Filiale bei jeder Buchung mitsenden (store_ref mit deiner Filialnummer genügt) – sonst greifen filialbezogene Coupons nicht.

Fragen zur Anbindung? Schreib uns über die Hilfe im Kundenkonto oder an mail@backperk.com.

Datenschutzerklärung · Impressum · Jederzeit über „Cookie-Einstellungen“ änderbar.