Rekomi Docs
Für Entwickler
Developers

Webhooks

Abonniere Ereignisse, prüfe Signaturen und behandle Wiederholungen.

Rekomi sendet Ereignisse in dem Moment an deine Webhook-Endpunkte, in dem sie passieren. Conversions, Auszahlungen, der Lebenszyklus eines Affiliates: alles, worauf deine eigenen Systeme reagieren sollen.

Erforderliche Planstufe: Starter oder höher, um Webhook-Endpunkte anzulegen, zu rotieren, zu ändern oder zu testen. Bestehende Endpunkte bekommen weiter Ereignisse, wenn dein Abonnement ausläuft (wir lassen deine Integration nicht mitten im Abrechnungszeitraum hängen), aber um neue Endpunkte hinzuzufügen oder die Konfiguration zu ändern, brauchst du einen aktiven Plan ab Starter. In der Testphase (14 Tage kostenlos, auf jeder bezahlten Planstufe) kannst du Webhooks frei einrichten. Die Zapier-Integration nutzt intern genau diese Schnittstelle, deshalb gibt es Webhooks in jedem bezahlten Plan.

Ein Webhook-Abonnement anlegen

Die Anleitung unten beschreibt direkte Webhook-Abonnements, die du selbst verwaltest. Willst du dieselben Ereignisse an Slack, Gmail, HubSpot, Notion, Google Sheets oder eine der 8,000+ Apps weitergeben, mit denen Zapier funktioniert, ohne Code zu schreiben oder einen Endpunkt zu betreiben, nutze stattdessen die Zapier-Integration. Zapier nutzt dieselbe Webhook-Schnittstelle als REST-Hook-Abonnent: Schaltest du einen Zap ein, registriert er ein Abonnement für deine Organisation, schaltest du ihn aus, meldet er es sauber wieder ab. Davon bekommst du nichts mit.

Lege einen Endpunkt im Dashboard an: Öffne /dashboard/settings/webhooks und klicke auf „Endpunkt hinzufügen“.

Die Webhook-Einstellungen im Rekomi-Dashboard mit der Schaltfläche Endpunkt hinzufügen und der Liste der eingerichteten Webhook-Endpunkte samt abonnierten Ereignissen und Zustellstatus.

Pflichtangaben:

  • URL: ein HTTPS-Endpunkt, den du kontrollierst.
  • Ereignisse: Die Auswahl im Dashboard bietet die häufigsten Ereignisse an (*, conversion.created, conversion.approved, payout.created, payout.paid, affiliate.created, affiliate.approved); die vollständige Liste unten abonnierst du über die API, oder du wählst * für alles.

Klicke auf „Erstellen“. Das Signing Secret (im Dashboard „Signatur-Secret“) wird GENAU EINMAL angezeigt. Speichere es; es lässt sich nicht wiederherstellen.

Dasselbe Signing Secret signiert jede Payload, die an diesen Endpunkt geht. Rotierst du das Secret, wird das alte sofort nicht mehr verwendet.

Ereignistypen

