Rekomi Docs
Für MarkenTracking installieren
Developers

Referenz der S2S-Tracking-API

HMAC-signierte Übermittlung von Conversions für jeden Zahlungsanbieter jenseits der direkten Verbindungen zu Stripe, Paddle, Braintree, Shopify, Wix, BigCommerce, Lemon Squeezy, Chargebee, Polar, Recurly, Gumroad, Creem, Dodo Payments und Mollie.

Über den Endpunkt für Server-zu-Server-Tracking (S2S) erfasst du eine Conversion von jedem beliebigen Zahlungsanbieter, auch eine Conversion, die nie über einen verbundenen Zahlungsdienstleister läuft. Stripe, Paddle, Braintree, Shopify, Wix, BigCommerce, Lemon Squeezy, Chargebee, Polar, Recurly, Gumroad, Creem, Dodo Payments und Mollie sind direkt angebunden, dafür brauchst du also kein Relay. Für alles andere ist das hier der Weg, auch für In-App-Käufe in mobilen Apps, serverseitige Conversions nach einer Testphase und eigene Ereignisquellen: Wenn dein Backend eine HMAC-signierte Anfrage per POST senden kann, erfasst Rekomi die Conversion. Rekomi behandelt S2S-Conversions genauso wie direkt erfasste: Attribution, Umgang mit Erstattungen, Auszahlungen.

Erforderlicher Plan: Starter oder höher. Organisationen in der Testphase (14 Tage kostenlos) werden immer angenommen. Liegt der Plan einer Organisation unter Starter und läuft keine Testphase, kommt 402 mit { "error": "plan_tier_required", "required": "Starter", "current": "<tier>", ... } zurück.

Funktioniert mit jedem Zahlungsanbieter

S2S ist der universelle Weg: Wenn dein Backend eine HMAC-signierte Anfrage per POST senden kann, erfasst Rekomi die Conversion, egal welcher Zahlungsanbieter dahinter steckt (oder ob überhaupt einer). Der Weg ist bewusst an keine Liste unterstützter Plattformen gebunden, deshalb funktioniert hier alles, was nicht direkt angebunden ist, auch eigene Checkouts, In-App-Käufe in mobilen Apps und serverseitige Conversions nach einer Testphase.

Du nutzt schon einen direkt angebundenen Zahlungsanbieter? Stripe, Paddle, Braintree, Shopify, Wix, BigCommerce, Lemon Squeezy, Chargebee, Polar, Recurly, Gumroad, Creem, Dodo Payments und Mollie verbindest du mit wenigen Klicks direkt, ohne ein Relay zu bauen. Nutzt du einen davon, nimm lieber die direkte Integration; S2S ist für jeden anderen Zahlungsanbieter da.

Für die Plattformen unten gibt es in der Einrichtungs-Checkliste fertige S2S-Anleitungen Schritt für Schritt. Sie sollen dir die Arbeit erleichtern und sind keine Grenze; jedes Backend funktioniert (siehe unten):

Jedes Backend funktioniert. S2S ist einfach ein HTTPS-POST mit HMAC-SHA256-Signatur. Das Node.js-Beispiel unten lässt sich eins zu eins auf Python, Ruby, Go, PHP, .NET, Elixir oder alles andere übertragen, was HTTP-Anfragen senden und HMAC berechnen kann. Nimm diesen Weg immer dann, wenn du ein Secret serverseitig sicher aufbewahren kannst.

Endpunkt

POST /api/tracking/s2s

Produktivumgebung: https://api.rekomi.com/api/tracking/s2s

Es gibt keinen eigenen Staging-Host und keinen Schlüssel für einen Testmodus. Um den Endpunkt ohne echtes Geld auszuprobieren, sendest du das Postback mit dem Slug des vorab angelegten Test-Affiliates: Alles, was dem Test-Affiliate gutgeschrieben wird, ist nie auszahlbar und wird aus den Berichten herausgefiltert, erscheint aber trotzdem im Tab Verkäufe der Seite Conversions und in Webhooks (siehe Teste deine Integration).

