More in Developers

Subscriptions, coupon codes and referral tokens

Three things a merchant's server does with a secret key besides reporting one-off sales: tell Refficks about subscriptions, keep partners' discount codes in step, and mint the token the refer-a-friend panel needs. For a developer who already sends events (Send events from your server).

All three take a secret key (rk_live_…) in Authorization: Bearer <SECRET_KEY>. A publishable key is refused with 403 FORBIDDEN.

Subscription events

Refficks keeps a record of each subscription from the events you send, so a customer's page can say what they are on and a partner's page what they brought in. None of this pays anybody. Commission comes from the invoice.paid events, as for any sale.

Event Send it when
trial.started A free trial begins. Send properties.trial_ends_at.
subscription.created A subscription begins.
subscription.updated Its plan, price, quantity, interval or status changes.
subscription.cancelled It ends.
invoice.paid A payment on it succeeds, the first and every renewal. This is what earns commission.
invoice.payment_failed A payment on it fails. The subscription reads as overdue.

Which subscription. Send its id in properties.subscription (properties.subscription_id is read the same way). A subscription.* or trial.started event that leaves it out is taken to be about its own external_id. An invoice.paid that names no subscription is a one-off sale.

Properties a subscription event reads

Property Meaning
plan Your plan's name or id.
amount + currency The price of one unit, in minor units.
quantity What amount is multiplied by: seats, for example.
interval day, week, month or year (daily, weekly, monthly, yearly, annual and annually are read too). Without it there is no figure a month.
interval_count Billed every this many intervals: 3 with month is quarterly.
status Stripe's words: trialing, active, past_due or unpaid (read as overdue), paused, and canceled, cancelled or incomplete_expired (read as cancelled). Another word leaves the status where it was.
cancel_at_period_end true when the customer has decided to leave at the end of the period, false when they changed their mind. Send ends_at with it.
trial_ends_at On trial.started, when the trial ends.
billing_reason On an invoice.paid: subscription_create for the first invoice, subscription_cycle for a renewal, subscription_update for a change of plan part-way through a period.

How they are applied:

  • Newest wins. A status or a price is applied only from an event at least as new as the one that set it last, so events that arrive out of order do not undo each other. Send occurred_at on every one.
  • A cancelled subscription stays cancelled. A later active from your server does not bring it back. A customer who subscribes again is a new subscription, with a new id.
  • The price comes from subscription events. An invoice fills in the plan and currency while nothing better has arrived, never the price: a first invoice is often prorated or discounted.
  • A subscription.created with status: trialing records a trial too, so you need not send trial.started as well.

Renewals keep their partner

An invoice.paid that names a subscription no newer click or code wins is credited to the partner who won that subscription, even after their link's window has closed. Whether a newer partner's click can take a renewal away is the program's attribution rule (How attribution works). Name the subscription on every invoice, or each renewal is judged as a sale of its own.

A renewal of a subscription Refficks never saw begin is credited to nobody. When an invoice.paid says it is a renewal (billing_reason subscription_cycle or subscription_update), names a subscription no partner won, and Refficks has had no subscription.created, no trial.started and no first invoice (subscription_create) for it, its conversion reads "This subscription began before Refficks heard of it; nobody acquired it through a partner." So send subscription.created when a subscription starts, or billing_reason: subscription_create on its first invoice. A person can still credit such a sale by hand.

Example: a subscription and its renewal

curl -X POST https://go.refficks.com/api/v1/events \
  -H "Authorization: Bearer <SECRET_KEY>" \
  -H "Idempotency-Key: sub_8812-created" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "subscription.created",
    "external_id": "sub_8812",
    "occurred_at": "2026-10-16T10:00:00Z",
    "customer": { "external_id": "usr_4821", "email": "jordan@example.com" },
    "properties": {
      "plan": "team",
      "amount": 1900,
      "currency": "USD",
      "quantity": 3,
      "interval": "month",
      "status": "active"
    }
  }'

That reads as USD 57.00 every month. A month later, the renewal:

curl -X POST https://go.refficks.com/api/v1/events \
  -H "Authorization: Bearer <SECRET_KEY>" \
  -H "Idempotency-Key: in_9031-paid" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "invoice.paid",
    "external_id": "in_9031",
    "occurred_at": "2026-11-16T10:00:00Z",
    "customer": { "external_id": "usr_4821" },
    "properties": {
      "subscription": "sub_8812",
      "billing_reason": "subscription_cycle",
      "amount": 5700,
      "net_amount": 5700,
      "currency": "USD"
    }
  }'

Where to see it

  • Customers › the customer › Subscriptions: "Built from what your billing system reported. Nothing here decides a commission." Each subscription shows its plan, a status (On a trial, Active, Payment overdue, Paused or Cancelled), its price and figure a month, and when it started, was first and last paid, and was cancelled. Where you sent no interval it says "How often it is billed was not reported, so there is no figure a month."
  • Partners › the partner › Performance › Subscriptions they brought: what is running now and how the monthly figure moved, per currency. The card is left out for a partner who brought no subscriptions.

Coupon codes over the Server API

A coupon code in Refficks is the record that says this code belongs to this partner, so a checkout that reports the code credits them. Creating one here does not create a discount anywhere: making the code work at your checkout is your payment platform's job, or Refficks's code sync's (Sync partner codes to Stripe or your store).