Derzeit verfügbar:

  • * (alle Ereignisse)
  • conversion.created (neue Conversion zugeordnet; umfasst bezahlte Klicks und bezahlte Leads in Kampagnen, die pro Klick oder pro Lead vergüten)
  • conversion.approved (eine Conversion im Status Pending wurde ausdrücklich freigegeben)
  • conversion.refunded (Provision zurückgeholt)
  • conversion.denied (Conversion abgelehnt)
  • conversion.validated (eine wegen Betrugsverdachts in Quarantäne gesetzte Conversion wurde wieder als auszahlbar freigegeben)
  • conversion.invalidated (eine Conversion wurde wegen Betrugsverdachts in Quarantäne gesetzt oder von Hand für ungültig erklärt; nicht auszahlbar)
  • conversion.adjusted (Betrag oder Provision der Conversion angepasst)
  • payout.created (Auszahlungslauf angelegt, mit diesem Affiliate als Empfänger)
  • payout.paid (Auszahlung abgeschlossen)
  • payout.failed (Überweisung der Auszahlung fehlgeschlagen)
  • affiliate.created (neue Bewerbung eines Affiliates oder freigegebener Treffer aus dem Netzwerk)
  • affiliate.approved (Status des Affiliates auf Approved geändert)
  • affiliate.application_completed (ein Bewerber hat Schritt 2 eines zweistufigen Bewerbungsformulars abgeschlossen; trägt die vollständige Map der Antworten)
  • deal.funded (die Treuhand-Belastung für einen Meilenstein eines Creator-Deals wurde angelegt; die Payload trägt dealId, milestoneId, seq, title, amountCents, takeCents, totalBrandCharge, fundingChargeId, dealStatus)
  • milestone.submitted (der Creator hat einen Meilenstein zur Prüfung eingereicht; trägt submissionUrls und autoReleaseAt)
  • milestone.released (die Auszahlung für einen Meilenstein wurde freigegeben; trägt grossCents, netCents, payoutId, dealStatus)
  • dispute.opened (eine der beiden Seiten hat zu einem Meilenstein einen Streitfall eröffnet; trägt openedBy, reason, proposedResolution)
  • campaign_coupon.created (die Marke hat einen übergeordneten Kampagnengutschein angelegt, zu dem der Stripe-coupon gehört)
  • campaign_coupon.updated (Einstellungen des Kampagnengutscheins geändert, Name und Schalter)
  • campaign_coupon.deactivated (Kampagnengutschein archiviert; die untergeordneten Codes der einzelnen Affiliates werden ebenfalls widerrufen)
  • campaign_coupon.deleted (Kampagnengutschein endgültig gelöscht; nur möglich, wenn es keine Codes einzelner Affiliates gab)
  • affiliate_coupon.created (einlösbarer Code für einen Affiliate erstellt; von der Marke ODER vom Affiliate selbst)
  • affiliate_coupon.deactivated (einlösbarer Code eines Affiliates von der Marke widerrufen)
  • affiliate_coupon.revoked (ein Affiliate hat seinen eigenen Code gelöscht; source: "self_deleted")

Pro Endpunkt kannst du eine Auswahl davon abonnieren. Verschiedene Endpunkte können verschiedene Ereignisse abonnieren. Das genaue Feld data für jeden Typ findest du unten bei den Payloads der einzelnen Ereignisse.

Neun dieser Ereignisse gibt es auch als fertige Zapier-Trigger: conversion.created, conversion.refunded, affiliate.created, affiliate.approved, payout.created, payout.paid, payout.failed, campaign_coupon.created und affiliate_coupon.created. Ist das Zielsystem eine App, mit der Zapier funktioniert, musst du gar keinen Webhook-Endpunkt betreiben oder Signaturen prüfen: Zapier übernimmt für dich das Abonnement, den HTTPS-Endpunkt und die Prüfung der Signatur.

Envelope der Payload

Jede Zustellung ist ein JSON-Objekt mit demselben äußeren Envelope. Das Feld data ändert sich je nach Ereignistyp (die vollständigen Formate stehen weiter unten).

{
  "id": "evt_9f8e7d6c5b4a39281706f5e4d3c2b1a0",
  "type": "conversion.created",
  "createdAt": "2026-05-11T03:14:25.000Z",
  "organizationId": "01234567-89ab-cdef-0123-456789abcdef",
  "data": { /* see per-event tables below */ }
}

id ist evt_, gefolgt von 32 Hex-Zeichen.

  • id: eindeutige Kennung des Ereignisses. Nutze sie bei dir zur Deduplizierung (Rekomi setzt außerdem X-Rekomi-Delivery-Id für die Deduplizierung bei Wiederholungen; siehe den Abschnitt zu den Headern).
  • type: der Ereignistyp aus der Liste oben.
  • createdAt: Zeitstempel nach ISO-8601 in UTC.
  • organizationId: die ID deiner Organisation. Immer deine eigene; Ereignisse anderer Organisationen bekommst du nie (zusätzlicher Filter im Dispatcher als Defense in Depth).
  • data: die Payload des jeweiligen Ereignisses. Felder mit dem Wert null fehlen in der serialisierten Payload ganz. Lies optionale Felder also defensiv, statt ein explizites null zu erwarten.

Formate von data je Ereignis

Die Formate unten sind mit dem Code abgeglichen, der die Ereignisse im Live-Betrieb erzeugt; das sind genau die Feldnamen, die du bekommst.

conversion.created

{
  "conversionId": "c0nv0001-...",
  "affiliateId": "01234567-...",
  "programId": "0789abcd-...",
  "amountCents": 9900,
  "currency": "USD",
  "commissionCents": 1980,
  "status": "Pending",
  "externalEventId": "in_1XYZ...",
  "type": "Recurring",
  "attributionMethod": "coupon"
}