Header

Authorization: Bearer rk_live_xxxxxxxxxxxxxxxxxxxxx
X-Rekomi-Signature: t=1715366423,sig=8f4e2c5b...
Content-Type: application/json

Zur Authentifizierung funktioniert jeder der beiden Header (werden beide gesendet, hat Authorization Vorrang):

  • Authorization: Bearer rk_live_* (empfohlen): dein API-Schlüssel als Bearer. Das Präfix rk_test_ ist reserviert und wird nicht vergeben.
  • X-Rekomi-Api-Key: rk_live_*: älterer Header. Derselbe Schlüssel, dieselbe Bedeutung.

Außerdem:

  • X-Rekomi-Signature (Pflicht): HMAC-SHA256-Signatur im Format t=<unix-seconds>,sig=<hex>. Berechnet als HMAC-SHA256(signing_secret, "<unix-seconds>.<raw-body-bytes>"). Das Signing Secret ist der separate Wert rks_..., den du beim Erstellen des API-Schlüssels bekommen hast, nicht der Bearer.

Anfrageinhalt

{
  "externalEventId": "purchase_abc123",
  "affiliateSlug": "jane-recommends",
  "amountCents": 9900,
  "currency": "USD",
  "customerId": "cus_external_xyz"
}
  • externalEventId (Pflicht): deine eindeutige Kennung für diese Conversion. Dient der Deduplizierung.
  • affiliateSlug (Pflicht): der Affiliate, dem die Conversion gutgeschrieben wird. Sende entweder den Slug aus dem Tracking-Link des Affiliates oder seinen kontoweiten Slug. Den kontoweiten Slug zeigen die Affiliate-Liste im Dashboard, GET /api/v1/affiliates/{id} (effectiveSlug) und der eigene Link zum Teilen des Affiliates, sobald er sich einen gesichert hat. Beide führen zur selben Mitgliedschaft, auch beim vorab angelegten Test-Affiliate. Groß- und Kleinschreibung spielt keine Rolle, SERKAN und serkan führen also zum selben Affiliate. Ein Sonderfall: Ein kontoweiter Slug gilt für alle Kampagnen. Ist der Affiliate Mitglied in mehr als einer deiner Kampagnen, sende stattdessen den Link-Slug der Kampagne.
  • amountCents (Pflicht): der Betrag der Conversion in der kleinsten Einheit der gesendeten currency (Cent bei USD, ganze Yen bei JPY, Tausendstel bei KWD, BHD und OMR). Positive ganze Zahl. Obergrenze 1,000,000,000,000 kleinste Einheiten, eine währungsunabhängige Plausibilitätsgrenze, die in jeder unterstützten Währung über jedem denkbaren Einzelverkauf liegt.
  • currency (optional): ISO-4217-Code, genau 3 Buchstaben. Fehlt der Wert oder ist er leer, gilt USD; jeder andere Wert muss ein unterstützter Code aus 3 Buchstaben sein, sonst wird die Anfrage mit 400 unsupported_currency abgelehnt. Wird serverseitig in Großbuchstaben umgewandelt, usd und USD sind also gleichwertig.
  • customerId (optional): deine Kundenkennung. Praktisch, um die Conversion deiner eigenen Datenbank zuzuordnen. Außerdem ist das der Wert, auf den der Filter stripeCustomerId bei GET /api/v1/conversions passt, sodass du ein Postback, dessen Antwort verloren ging, über den Kunden wiederfindest. Sende eine feste ID pro echtem Kunden und nie einen Platzhalter. Beachte, dass ein S2S-Postback als einzelner Verkauf zum aktuellen Satz der Kampagne berechnet wird: Das Zeitfenster in Monaten pro Kunde und die Verzögerung um die ersten N Rechnungen gelten für direkt verbundene Zahlungsdienstleister, nicht für S2S-Postbacks. Melde also nur die Zahlungen, für die eine Provision fällig werden soll.
  • customerName (optional): der Anzeigename des Kunden, zu sehen in der Zeile der Conversion im Dashboard.
  • customerEmail (optional): die E-Mail-Adresse des Kunden.
  • customerAvatarUrl (optional): eine https-URL zu einem Avatarbild; fehlt sie, wird ein Avatar aus customerEmail abgeleitet.
  • subId (optional): der Kanal-Tag, den der Besuch des Kunden mitbrachte (?sub=yt am Link des Affiliates), beim Checkout aus window.Rekomi.subId oder dem Cookie _rkmisub gelesen. Wird serverseitig normalisiert (Kleinbuchstaben, Ziffern, Punkte, Bindestriche, Unterstriche, höchstens 64 Zeichen). Fehlt der Wert, trägt der Verkauf keinen Tag (das Slug-Postback speichert nur, was du sendest); ein Postback, das über den E-Mail-Abgleich läuft, übernimmt dagegen den Tag eines Leads. Siehe Sub-IDs.

