Rekomi Docs
For developers
Developers

Coupon code tracking

Create per-affiliate Stripe promotion codes, attribute conversions on redemption, and clawback refunds automatically.

Rekomi mints coupons + promotion codes on your behalf and credits the right affiliate when a customer redeems one at checkout. No click cookie required, no tracking script. Use this when your buyers will type a code rather than click a link (influencer programs, podcast spots, offline ads). On a Stripe workspace the codes are minted as Stripe coupons and promotion codes; on a Shopify-connected workspace the per-affiliate codes are minted directly in Shopify as discount codes instead.

Plan tier required: Starter or higher for direct rk_live_* API access. Dashboard sessions work on every paid plan.

Concepts

  • Campaign coupon: the discount semantics. Maps 1:1 to a Stripe coupon. You set the percent or amount, duration, max redemptions, expiry. Once created, the discount is immutable (Stripe rule). To change a discount, archive the parent and create a new one.
  • Affiliate coupon: a redeemable code attached to one affiliate. Maps 1:1 to a Stripe promotion_code. Multiple per affiliate allowed (vanity variants like SARAH20, SARAHSUMMER).

Attribution: when a customer redeems a promotion code at checkout, Rekomi's Stripe webhook sees invoice.discounts[].promotion_code and credits the affiliate who owns it. If overridesClickAttribution is on (recommended, and what the dashboard pre-selects), the coupon wins even when there's a click cookie. Refunds reverse commission, and once the order is fully refunded they mark the conversion refunded and decrement the redemption count.

Authentication

All endpoints accept either a dashboard JWT or a public-API bearer:

Authorization: Bearer rk_live_xxx

Get a key in /dashboard/settings/api-keys. Reads require read scope; writes require read_write.

Create a campaign coupon

This sets up the discount rules that per-affiliate codes will inherit.

POST /api/v1/campaigns/{programId}/coupons

Request body:

{
  "name": "Summer 20% off",
  "discountType": "PercentOff",
  "discountValue": 20,
  "discountCurrency": null,
  "duration": "Once",
  "durationInMonths": null,
  "maxRedemptionsPerCode": 100,
  "expiresAt": "2026-12-31T23:59:59Z",
  "selfMintMode": "Disabled",
  "grantedAffiliateIds": [],
  "maxCodesPerAffiliate": 0,
  "overridesClickAttribution": true,
  "productRefs": []
}

Validation:

  • discountType is PercentOff (discountValue 1-100) or AmountOff (discountValue positive cents, discountCurrency required ISO 4217).
  • duration is Once | Repeating | Forever. When Repeating, durationInMonths is required (1-94).
  • expiresAt must be in the future when set.
  • maxRedemptionsPerCode ≥ 1 when set.
  • productRefs: optional array of Stripe Product ids (prod_...) to limit the coupon to. Max 100 entries. Omit or send an empty array for a whole-order coupon (the default). Creating campaign coupons via this endpoint requires a connected Stripe rail; workspaces without one get 422 stripe_not_connected. productRefs must be Stripe Product ids and only apply to Stripe-minted codes today.

Self-mint fields:

  • selfMintMode: "Disabled" | "AllAffiliates" | "SpecificAffiliates". Controls who may self-mint a vanity code from this coupon. Self-mint runs on the Stripe rail today: affiliates of workspaces without a mintable Stripe rail (including Shopify-connected stores) are not offered self-mint, and the mint endpoint returns 422 stripe_not_connected there.
  • grantedAffiliateIds: the allow-list of affiliate ids, used only when selfMintMode is "SpecificAffiliates". Removing an affiliate from the list later also revokes any codes they already minted from this coupon.
  • maxCodesPerAffiliate: positive cap on the number of active codes a single affiliate may hold from this coupon; 0 or omitted means unlimited.
  • allowAffiliateSelfMint is still accepted as a legacy alias and mirrors the mode: true maps to "AllAffiliates", false to "Disabled".

Creating a coupon requires a live payment connection first: with no Stripe connection you get 422 stripe_not_connected.

Response: 201 Created with the full campaign coupon record including stripeCouponId and productRefs (empty array when the coupon applies to the whole order).

curl -X POST https://api.rekomi.com/api/v1/campaigns/$PROGRAM_ID/coupons \
  -H "Authorization: Bearer $REKOMI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer 20% off",
    "discountType": "PercentOff",
    "discountValue": 20,
    "duration": "Once",
    "selfMintMode": "Disabled",
    "overridesClickAttribution": true
  }'
const res = await fetch(
  `https://api.rekomi.com/api/v1/campaigns/${programId}/coupons`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.REKOMI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: "Summer 20% off",
      discountType: "PercentOff",
      discountValue: 20,
      duration: "Once",
      selfMintMode: "Disabled",
      overridesClickAttribution: true,
    }),
  },
);
const coupon = await res.json();

List, update, archive, delete

Manage existing campaign coupons with these endpoints.