Enum status: Pending | Approved | Paid | Denied | Refunded | Invalidated (wegen Betrugsverdachts in Quarantäne, nicht auszahlbar). Enum type: OneTime | Recurring | Bonus | ClickReward (ein bezahlter Klick in einer Kampagne, die pro Klick vergütet, CPC) | LeadReward (ein bezahlter Lead in einer Kampagne, die pro Lead vergütet, CPL).

attributionMethod sagt dir, wie der Verkauf zugeordnet wurde. So kannst du eine Gutscheineinlösung genauso abbilden wie einen Klick auf einen Link. Es ist der Rohwert; das Rekomi-Dashboard fasst die Werte in der Spalte „Quelle“ zu einem Tag zusammen. Die vollständige Liste:

attributionMethod (Rohwert)„Quelle“ im DashboardBedeutung
coupon, coupon_with_metadata, shopify_discount_code, wix_coupon_code, bigcommerce_coupon_code, wordpress_coupon, gumroad_coupon_code, creem_coupon_code, mollie_coupon_codeCouponZugeordnet über einen eingelösten Gutscheincode eines Affiliates (bei Mollie über den Eintrag metadata.rekomi_coupon)
metadata_or_customer, paddle_custom_data, braintree_custom_field, rekomi_self_referral, lemonsqueezy_custom_data, chargebee_referral, recurly_referral, polar_metadata, polar_reference_id, gumroad_url_param, creem_metadata, creem_request_id, dodo_metadata, mollie_metadata, shopify_pixel_journey, shopify_landing_site, shopify_customer_journeyLinkZugeordnet über Empfehlungs-Metadaten, einen erfassten Klick oder einen URL- bzw. Referenzparameter des Zahlungsanbieters. Die drei Shopify-Werte sind eine Link-Attribution, ermittelt (in dieser Reihenfolge) aus dem Klick im Web-Pixel des Shops, der gespeicherten Einstiegsseite der Bestellung oder Shopifys eigenen Daten zur Customer Journey
paddle_customer_history, braintree_customer_history, shopify_customer_history, wix_customer_history, bigcommerce_customer_history, wordpress_customer_history, lemonsqueezy_customer_history, chargebee_customer_history, recurly_customer_history, polar_customer_history, gumroad_customer_history, creem_customer_history, dodo_customer_history, mollie_customer_history, mollie_subscription_history, email_lead_matchCustomer matchZugeordnet über den Abgleich mit einem wiederkehrenden Kunden oder einem früheren Lead
crm_lookup:<platform> (crm_lookup:beehiiv, crm_lookup:convertkit, crm_lookup:mailerlite, crm_lookup:ontraport, crm_lookup:gohighlevel)Platform matchGutgeschrieben über den Anmeldedatensatz der verbundenen Plattform selbst: Nichts anderes hat einen Affiliate benannt, also hat Rekomi die E-Mail-Adresse des Käufers auf der Plattform nachgeschlagen, und das dort gespeicherte Herkunfts-Tag hat den Affiliate benannt
manual_stripe_appLinkVon der Marke von Hand zugeordnet, über die Verwaltungsansicht in Stripe
cpc_aggregationClickZusammengefasste Vergütung für Pay-per-Click
lead_signupLeadEine erfasste Anmeldung ohne Provision (kostenlose, identifizierte Anmeldung)
lead_formLeadEin qualifiziertes, abgeschicktes Lead-Formular in einer Kampagne, die pro Lead vergütet (CPL); bringt den Betrag pro Lead der Kampagne ein
sub_affiliate_overrideOverrideOverride-Zeile des Werbers (siehe unten)
historical_import:<source>ImportedVon einer anderen Plattform übernommen
null (fehlt)ServerServer-zu-Server-Postback (S2S setzt keine Methode)

Dasselbe Feld attributionMethod gibt es in der REST-Liste und der Detailansicht von GET /api/v1/conversions und im CSV-Export (Spalte attribution_method); du brauchst also keinen Webhook, um es auszulesen. Jede Payload von conversion.created trägt außerdem subId, das Kanal-Tag, das der Besuch des Kunden trug (?sub=yt am Link des Affiliates); ohne Tag fehlt das Feld. Siehe Sub-IDs.

Override-Zeilen für Sub-Affiliates