Die Provisionsregeln gelten genau wie bei einem Webhook des Zahlungsanbieters: zuerst der individuelle Satz des Affiliates, dann der Satz seiner Partnergruppe, dann die Performance-Stufe, die sein vermittelter Umsatz erreicht hat, dann der Standard der Kampagne (siehe Provisionsmodelle). Der Anfrageinhalt enthält keinen Gutscheincode, deshalb greift der eigene Provisionssatz eines Gutscheins hier als einzige Stufe nicht. Die zweite Ausnahme sind das wiederkehrende Zeitfenster und die Rechnungsverzögerung der Kampagne: Sie gelten nicht für S2S-Postbacks (siehe customerId oben), die Marke entscheidet also selbst, welche Zahlungen sie meldet.

Währung

Das Feld currency nimmt Codes aus einer Positivliste mit über 135 ISO-4217-Codes an (jede Darstellungswährung von Stripe plus die Golfwährungen mit drei Nachkommastellen); Groß- und Kleinschreibung spielt keine Rolle (usd und USD sind gleichwertig). Fehlt currency oder ist das Feld leer, gilt USD. Jeder andere Wert muss ein unterstützter Code aus 3 Buchstaben sein: Ein Code, der nicht auf der Liste steht, und jeder Wert, der nicht aus genau 3 Buchstaben besteht (EURO, YEN, US$), liefert 400 unsupported_currency und es wird nichts erfasst. Eine Ausnahme gilt für den Übergang: ANG wird als XCG erfasst, seinem Nachfolger im Verhältnis eins zu eins. Sende den echten ISO-Code des Verkaufs; eine falsch angegebene Währung wird nie in USD umgedeutet. Die vollständige Liste, die Formen der Fehlerantworten und wie die Heimatwährung der Organisation für die Anzeige festgelegt wird, findest du unter Conversion-Währung.

Antwort (Erfolg)

Es gibt zwei Erfolgsformen: neue Conversion oder Duplikat (sicher wiederholbar).

Neu:

{ "ok": true, "conversionId": "c0nv0001-..." }

Duplikat (dieselbe externalEventId kam für diese Organisation schon einmal an):

{ "ok": true, "deduped": true }

Die Form mit deduped gibt keine conversionId zurück; das Ereignis gehört weiterhin zur ursprünglichen Conversion. Werte auf deiner Seite beides als Erfolg. Wiederholungen sind beliebig oft sicher.

Antwort (Fehler)

Fehlerantworten haben je nach Ursache eine von vier Formen:

401 Unauthorized: leerer Antwortinhalt. Ausgelöst durch:

  • fehlenden oder ungültigen Bearer-Schlüssel
  • fehlende, fehlerhaft aufgebaute oder falsche HMAC-Signatur
  • einen Zeitstempel, der mehr als 300 Sekunden abweicht