GET    /api/v1/campaigns/{programId}/coupons
GET    /api/v1/campaigns/{programId}/coupons/{id}
PATCH  /api/v1/campaigns/{programId}/coupons/{id}        # editable: name, self-mint settings, attribution toggle
POST   /api/v1/campaigns/{programId}/coupons/{id}/archive
DELETE /api/v1/campaigns/{programId}/coupons/{id}        # only when no per-affiliate codes exist

PATCH accepts name, the self-mint settings (selfMintMode, grantedAffiliateIds, and the legacy allowAffiliateSelfMint), overridesClickAttribution, and maxCodesPerAffiliate. Discount semantics stay immutable per Stripe's rules. Archive sets isActive=false, deactivates all child codes on whichever platform minted them, and marks them revoked locally.

Generate a per-affiliate coupon

This mints an individual redeemable code tied to one affiliate.

POST /api/v1/affiliates/{affiliateId}/coupons

Request body:

{
  "campaignCouponId": "c0upon01-...",
  "code": "SARAH20"
}
  • campaignCouponId is required. The campaign coupon must belong to the same program as the affiliate.
  • code is optional. When null, Rekomi generates an 8-character Crockford-style code (no 0/O, 1/I/L for transcribability). When set, must match ^[A-Z0-9_-]{3,32}$.
  • Codes are unique per organization among active codes; a revoked code frees its string for re-minting. Duplicates return 422 code_already_taken.
  • On a Shopify-connected workspace, a stale Shopify token fails per-affiliate code generation with 422 shopify_reconnect_required; reconnect Shopify from the dashboard and retry.

Response: 201 Created with the new affiliate coupon record including stripePromotionCodeId.

List + revoke

List an affiliate's codes, or deactivate one.

GET    /api/v1/affiliates/{affiliateId}/coupons
DELETE /api/v1/affiliates/{affiliateId}/coupons/{id}    # revokes (deactivates in Stripe + marks inactive)

Affiliate self-mint

When the brand sets selfMintMode to "AllAffiliates" (or names the affiliate in grantedAffiliateIds under "SpecificAffiliates") on a campaign coupon, that affiliate can mint their own vanity codes from /a:

POST /api/me/coupons/mint

Request body:

{
  "campaignCouponId": "c0upon01-...",
  "code": "SARAH-SPRING"
}

The endpoint runs under affiliate-side auth (Clerk JWT, no org context). The caller must belong to the same program the campaign coupon targets, be on the coupon's self-mint allow-list, be Approved status, and stay within maxCodesPerAffiliate if a cap is set; the vanity code must satisfy the regex above.

Attribution behavior

On every Stripe invoice.paid or customer.subscription.created event, Rekomi extracts discounts[].promotion_code from the payload. If the promotion code maps to one of your AffiliateCoupon rows:

  1. overridesClickAttribution=true (default): attribute to the coupon's owning affiliate even when a click cookie exists. Conversion.attributionMethod = "coupon".
  2. overridesClickAttribution=false: check metadata + customer history first. If those don't credit anyone, fall back to the coupon owner. attributionMethod = "metadata_or_customer" or "coupon" depending on which won.
  3. Checkout metadata or customer history credits the same affiliate who owns the redeemed code: the conversion records attributionMethod = "coupon_with_metadata". (The webhook never consults click cookies for this case.)

The conversion row carries affiliateCouponId pointing at the redeemed coupon. Refund cascade uses this to decrement redemptionCount and lastRedeemedAt is also bumped on every successful conversion.

Free / 100%-off coupon signups become leads

When a referred visitor redeems a fully discounting (100%-off) coupon, the subscription is created but no paid invoice ever fires. When lead tracking is enabled (the default), Rekomi records that signup as a lead (a lead-only row, no commission), credited to the affiliate who owns the code, so it appears in your funnel even if the customer never pays. The coupon's redemption count still increments, and if the customer later starts paying, that sale is matched back to the lead and commissioned. See Track leads and signups.

Refunds

When Stripe fires charge.refunded, Rekomi:

  1. Finds the conversion by payment_intent_id or charge_id.
  2. Reverses commission proportionally to the refunded amount.
  3. Sets the conversion status to Refunded and refundedAt once cumulative refunds cover the full original amount; a partial refund keeps the original status.
  4. If affiliateCouponId is set, decrements the coupon's redemptionCount only on the refund that transitions the conversion to fully refunded (never for partial refunds, and guarded so replays never drive the counter below zero).
  5. Emits conversion.refunded webhook.

Webhook events

See the Webhooks reference. Coupon events come in two families. campaign_coupon.created, campaign_coupon.updated, campaign_coupon.deactivated, and campaign_coupon.deleted fire for the parent coupon. affiliate_coupon.created, affiliate_coupon.deactivated, and affiliate_coupon.revoked fire for individual per-affiliate codes. Subscribe to * for everything, or list each coupon event name explicitly at endpoint creation; prefix wildcards like campaign_coupon.* are not supported.

Rate limits

The campaign coupon and per-affiliate coupon endpoints share the standard authenticated rate-limit bucket (600 req/min per key; see Rate limits). Stripe-side rate limits apply independently; bulk-minting more than a few codes per second can fail with 422 stripe_create_failed. Each request carries a stable idempotency key on the payment-provider side, so retrying the request from your side is safe and will not double-create codes.