Ist in der Kampagne Sub-Affiliate-Recruiting eingeschaltet, erzeugt jeder Verkauf eines Geworbenen ein zweites Ereignis conversion.created für die Override-Zeile des Werbers. Override-Ereignisse haben denselben Aufbau, tragen aber attributionMethod = "sub_affiliate_override", amountCents = 0, commissionCents > 0 und eine parentAffiliateConversionId, die auf die conversionId des zugrunde liegenden Verkaufs zeigt.

{
  "conversionId": "c0nv0override-...",
  "affiliateId": "01234567-A-...",
  "programId": "0789abcd-...",
  "amountCents": 0,
  "currency": "USD",
  "commissionCents": 200,
  "status": "Pending",
  "attributionMethod": "sub_affiliate_override",
  "parentAffiliateConversionId": "c0nv0parent-...",
  "type": "OneTime"
}

Achtung: Filtert dein Handler Ereignisse conversion.created nach amountCents > 0, um Ereignisse ohne Umsatz auszusortieren, verlierst du stillschweigend alle Override-Ereignisse. Filtere stattdessen nach commissionCents > 0 oder nach status. Alle Details zur Payload: Sub-Affiliate-API.

conversion.refunded

{
  "conversionId": "c0nv0001-...",
  "affiliateId": "01234567-...",
  "programId": "0789abcd-...",
  "refundedAmountCents": 9900,
  "commissionReversedCents": 1980,
  "cumulativeRefundedAmountCents": 9900,
  "isFullRefund": true,
  "currency": "USD",
  "refundedAt": "2026-05-12T14:00:00Z",
  "attributionMethod": "coupon"
}

attributionMethod trägt die Attribution des ursprünglichen Verkaufs (dieselben Werte wie bei conversion.created), damit du Erstattungen nach Quelle abgleichen kannst. cumulativeRefundedAmountCents ist die laufende Summe, die für die Conversion erstattet wurde (Teilerstattungen summieren sich), und isFullRefund ist true, sobald der volle Verkaufsbetrag erstattet ist. Hatte der erstattete Verkauf eine Override-Zeile für einen Werber, trägt das Erstattungsereignis dieser Override-Zeile zusätzlich parentAffiliateConversionId.

conversion.approved

Wird ausgelöst, wenn eine Conversion im Status Pending ausdrücklich freigegeben wird. Diese Aktion gibt es derzeit in der Verwaltungsansicht in Stripe. Die Payload ist minimal:

{ "conversionId": "c0nv0001-..." }

Conversions im Status Pending, die in einen Auszahlungslauf aufgenommen werden, erzeugen payout.created statt eines eigenen Freigabeereignisses. Willst du ein Signal für „freigegeben ODER ausgezahlt“, abonniere also beide und dedupliziere über conversionId.

affiliate.created

{
  "affiliateId": "01234567-...",
  "programId": "0789abcd-...",
  "email": "newaffiliate@example.com",
  "fullName": "New Affiliate",
  "status": "Pending",
  "slug": "newaffiliate",
  "applicationAnswers": {
    "website": "https://example.com",
    "how_will_promote": "Newsletter and YouTube reviews",
    "custom_promo_code": "SPRING20"
  }
}

Enum status: Pending | Approved | Rejected | Paused | Banned. slug ist der automatisch erzeugte Standard-Slug des Tracking-Links. Bewerbungen, die über das öffentliche Formular eingehen, tragen außerdem ein Feld source mit dem Wert "public_apply".

applicationAnswers enthält jede Antwort aus dem Bewerbungsformular deiner Kampagne, mit dem Feldschlüssel als Key, den du im Editor der Anmeldeseite festgelegt hast (eingebaute Felder nutzen ihre Systemschlüssel website, audience, why_interested; eigene Felder den Schlüssel, den du gewählt hast). So kommt ein eigenes Feld wie ein Promo-Code oder ein Social-Media-Handle in deine Automatisierung (Make, Zapier, n8n, dein CRM), ohne dass du das Feld für den vollständigen Namen zweckentfremden musst. Der Key fehlt, wenn der Affiliate ohne Formular angelegt wurde (von einem Admin hinzugefügt oder per Sammelimport). Signale der Betrugsprüfung, die auf der Prüfseite der Marke stehen, sind nie enthalten.

affiliate.approved

{
  "affiliateId": "01234567-...",
  "programId": "0789abcd-...",
  "email": "newaffiliate@example.com",
  "fullName": "New Affiliate",
  "approvedAt": "2026-05-12T11:30:00Z",
  "applicationAnswers": {
    "website": "https://example.com",
    "how_will_promote": "Newsletter and YouTube reviews"
  }
}