409 Conflict: Deine Organisation erfasst Conversions bereits automatisch über eine direkte Verbindung, deshalb ist die S2S-Übermittlung deaktiviert, damit derselbe Verkauf nicht doppelt gezählt wird. JSON-Antwortinhalt mit einem error-Code und einer lesbaren message. Es gibt einen Code pro direkter Verbindung, und die Liste wächst mit jeder neuen:

  • stripe_connect_active
  • bigcommerce_connect_active
  • wix_connect_active
  • wordpress_connect_active (das WooCommerce-Plugin)
  • braintree_connect_active
  • paddle_connect_active
  • lemonsqueezy_connect_active
  • shopify_connect_active
  • chargebee_connect_active
  • polar_connect_active
  • recurly_connect_active
  • gumroad_connect_active
  • creem_connect_active
  • dodo_connect_active
  • mollie_connect_active

Wenn du wirklich zu S2S wechseln willst, trenne den genannten Zahlungsanbieter auf seiner Seite im Schritt Zahlungsanbieter verbinden (bei Shopify, Wix und BigCommerce deinstallierst du die Rekomi-App im Dashboard der jeweiligen Plattform; bei WooCommerce trennst du das Rekomi-Plugin auf deiner WordPress-Website). Dieselben Konflikte gelten für POST /api/tracking/lead (siehe unten).

402 Plan tier required: JSON-Antwortinhalt mit einem Hinweis zum Upgrade:

{
  "error": "plan_tier_required",
  "required": "Starter",
  "current": "None",
  "trialing": false,
  "message": "S2S tracking requires Starter tier or higher. Upgrade in Settings > Billing."
}

400 Bad Request: JSON-Antwortinhalt nur mit einem error-Code:

{ "error": "amount_out_of_range" }

Alle 400-Codes:

  • external_event_id_required: externalEventId fehlt oder ist leer
  • affiliate_slug_required: affiliateSlug fehlt oder ist leer
  • amount_out_of_range: amountCents ist ≤ 0 oder größer als 1,000,000,000,000 kleinste Einheiten
  • unsupported_currency: currency wurde gesendet (nicht leer) und ist kein unterstützter Code aus 3 Buchstaben, egal ob ein unbekannter Code oder ein Wert anderer Länge wie EURO (der Antwortinhalt gibt die fehlerhafte currency mit zurück)
  • invalid_body: JSON konnte nicht gelesen werden
  • no_active_program: Die Kampagne, zu der dieser Affiliate gehört, ist inaktiv oder gesperrt
  • affiliate_not_found: affiliateSlug passt zu keinem Link in deiner Organisation
  • affiliate_not_active: Den Affiliate gibt es, er ist aber gerade nicht freigegeben, sein Slug bringt also keine Provision mehr
  • not_a_sale_campaign: Die Kampagne des Affiliates vergütet Klicks oder Leads, keine Verkäufe, deshalb wird ein Verkaufs-Postback abgelehnt

Signatur berechnen

Die Signatur ist der HMAC-SHA256 des Strings {unix-seconds}.{raw-body-bytes} mit deinem Signing Secret im Klartext als Schlüssel. Kodiere das Ergebnis hexadezimal.

import crypto from "node:crypto";

function sign(body: string, signingSecret: string): string {
  const t = Math.floor(Date.now() / 1000);
  const payload = `${t}.${body}`;
  const sig = crypto.createHmac("sha256", signingSecret).update(payload).digest("hex");
  return `t=${t},sig=${sig}`;
}

Signiere genau die Bytes, die du sendest. Der Server berechnet den HMAC über den rohen Anfrageinhalt, den er empfängt, also musst du dasselbe Byte-Array signieren und senden. Die häufigste Ursache für einen 401 bei einer sonst korrekten Anfrage ist, dass der Inhalt zweimal serialisiert wird (du signierst einen JSON-String und sendest einen anderen, der sich in Leerzeichen oder Groß- und Kleinschreibung der Schlüssel unterscheidet). Serialisiere einmal zu Bytes, signiere diese Bytes, sende diese Bytes.