Create one

curl -X POST https://go.refficks.com/api/v1/server/coupon-codes \
  -H "Authorization: Bearer <SECRET_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "program_id": "<PROGRAM_ID>",
    "partner_id": "<PARTNER_ID>",
    "code": "SAM20"
  }'
Field Notes
program_id, partner_id Required.
code Up to 64 characters, kept in capitals. Left out, one is made from the partner's name.
attribution_priority 0 to 1,000, default 50: which code wins when a basket carries two.
provider Optional free text for your own records.
external_promotion_id, account_id A claim that a promotion code you already made in Stripe is this code. See Claiming a Stripe promotion.
uses_count Refused: "Refficks counts redemptions. A code cannot be created with a count."

Responds: 201 with a coupon_code: id, program_id, partner_id, code, provider, attribution_priority, status, max_uses, uses_count, expires_at, minimum_amount, minimum_amount_currency, first_time_transaction, placements, created_at, updated_at.

Refused with 409 Why
"That code is already in use." Another coupon, or another partner's link, has that code.
"You have already given this partner a code of your own in this program. Turn on more than one code per partner in the program settings to give them another." The program allows one code per partner, not counting the one they were given on joining.
"A coupon needs a code." code was sent empty, and nothing could be made.

Change one

PATCH /server/coupon-codes/{id} takes status, attribution_priority, max_uses, expires_at, minimum_amount with minimum_amount_currency, first_time_transaction, and the claim fields. A minimum_amount of null means the program's default applies, not "no minimum".

These are refused with 422, each with a reason: code ("A code is what somebody types at the checkout. Retire this one and make another instead."), partner_id ("A code belongs to the partner it was made for. Moving it would reassign the sales it has already credited."), program_id and uses_count.

Claiming a Stripe promotion

If you made a promotion code in Stripe by hand, send its id as external_promotion_id so Refficks does not make it a second time, and so a Stripe checkout that uses it credits this partner. Refficks checks the claim before believing it:

Refusal When
409 "This organization has more than one Stripe account connected, so which one holds this promotion code has to be said." You have two Stripe accounts connected; send account_id (the acct_… id).
409 "This organization has no connected Stripe account called acct_…." account_id names an account this workspace has not connected.
409 "Another code already claims that promotion. Take that claim back first." One promotion credits one code.
422 "That promotion's code in Stripe is not SAM20. Claiming it would credit this partner for every checkout that uses another code." Refficks asked Stripe, and the promotion's code is different.
422 "Stripe has no promotion code with that id on this account." The id is wrong, or on another account.
422 "Stripe would not say what that promotion code is, so it was not claimed. Try again in a moment." Stripe did not answer. Retry.
403 "A key made for Zapier cannot claim a promotion made in Stripe for a code. …" A key made for an automation platform. Use a key from Settings › Developer › API keys.

To take a claim back, send "external_promotion_id": null on a PATCH, with account_id to take it back from one account only.

placements on the code says where it exists in a payment provider: a list of { "object": "coupon_placement", "provider", "integration_id", "account_id", "external_promotion_id" }, one per account. An empty list means the code is placed nowhere, the usual case for a code you type into your own checkout. A placement on an account you later disconnect is kept, and counts again when the same account is connected.

Referral widget tokens

The refer-a-friend panel shows a signed-in customer their own referral link and code on your site. The page must not name the customer: the publishable key in it is public, so anybody could ask for anybody's code. Your server, which knows who is signed in, mints a short-lived token instead.

This needs the referral module. Without it the endpoint answers 404.

curl -X POST https://go.refficks.com/api/v1/referral-tokens \
  -H "Authorization: Bearer <SECRET_KEY>" \
  -H "Content-Type: application/json" \
  -d '{ "customer_external_id": "usr_4821", "customer_email": "jordan@example.com" }'
Field Notes
customer_external_id Required, up to 255 characters. Your id for the signed-in customer, the same one you send as customer.external_id on events.
customer_email Optional. Send it on an order confirmation page (see below).

Responds: 200 with

{
  "data": {
    "object": "referral_widget_token",
    "token": "<TOKEN>",
    "expires_in": 600
  }
}

The token lasts ten minutes, so mint one each time you render the page. Put it in the panel's data-token:

<div id="refficks-referral" data-token="<TOKEN>"></div>
<script
  src="https://go.refficks.com/referral-widget.js"
  data-account="<PUBLISHABLE_KEY>">
</script>

data-account is your whole publishable key (rk_pub_…). The Refer-a-friend panel card on Integrations › Developer shows the snippet with only the key's first characters, because the full key is never shown again: paste in the key you saved. The card is there only while the referral module is on.

What the panel shows:

  • A customer who is a referrer sees their link and code.
  • Anybody else sees nothing, unless your referral program's Referrers setting is "Any customer can join". Then they are offered a Get my link button that enrols them and shows their link and code there and then.
  • On an order confirmation page, send customer_email in the token. That page usually loads before the payment has reached Refficks, so without the address there is nobody on file to enrol and the button is not offered. An address already on file is kept; the token's never replaces it.

A bad token, an expired one and a customer who is not a referrer all draw the same nothing, so the panel is safe to leave on the page for everybody.

POST /referral-tokens shares POST /events' allowance of 600 requests a minute.