Wird eine Bewerbung beim Eingang automatisch freigegeben (die automatische Freigabe ist in der Kampagne eingeschaltet), trägt die Payload zusätzlich "autoApproved": true. applicationAnswers hat denselben Aufbau wie bei affiliate.created und gibt die Antworten wieder, die zum Zeitpunkt der Freigabe vorliegen. Eine manuelle Freigabe nach einer zweistufigen Bewerbung enthält also auch die Antworten aus Schritt 2.

affiliate.application_completed

{
  "affiliateId": "01234567-...",
  "programId": "0789abcd-...",
  "email": "newaffiliate@example.com",
  "fullName": "New Affiliate",
  "status": "Approved",
  "applicationAnswers": {
    "website": "https://example.com",
    "how_will_promote": "Newsletter and YouTube reviews",
    "audience": "10k newsletter subscribers"
  }
}

Wird ausgelöst, wenn ein Bewerber Schritt 2 eines zweistufigen Bewerbungsformulars abschickt. affiliate.created (und in Kampagnen mit automatischer Freigabe auch affiliate.approved) wurde schon bei Schritt 1 ausgelöst, bevor es diese Antworten gab. Hängt deine Automatisierung an einem Feld aus Schritt 2, abonniere also dieses Ereignis. Es wird erneut ausgelöst, wenn der Bewerber Schritt 2 noch einmal abschickt; die Map der Antworten enthält immer den vollständigen aktuellen Stand.

campaign_coupon.created

Wird ausgelöst, wenn eine Marke einen übergeordneten Kampagnengutschein anlegt. Der zugehörige Stripe-coupon wird gleichzeitig angelegt (Rekomi führt beides in einer einzigen Transaktion aus).

{
  "campaignCouponId": "c0upon01-...",
  "programId": "0789abcd-...",
  "name": "Summer 20% off",
  "discountType": "PercentOff",
  "discountValue": 20,
  "duration": "Once",
  "selfMintMode": "Disabled",
  "grantedAffiliateCount": 0,
  "createdAt": "2026-05-17T20:00:00Z"
}

selfMintMode legt fest, wer sich aus diesem Gutschein selbst einen Code erstellen darf (Disabled, AllAffiliates oder SpecificAffiliates); grantedAffiliateCount gibt an, wie viele Affiliates auf der Positivliste stehen, wenn der Modus SpecificAffiliates ist. discountCurrency erscheint nur bei Gutscheinen vom Typ AmountOff (Felder mit null fehlen).

campaign_coupon.updated

Wird ausgelöst, wenn eine Marke die bearbeitbaren Felder eines Kampagnengutscheins ändert (Name, Schalter für selbst erstellte Codes, Schalter für den Vorrang vor der Klick-Attribution). Die Rabattbedingungen sind nach den Regeln von Stripe unveränderlich und tauchen nie in einer Payload von .updated auf.

{
  "campaignCouponId": "c0upon01-...",
  "programId": "0789abcd-...",
  "name": "Summer 20% off (updated)",
  "allowAffiliateSelfMint": true,
  "overridesClickAttribution": true,
  "selfMintMode": "AllAffiliates",
  "grantedAffiliateCount": 0,
  "updatedAt": "2026-05-17T20:30:00Z"
}

campaign_coupon.deactivated

Wird ausgelöst, wenn eine Marke einen Kampagnengutschein archiviert. Alle untergeordneten Codes der einzelnen Affiliates werden ebenfalls deaktiviert, in Stripe und bei Rekomi; deactivatedChildCount gibt an, wie viele.

{
  "campaignCouponId": "c0upon01-...",
  "programId": "0789abcd-...",
  "archivedAt": "2026-05-17T21:00:00Z",
  "deactivatedChildCount": 12
}

campaign_coupon.deleted

Wird ausgelöst, wenn eine Marke einen Kampagnengutschein endgültig löscht. Endgültiges Löschen ist nur möglich, wenn nie Codes für einzelne Affiliates erstellt wurden; sonst liefert die API 422 has_affiliate_coupons und erzwingt stattdessen eine Archivierung.

{
  "campaignCouponId": "c0upon01-...",
  "programId": "0789abcd-..."
}

affiliate_coupon.created

Wird ausgelöst, wenn ein einlösbarer Code für einen Affiliate erstellt wird (von der Marke ODER vom Affiliate selbst).