Ein Beispiel in .NET: var body = JsonSerializer.SerializeToUtf8Bytes(payload);, dann den HMAC über "{t}." + body und new ByteArrayContent(body), nie ein zweiter Aufruf von JsonSerializer.Serialize(payload) für den Inhalt der Anfrage.

Schutz vor Replays

Der Server lehnt Signaturen ab, deren t= mehr als 300 Sekunden (5 Minuten) in der Vergangenheit oder mehr als 300 Sekunden in der Zukunft liegt. Halte die Uhr deines Servers per NTP synchron.

Außerdem lehnt der Server doppelte Paare aus (externalEventId, organization) ab. Der zweite Aufruf mit derselben externen Ereignis-ID liefert die ursprüngliche Conversion mit deduped = true. Beliebig oft sicher wiederholbar.

Beispiele

Dieselbe Anfrage in vier Sprachen; jeder andere Stack folgt genau demselben Muster (einmal serialisieren, den HMAC über genau diese Bytes berechnen, genau diese Bytes senden).

import crypto from "node:crypto";

const BEARER = process.env.REKOMI_API_KEY!;        // rk_live_...
const SIGNING = process.env.REKOMI_SIGNING_SECRET!; // rks_...

async function logConversion(input: {
  externalEventId: string;
  affiliateSlug: string;
  amountCents: number;
  currency?: string;
  customerId?: string;
}) {
  const body = JSON.stringify(input);
  const t = Math.floor(Date.now() / 1000);
  const sig = crypto.createHmac("sha256", SIGNING).update(`${t}.${body}`).digest("hex");
  const res = await fetch("https://api.rekomi.com/api/tracking/s2s", {
    method: "POST",
    headers: {
      "X-Rekomi-Api-Key": BEARER,
      "X-Rekomi-Signature": `t=${t},sig=${sig}`,
      "Content-Type": "application/json",
    },
    body,
  });
  if (!res.ok) throw new Error(`Rekomi S2S failed: ${res.status} ${await res.text()}`);
  return await res.json();
}
import hmac, hashlib, time, json, requests

BEARER = "rk_live_..."
SIGNING = "rks_..."

def log_conversion(external_event_id, affiliate_slug, amount_cents, currency="USD"):
    body = json.dumps({
        "externalEventId": external_event_id,
        "affiliateSlug": affiliate_slug,
        "amountCents": amount_cents,
        "currency": currency,
    }, separators=(",", ":"))
    t = int(time.time())
    sig = hmac.new(SIGNING.encode(), f"{t}.{body}".encode(), hashlib.sha256).hexdigest()
    r = requests.post("https://api.rekomi.com/api/tracking/s2s", data=body, headers={
        "X-Rekomi-Api-Key": BEARER,
        "X-Rekomi-Signature": f"t={t},sig={sig}",
        "Content-Type": "application/json",
    })
    r.raise_for_status()
    return r.json()
<?php
// Works in plain PHP, Laravel, WordPress plugins, anywhere with curl.
$bearer  = getenv('REKOMI_API_KEY');        // rk_live_...
$signing = getenv('REKOMI_SIGNING_SECRET'); // rks_...

function logConversion(string $externalEventId, string $affiliateSlug, int $amountCents, string $currency = 'USD'): array
{
    global $bearer, $signing;
    // Serialize ONCE; the same bytes are signed and sent.
    $body = json_encode([
        'externalEventId' => $externalEventId,
        'affiliateSlug'   => $affiliateSlug,
        'amountCents'     => $amountCents,
        'currency'        => $currency,
    ], JSON_UNESCAPED_SLASHES);
    $t   = time();
    $sig = hash_hmac('sha256', $t . '.' . $body, $signing);

    $ch = curl_init('https://api.rekomi.com/api/tracking/s2s');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $body,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            'X-Rekomi-Api-Key: ' . $bearer,
            'X-Rekomi-Signature: t=' . $t . ',sig=' . $sig,
            'Content-Type: application/json',
        ],
    ]);
    $res    = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($status >= 400) {
        throw new RuntimeException("Rekomi S2S failed: {$status} {$res}");
    }
    return json_decode($res, true);
}
require "net/http"
require "openssl"
require "json"

