RekomiRekomiBlogPricing
Rekomi Docs
Rekomi Docs
Welcome to Rekomi
API overviewAuthenticationOAuth 2.0Server-to-server trackingTracking script & window.RekomiTrack leads and signupsNo-code & non-Stripe checkoutsCustom domainConversion currencyCoupon code trackingSub-affiliate recruiting APIWebhooksZapierWhite-label embedMCP serverAPI reference
For developers
|Developers|

Webhooks

Subscribe to events, verify signatures, and handle retries.

Rekomi posts events to your webhook endpoints as they happen. Conversions, payouts, affiliate lifecycle: anything you might want to react to from your own systems.

Plan tier required: Starter or higher to create / rotate / update / test webhook endpoints. Existing endpoints continue to fire if you downgrade to Free (we don't strand your integration mid-cycle), but you'll need a paid plan to add new ones or change configuration. Trial users (14-day free trial on any paid tier) can configure webhooks freely. The Zapier integration uses this same surface under the hood, which is why webhooks are available on every paid plan.

Create a webhook subscription

The instructions below describe direct webhook subscriptions you manage yourself. If you want the same events to fan out to Slack, Gmail, HubSpot, Notion, Google Sheets, or any of the 8,000+ apps Zapier supports, without writing code or hosting an endpoint, use the Zapier integration instead. Zapier consumes this same webhook surface as a REST-hook subscriber: turning a Zap on registers a subscription against your org, and turning it off cleanly unregisters it, all transparently to you.

/dashboard/settings/webhooks. Click "Add endpoint".

Required:

  • URL: an HTTPS endpoint you control.
  • Events: pick one or more from the event types below, or * for everything.

Click Create. A signing secret is shown EXACTLY ONCE. Save it; it cannot be recovered.

The same signing secret will sign every payload sent to this endpoint. If you rotate the secret, the old one stops being used immediately.

Event types

Currently supported:

  • * (all events)
  • conversion.created (new conversion attributed)
  • conversion.approved (refund window passed)
  • conversion.refunded (commission clawed back)
  • payout.created (payout batch created with this affiliate as a recipient)
  • payout.paid (payout settled)
  • payout.failed (payout transfer failed)
  • affiliate.created (new affiliate application or approved network match)
  • affiliate.approved (affiliate status moved to Approved)
  • campaign_coupon.created (brand created a parent campaign coupon, owns the Stripe coupon)
  • campaign_coupon.updated (campaign coupon settings changed, name + toggles)
  • campaign_coupon.deactivated (campaign coupon archived; child per-affiliate codes also revoked)
  • campaign_coupon.deleted (campaign coupon hard-deleted; only possible when no per-affiliate codes existed)
  • affiliate_coupon.created (per-affiliate redeemable code minted; brand-side OR affiliate self-mint)
  • affiliate_coupon.deactivated (per-affiliate redeemable code revoked)

You can subscribe to a subset on each endpoint. Different endpoints can subscribe to different events. See event payload shapes below for the exact data field for each type.

Nine of these events also surface as ready-made Zapier triggers: conversion.created, conversion.refunded, affiliate.created, affiliate.approved, payout.created, payout.paid, payout.failed, campaign_coupon.created, and affiliate_coupon.created. If your downstream consumer is a Zapier-supported app, you do not need to host or sign a webhook endpoint at all, Zapier handles the subscription, the HTTPS endpoint, and the signature verification on your behalf.

Payload envelope

Every delivery is a JSON object with the same outer envelope. The data field changes per event type (full shapes documented below).

{
  "id": "evt_01HVABCDEFGHIJKLMN",
  "type": "conversion.created",
  "createdAt": "2026-05-11T03:14:25.000Z",
  "organizationId": "01HORG123...",
  "data": { /* see per-event tables below */ }
}
  • id: unique event identifier. Use for de-dup on your side (Rekomi also sets X-Rekomi-Delivery-Id for retry de-dup; see the headers section).
  • type: the event type from the list above.
  • createdAt: ISO-8601 UTC timestamp.
  • organizationId: your org id. Always your own org; you never receive events for other orgs (defense-in-depth filter at the dispatcher).
  • data: event-specific payload.

Event-specific data shapes

The shapes below are verified against the live event emitter code; these are the exact field names you'll receive.

conversion.created

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

status enum: Pending | Approved | Paid | Denied | Refunded. type enum: OneTime | Subscription.

Sub-affiliate override rows

If the campaign has sub-affiliate recruiting enabled, every parent sale produces a second conversion.created event for the recruiter's override row. Override events look identical in shape but carry attributionMethod = "sub_affiliate_override", amountCents = 0, commissionCents > 0, and a parentAffiliateConversionId pointing at the parent sale's conversionId.

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

Warning: if your handler filters conversion.created events by amountCents > 0 to drop zero-revenue events, you will silently lose all override events. Filter on commissionCents > 0 instead, or on status. Full payload details: Sub-affiliate API.

conversion.refunded

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

conversion.approved

Reserved. Today, conversion approval fires implicitly when a conversion is included in a payout.created batch (Rekomi flips Pending → Approved at payout-build time). If you need an explicit "refund window passed but no payout yet" signal, subscribe to both conversion.approved and payout.created and de-dup on conversionId. A future release will move conversion approval to a dedicated scheduled job and emit this independently.

affiliate.created

{
  "affiliateId": "01234567-...",
  "programId": "0789abcd-...",
  "email": "newaffiliate@example.com",
  "fullName": "New Affiliate",
  "status": "Pending",
  "slug": "newaffiliate"
}

status enum: Pending | Approved | Rejected | Paused. slug is the auto-generated default tracking link slug.

affiliate.approved

{
  "affiliateId": "01234567-...",
  "programId": "0789abcd-...",
  "email": "newaffiliate@example.com",
  "fullName": "New Affiliate",
  "approvedAt": "2026-05-12T11:30:00Z"
}

campaign_coupon.created

Fires when a brand creates a parent campaign coupon. The corresponding Stripe coupon is created at the same time (Rekomi runs them in a single transaction).

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

campaign_coupon.updated

Fires when a brand updates a campaign coupon's editable fields (name, self-mint toggle, click-override toggle). Discount semantics are immutable per Stripe's rules and never appear in an .updated payload.

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

campaign_coupon.deactivated

Fires when a brand archives a campaign coupon. All child per-affiliate codes are also deactivated in Stripe and locally; deactivatedChildCount reports how many.

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

campaign_coupon.deleted

Fires when a brand hard-deletes a campaign coupon. Hard-delete is only possible when no per-affiliate codes were ever minted; otherwise the API returns 422 has_affiliate_coupons and forces an archive instead.

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

affiliate_coupon.created

Fires when a per-affiliate redeemable code is minted (brand-side OR affiliate self-mint).

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

source is only present when the affiliate self-minted from /a. Brand-side mints omit the field.

affiliate_coupon.deactivated

Fires when a brand revokes a single per-affiliate redeemable code. The Stripe promotion_code is deactivated and the Rekomi row is marked inactive.

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

Currency on payout events

payout.created, payout.paid, and payout.failed all carry a currency field. The value is always present and is the ISO 4217 code of the payout itself. Because payouts are batched per (affiliate, currency), a single batch run can emit multiple payout.created events for the same affiliate with different currency values, for example one USD payout and one EUR payout if the affiliate earned in both. See Multi-currency for the full payout-splitting model and Conversion currency for the 31-code allowlist.

payout.created (fires once per recipient per currency)

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

method enum: Manual | StripeConnect | PayPal. currency is the ISO 4217 code of this specific payout. For an affiliate earning in multiple currencies, you will receive one payout.created per currency, each with its own payoutId and lineItemConversionIds.

payout.paid (Manual method)

{
  "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 (Stripe Connect method)

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

Note JPY has no minor unit; amountCents of 1200000 represents 1,200,000 JPY (not 12,000.00 JPY). Treat the integer as the smallest indivisible unit the currency supports.

payout.failed

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

failureReason is a server-generated short string. Common values include "Affiliate has no Stripe Connect account", "insufficient_funds", and "transfer_rejected". Raw Stripe API error bodies are never included.

Signature header

Every POST includes:

X-Rekomi-Signature: t=1715366423,v1=8f4e2c5b...
X-Rekomi-Event: conversion.created
X-Rekomi-Delivery-Id: dlv_01HV...
  • X-Rekomi-Signature: HMAC-SHA256 in t=<unix-seconds>,v1=<hex> format (Stripe-compatible). Note: the S2S tracking endpoint uses a similar header but tags the signature sig= instead of v1=; outbound webhooks and inbound S2S have separate verifier code paths.
  • X-Rekomi-Event: the event type. Convenient for routing without parsing the body.
  • X-Rekomi-Delivery-Id: the delivery attempt id. Use for de-dup; retries reuse the same delivery id.

Verify the signature

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");
  return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

Use crypto.timingSafeEqual (or your language equivalent) to prevent timing attacks. Compare hex strings of equal length. Reject the request if the comparison fails.

Retry semantics

If your endpoint returns a non-2xx status (or times out after 10 seconds), Rekomi retries with exponential backoff:

AttemptDelay after previous
1(immediate)
230 seconds
32 minutes
410 minutes
51 hour

After attempt 5, the delivery is marked Failed and not retried automatically. You can manually re-trigger a failed delivery from the dashboard.

A 2xx response stops retries. The body content does not matter; only the status code.

Idempotency on your side

Retries reuse the same X-Rekomi-Delivery-Id. Use it to de-dup so a retried delivery does not double-process on your side. 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");

Test deliveries

From /dashboard/settings/webhooks, click "Test" next to any endpoint. Rekomi sends a synthetic event with type = test.ping. Use this to verify your handler is wired correctly before relying on real events.

Delivery history

Each endpoint has a delivery history view. Expand any endpoint to see the last 50 deliveries with:

  • Event type
  • Delivery id
  • Response status code
  • Response body excerpt (first 1KB)
  • Attempt count
  • Settled timestamp

Useful for debugging "why didn't I get notified".

Rotating the signing secret

Click "Rotate" on any endpoint. A new secret is shown EXACTLY ONCE. The old secret stops working immediately. There is no overlap window; rotate during a maintenance window.

Security checklist

  • Always verify the HMAC signature. Never trust an unsigned request.
  • Always validate the timestamp is within 5 minutes.
  • Always use crypto.timingSafeEqual for the comparison.
  • Always require HTTPS for your webhook URL (the dashboard enforces this).
  • Always handle retries idempotently using the delivery id.
  • Store the signing secret in a secrets manager. Never log it.

Sub-affiliate recruiting API

Endpoints, override conversion shape, webhook payloads, and apply-form semantics for the bounded 1-tier sub-affiliate model.

Zapier

Connect Rekomi to 8,000+ apps with no code. 9 triggers, 9 actions, 4 searches.

On this page

Create a webhook subscriptionEvent typesPayload envelopeEvent-specific data shapesconversion.createdSub-affiliate override rowsconversion.refundedconversion.approvedaffiliate.createdaffiliate.approvedcampaign_coupon.createdcampaign_coupon.updatedcampaign_coupon.deactivatedcampaign_coupon.deletedaffiliate_coupon.createdaffiliate_coupon.deactivatedCurrency on payout eventspayout.created (fires once per recipient per currency)payout.paid (Manual method)payout.paid (Stripe Connect method)payout.failedSignature headerVerify the signatureRetry semanticsIdempotency on your sideTest deliveriesDelivery historyRotating the signing secretSecurity checklist