{
  "affiliateCouponId": "ac0upon0-...",
  "campaignCouponId": "c0upon01-...",
  "affiliateId": "01234567-...",
  "programId": "0789abcd-...",
  "code": "SARAH20",
  "createdAt": "2026-05-17T20:05:00Z",
  "source": "self_minted"
}

source ist nur vorhanden, wenn der Affiliate sich den Code unter /a selbst erstellt hat. Bei Codes, die die Marke erstellt, fehlt das Feld.

affiliate_coupon.deactivated

Wird ausgelöst, wenn eine Marke einen einzelnen einlösbaren Code eines Affiliates widerruft. Der Stripe-promotion_code wird deaktiviert und die Zeile bei Rekomi als inaktiv markiert.

{
  "affiliateCouponId": "ac0upon0-...",
  "campaignCouponId": "c0upon01-...",
  "affiliateId": "01234567-...",
  "programId": "0789abcd-...",
  "code": "SARAH20",
  "revokedAt": "2026-05-17T21:05:00Z"
}

Währung bei Auszahlungsereignissen

payout.created, payout.paid und payout.failed tragen alle ein Feld currency. Der Wert ist immer vorhanden und ist der ISO-4217-Code der Auszahlung selbst. Auszahlungen laufen in USD: Ein Auszahlungslauf fasst die Einnahmen eines Affiliates über alle Währungen hinweg zu einer USD-Auszahlung zusammen (jede Provision in einer anderen Währung als USD wird zum veröffentlichten Mittelkurs am Tag des Laufs umgerechnet, pro Position festgehalten). currency ist bei diesen Ereignissen also "USD", und ein Affiliate mit Einnahmen in zwei Währungen bekommt trotzdem eine Auszahlung pro Lauf. Auszahlungen, die über den älteren API-Vertrag mit nur einer Währung angelegt wurden, tragen stattdessen die Währung dieses Laufs. Der ursprüngliche Betrag pro Position, der angewandte Kurs und das Kursdatum stehen nicht in den Ereignissen selbst: Lies sie aus GET /api/v1/payouts/{id} unter lineItems[] (nativeAmountCents, nativeCurrency, fxRate, fxRateDate; bei älteren Zeilen null). So gleicht ein System, das ein conversion.created in JPY gespeichert hat, es mit dem USD-payout.created ab, das es später auszahlt. Das Auszahlungsmodell beschreibt die Seite Berichte in mehreren Währungen, die Positivliste der unterstützten Währungen steht unter Conversion-Währung.

payout.created (einmal pro Empfänger und Lauf)

{
  "payoutId": "payout-uuid",
  "affiliateId": "01234567-...",
  "amountCents": 9800,
  "currency": "USD",
  "method": "StripeConnect",
  "status": "Pending",
  "lineItemConversionIds": ["c0nv0001-...", "c0nv0002-..."],
  "periodStart": "2026-04-01",
  "periodEnd": "2026-04-30"
}

Enum method: StripeConnect (Stripe Express) | PayPal | GlobalPayouts (Banküberweisung über Stripe); ältere Auszahlungen können auch Manual zeigen. Dieselben Werte erscheinen bei payout.paid und payout.failed, betrachte die Liste also nicht als auf die ersten beiden beschränkt. currency ist der ISO-4217-Code genau dieser Auszahlung: "USD" bei zusammengefassten Läufen, bei denen ein Affiliate mit Einnahmen in mehreren Währungen trotzdem nur EIN payout.created pro Lauf bekommt (seine Provisionen in anderen Währungen als USD werden pro Position zum Mittelkurs am Tag des Laufs umgerechnet). Nur Auszahlungen über den älteren API-Vertrag mit nur einer Währung tragen eine andere Währung.

payout.paid

Neue Auszahlungen laufen über den Auszahlungsweg Stripe Express, PayPal oder Banküberweisung (Stripe Global Payouts), und die Payload von payout.paid trägt neben method eine Referenz für den jeweiligen Weg: Auszahlungen über Stripe Express tragen method StripeConnect mit stripeTransferId; Auszahlungen über PayPal tragen method PayPal mit paypalBatchId und paypalTransactionId; Banküberweisungen tragen method GlobalPayouts mit gpOutboundPaymentId. payout.paid und payout.failed haben kein Feld status (der Ereignistyp ist der Status), und keines der beiden trägt lineItemConversionIds oder die Zeitraumfelder. Die ältere Methode Manual und externalTransactionId im Beispiel unten kommen nur bei historischen Auszahlungen vor, die erfasst wurden, bevor die Funktion „als bezahlt markieren“ entfernt wurde.