BEARER  = ENV.fetch("REKOMI_API_KEY")        # rk_live_...
SIGNING = ENV.fetch("REKOMI_SIGNING_SECRET") # rks_...

def log_conversion(external_event_id:, affiliate_slug:, amount_cents:, currency: "USD")
  body = JSON.generate({
    externalEventId: external_event_id,
    affiliateSlug: affiliate_slug,
    amountCents: amount_cents,
    currency: currency,
  })
  t   = Time.now.to_i
  sig = OpenSSL::HMAC.hexdigest("SHA256", SIGNING, "#{t}.#{body}")

  uri = URI("https://api.rekomi.com/api/tracking/s2s")
  res = Net::HTTP.post(uri, body, {
    "X-Rekomi-Api-Key" => BEARER,
    "X-Rekomi-Signature" => "t=#{t},sig=#{sig}",
    "Content-Type" => "application/json",
  })
  raise "Rekomi S2S failed: #{res.code} #{res.body}" unless res.is_a?(Net::HTTPSuccess)
  JSON.parse(res.body)
end

Leads (Anmeldungen) tracken

Bei Stripe und Paddle werden Leads automatisch erfasst, ganz ohne zusätzlichen Code, dort brauchst du diesen Endpunkt also meistens nicht. Nutze ihn, wenn dein Zahlungsanbieter nicht direkt angebunden ist und du das Tracking aus deinem Backend steuerst: Sende eine kostenlose Anmeldung (keinen Verkauf) an POST /api/tracking/lead, mit demselben Bearer-API-Schlüssel und derselben HMAC-Authentifizierung über X-Rekomi-Signature wie oben. Die oben beschriebenen Konflikte durch den gegenseitigen Ausschluss gelten auch hier: Solange eine direkte Verbindung aktiv ist (Stripe, Paddle, Braintree, Lemon Squeezy, Chargebee, Polar, Recurly, Gumroad, Creem, Dodo Payments, Mollie, die Shopify-App oder die Wix-App), liefert der Lead-Endpunkt den passenden 409 *_connect_active, und der Weg für Leads ist stattdessen der Aufruf Rekomi.convert() im Browser. Ein Lead verknüpft die E-Mail-Adresse eines Kunden schon vor der Zahlung mit der Empfehlung, sodass ein späterer Verkauf dem Affiliate auch dann gutgeschrieben wird, wenn das Klick-Cookie verschwunden ist. Der Anfrageinhalt nimmt außerdem eine optionale subId (den Kanal-Tag) an, die der spätere Verkauf übernimmt. Unter Leads und Anmeldungen tracken findest du den Aufbau des Anfrageinhalts, das Gegenstück Rekomi.convert() im Browser und wie der Fallback über den E-Mail-Abgleich funktioniert.

Wann S2S und wann der Edge-Redirect

  • S2S, wenn Conversions auf dem Server entstehen (Käufe in mobilen Apps, eigene Checkout-Abläufe, serverseitige Conversions nach einer Testphase).
  • Edge-Redirect (der Tracking-Link /r/{slug} mit First-Party-Cookie), wenn Conversions aus einem Klick im Browser auf einen Empfehlungslink entstehen.
  • Gutscheincodes, wenn du gar keinen serverseitigen Code ausführen kannst; gib jedem Affiliate einen eigenen Rabattcode, dann passiert die Attribution beim Checkout, ohne dass auf deiner Website etwas laufen muss.

Für die meisten Abo-Produkte ist S2S der zuverlässigere Weg, weil er nicht davon abhängt, dass das Browser-Cookie den ganzen Weg bis zur Conversion übersteht.

