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_aton every one. - A cancelled subscription stays cancelled. A later
activefrom 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.createdwithstatus: trialingrecords a trial too, so you need not sendtrial.startedas 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_emailin 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.
Related
- Send events from your server — the events endpoint, amounts and idempotency keys.
- The Server API — partners, programs and links, with the same key.
- Set up a referral program — setting up the program the panel draws from.
- Sync partner codes to Stripe or your store — making codes work at a Stripe, Shopify or BigCommerce checkout.
- Why was my API call refused? — every refusal, and the fix.