{
  "payoutId": "payout-uuid",
  "affiliateId": "01234567-...",
  "amountCents": 9800,
  "currency": "USD",
  "method": "Manual",
  "externalTransactionId": "wire-2026-04-30-abc123",
  "paidAt": "2026-05-01T09:00:05Z"
}

payout.paid (Methode Stripe Connect; gezeigt ist ein älterer Lauf mit nur einer Währung)

{
  "payoutId": "payout-uuid",
  "affiliateId": "01234567-...",
  "amountCents": 1200000,
  "currency": "JPY",
  "method": "StripeConnect",
  "stripeTransferId": "tr_1ABC...",
  "paidAt": "2026-05-01T09:00:05Z"
}

Das Beispiel zeigt einen älteren Lauf mit nur einer Währung; nur so kommt heute noch eine andere Auszahlungswährung als USD vor. JPY hat keine Untereinheit: amountCents mit 1200000 steht für 1,200,000 JPY (nicht 12,000.00 JPY). Behandle die Ganzzahl als kleinste unteilbare Einheit, die die Währung kennt.

payout.paid (Banküberweisung über Stripe Global Payouts)

{
  "payoutId": "payout-uuid",
  "affiliateId": "01234567-...",
  "amountCents": 9800,
  "currency": "USD",
  "method": "GlobalPayouts",
  "gpOutboundPaymentId": "obp_1ABC...",
  "paidAt": "2026-05-01T09:00:05Z"
}

Affiliates mit Banküberweisung werden in USD aus dem Stripe-Guthaben von Rekomi bezahlt und erhalten ihre Landeswährung zum Umrechnungskurs von Stripe; gpOutboundPaymentId ist die Referenz der ausgehenden Zahlung bei Stripe.

payout.failed

{
  "payoutId": "payout-uuid",
  "affiliateId": "01234567-...",
  "amountCents": 9800,
  "currency": "USD",
  "method": "StripeConnect",
  "failureReason": "Affiliate has no Stripe Connect account",
  "failedAt": "2026-05-01T09:00:05Z"
}

failureReason ist eine kurze, lesbare Meldung: entweder ein Text von Rekomi wie "Affiliate has no Stripe Connect account" oder die durchgereichte Fehlermeldung des Zahlungsanbieters. Verzweige nicht anhand exakter Werte. method trägt dieselben Werte wie bei payout.paid (StripeConnect, PayPal, GlobalPayouts, älteres Manual), und failedAt ist bei jedem Ereignis payout.failed vorhanden.

Signatur-Header

Jeder POST enthält:

X-Rekomi-Signature: t=1715366423,v1=8f4e2c5b...
X-Rekomi-Timestamp: 1715366423
X-Rekomi-Event: conversion.created
X-Rekomi-Delivery-Id: 8f14e45f-ceea-467a-bb2c-d8b8c0afc8d8
  • X-Rekomi-Signature: HMAC-SHA256 im Format t=<unix-seconds>,v1=<hex> (kompatibel mit Stripe). Hinweis: Der S2S-Tracking-Endpunkt nutzt einen ähnlichen Header, kennzeichnet die Signatur aber mit sig= statt v1=; ausgehende Webhooks und eingehendes S2S werden in getrennten Code-Pfaden geprüft.
  • X-Rekomi-Timestamp: der Unix-Zeitstempel in Sekunden, über den die Signatur berechnet wird (derselbe Wert wie im Feld t=).
  • X-Rekomi-Event: der Ereignistyp. Praktisch, um zu routen, ohne den Anfrageinhalt zu parsen.
  • X-Rekomi-Delivery-Id: die ID der Zustellung (eine einfache UUID). Nutze sie zur Deduplizierung; Wiederholungen verwenden dieselbe ID.

Signatur prüfen

import crypto from "node:crypto";

const SECRET = process.env.REKOMI_WEBHOOK_SECRET!;