Conversions erstatten

S2S-Conversions erstattest du über POST /api/tracking/refund; Conversions von einem direkt verbundenen Zahlungsdienstleister werden automatisch über dessen Erstattungs-Webhook erstattet und brauchen diesen Endpunkt nicht.

POST /api/tracking/refund

Dasselbe Authentifizierungsmodell aus HMAC und Bearer wie beim S2S-Endpunkt oben. Anfrageinhalt:

{
  "externalEventId": "purchase_abc123",
  "refundAmountCents": 9900
}
  • externalEventId (Pflicht): die externe Ereignis-ID der ursprünglichen Conversion (camelCase, dieselbe Schreibweise wie im S2S-Anfrageinhalt).
  • refundAmountCents (optional): Teilerstattung in der kleinsten Einheit der Währung der Conversion. Lass das Feld weg, um alles zu erstatten, was noch nicht erstattet ist (den vollen ursprünglichen Betrag, wenn noch keine Teilerstattung erfasst wurde); nach einer Teilerstattung erstattet ein fehlender Betrag den Rest.

Antwort (vollständige Erstattung):

{
  "ok": true,
  "conversionId": "c0nv0001-...",
  "refundedAmountCents": 9900,
  "cumulativeRefundedAmountCents": 9900,
  "isFullRefund": true
}

Teilerstattungen summieren sich: Jede weitere Erstattung erhöht cumulativeRefundedAmountCents, und die Conversion wechselt erst dann in den Status Refunded, wenn die Summe den ursprünglichen Betrag erreicht. Die Provision wird anteilig zurückgeholt: Werden 40% des Bestellbetrags erstattet, werden 40% der Provision storniert.

Fehlerantworten

Dieser Endpunkt ist auch unter der Alias-Route POST /api/v1/tracking/refund erreichbar und verhält sich dort identisch.

HTTPerrorUrsache
400invalid_bodyJSON konnte nicht gelesen werden
401(leerer Antwortinhalt)Bearer fehlt oder ist ungültig, HMAC-Signatur fehlt oder ist ungültig, Zeitstempel weicht ab
402plan_tier_requiredOrganisation unter Starter und nicht in der Testphase
413payload_too_largeAnfrageinhalt größer als 4096 Bytes
404conversion_not_foundKeine Conversion dieser Organisation passt zur externen Ereignis-ID
422external_event_id_requiredFehlt oder ist leer
422refund_amount_invalid≤ 0
422already_fully_refundedDie Conversion wurde schon vollständig erstattet, es bleibt also nichts übrig, egal ob der Betrag fehlt oder gesendet wird
422amount_exceeds_originalMit dieser Erstattung läge die erstattete Summe über dem Betrag der Conversion

Signatur

Genau wie die Berechnung der S2S-Signatur. Verwende denselben HMAC-Helper.

const body = JSON.stringify({
  externalEventId: "purchase_abc123",
  refundAmountCents: 9900,
});
const t = Math.floor(Date.now() / 1000);
const sig = crypto.createHmac("sha256", SIGNING).update(`${t}.${body}`).digest("hex");

await fetch("https://api.rekomi.com/api/tracking/refund", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${BEARER}`,
    "X-Rekomi-Signature": `t=${t},sig=${sig}`,
    "Content-Type": "application/json",
  },
  body,
});

Idempotenz und Replays

Wiederholte Erstattungen für eine bereits vollständig erstattete Conversion liefern 422 already_fully_refunded, statt die Provision erneut zu verringern. Überläufe bei Teilerstattungen werden abgefangen: Würde refundAmountCents zusammen mit früheren Erstattungen den Betrag der Conversion übersteigen, schlägt die Anfrage mit 422 amount_exceeds_original fehl. Sende in refundAmountCents immer genau den Betrag, den dein Zahlungsdienstleister tatsächlich erstattet hat.