function verify(rawBody: string, headerValue: string): boolean {
  const parts = Object.fromEntries(
    headerValue.split(",").map((p) => p.split("="))
  );
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;
  const ageSec = Math.abs(Date.now() / 1000 - Number(t));
  if (ageSec > 300) return false; // 5-minute replay window

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(v1);
  const b = Buffer.from(expected);
  // timingSafeEqual throws on length mismatch, so guard first.
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

Nutze crypto.timingSafeEqual (oder die Entsprechung in deiner Sprache), um Timing-Angriffe zu verhindern. Vergleiche zuerst die Längen und gib bei Abweichung false zurück, weil timingSafeEqual einen Fehler wirft, wenn die Buffer unterschiedlich lang sind. Lehne die Anfrage ab, wenn der Vergleich fehlschlägt.

Verhalten bei Wiederholungen

Antwortet dein Endpunkt mit einem Status außerhalb von 2xx (oder läuft nach 10 Sekunden in einen Timeout), wiederholt Rekomi die Zustellung mit sich verdoppelndem Abstand. Die Versuche erfolgen etwa 30 Sekunden nach dem Ereignis, dann ungefähr 1, 2, 4 und 8 Minuten nach dem Ereignis (der Abstand verdoppelt sich mit jedem Versuch), insgesamt 5 Versuche:

VersuchUngefähr nach dem Ereignis
130 Sekunden
21 Minute
32 Minuten
44 Minuten
58 Minuten

Antwortet dein Endpunkt mit 429 oder 503 und einem Retry-After-Header, halten wir uns daran, begrenzt auf mindestens 30 Sekunden und höchstens 1 Stunde. Nach Versuch 5 wird die Zustellung als fehlgeschlagen markiert und nicht mehr wiederholt. Mit der Schaltfläche Testen (siehe unten) schickst du beim Debuggen ein frisches Beispiel.

Eine 2xx-Antwort beendet die Wiederholungen. Der Antwortinhalt spielt keine Rolle, nur der Statuscode zählt.

Idempotenz auf deiner Seite

Wiederholungen verwenden dieselbe X-Rekomi-Delivery-Id. Nutze sie als Idempotenzschlüssel (idempotency key) zur Deduplizierung, damit eine wiederholte Zustellung bei dir nicht doppelt verarbeitet wird. Pseudocode:

const deliveryId = req.headers["x-rekomi-delivery-id"];
if (await alreadyProcessed(deliveryId)) {
  return res.status(200).send("already processed");
}
await processEvent(payload);
await markProcessed(deliveryId);
res.status(200).send("ok");

Testzustellungen

Klicke unter /dashboard/settings/webhooks neben einem beliebigen Endpunkt auf „Testen“. Rekomi sendet ein künstliches Ereignis mit type = "test" oder ein realistisches Beispiel für einen der neun Trigger-Ereignistypen (conversion.created, conversion.refunded, affiliate.created, affiliate.approved, payout.created, payout.paid, payout.failed, campaign_coupon.created, affiliate_coupon.created). Für andere abonnierte Typen gibt es nur die generische Test-Payload, und der Endpunkt lehnt einen Test für einen Ereignistyp ab, den er nicht abonniert hat. So prüfst du, ob dein Handler richtig angebunden ist, bevor du dich auf echte Ereignisse verlässt.

Zustellverlauf

Jeder Endpunkt hat eine Ansicht mit seinem Zustellverlauf. Klappe einen Endpunkt aus, um seine Zustellungen seitenweise (50 pro Seite) durchzusehen, mit:

  • Ereignistyp
  • ID der Zustellung
  • Statuscode der Antwort
  • Auszug aus dem Antwortinhalt (die ersten 500 Zeichen)
  • Anzahl der Versuche
  • Zeitpunkt des Abschlusses

Hilfreich, wenn du herausfinden willst, warum eine Benachrichtigung nicht angekommen ist.

Signing Secret rotieren

Beim Rotieren tauschst du das Signing Secret eines Endpunkts aus, ohne seine URL oder seine abonnierten Ereignisse zu ändern. Klicke bei einem beliebigen Endpunkt auf „Rotieren“. Das neue Secret wird GENAU EINMAL angezeigt. Das alte Secret funktioniert sofort nicht mehr. Es gibt keine Übergangszeit, rotiere also in einem Wartungsfenster.

Checkliste zur Sicherheit

  • Prüfe immer die HMAC-Signatur. Vertraue nie einer unsignierten Anfrage.
  • Prüfe immer, ob der Zeitstempel innerhalb von 5 Minuten liegt.
  • Nutze für den Vergleich immer crypto.timingSafeEqual.
  • Verlange für deine Webhook-URL immer HTTPS (das Dashboard setzt das durch).
  • Behandle Wiederholungen immer idempotent über die ID der Zustellung.
  • Bewahre das Signing Secret in einem Secrets-Manager auf. Schreibe es nie in ein Log.