Developers

API reference

A JSON REST API over HTTPS at https://kickrail.com/api/v1. Reporting and looking up signups works on every plan; everything else needs Growth or Scale.

Authentication#

Send your program's server key as a bearer token. Find it in Settings → Server API key. Keys start with sk_kr_, belong to one program, and must stay on your server.

curl

curl https://kickrail.com/api/v1/affiliates \
  -H "Authorization: Bearer sk_kr_..."

There is one key per program. Rolling it in Settings replaces it at once, and the old key stops working immediately. Running several programs? Each has its own key, and a key only sees its own program.

Requests and responses#

  • Send bodies as JSON with Content-Type: application/json.
  • Times are ISO 8601 in UTC. Money is an integer in the currency's smallest unit (1500 USD is $15.00) with a lowercase currency.
  • Ids carry a prefix: aff_, ref_, com_, po_, cmp_.

Pagination

List endpoints return the newest first:

{ "object": "list", "data": [ ... ], "has_more": true }

To get the next page, pass the last id you received as starting_after, and keep going while has_more is true.

Node

async function* all(path) {
  let after;
  while (true) {
    const url = new URL("https://kickrail.com/api/v1" + path);
    url.searchParams.set("limit", "100");
    if (after) url.searchParams.set("starting_after", after);
    const res = await fetch(url, { headers: { Authorization: "Bearer " + process.env.KICKRAIL_SECRET_KEY } });
    const page = await res.json();
    yield* page.data;
    if (!page.has_more) return;
    after = page.data.at(-1).id;
  }
}

for await (const c of all("/commissions?status=pending")) console.log(c.id, c.amount_cents);

Errors

Errors use standard HTTP status codes with a JSON body: {"detail": "no such affiliate"}. For 422 from validation, detail is a list of the fields that failed.

StatusMeaning
401bad keyMissing, malformed, or rolled server key.
402planThis endpoint needs a higher plan. The message names it.
404not foundNo object with that id in this program.
409conflictA token or code is already taken.
422invalidA field failed validation.

Referrals#

POST/v1/referralsAll plans

Tell Kickrail who signed up and what referred them. Full guide: Report signups from your server.

FieldTypeDescription
referralstringThe kr_ref cookie or kickrail_referral form value.
viastringAn affiliate link token.
codestringA creator code typed at signup.
emailstringThe user's email.
user_idstringYour id for the user, up to 200 characters.
stripe_customer_idstringcus_...
ipstringSignup IP, for the self-referral check. Stored hashed.

At least one of email, user_id, stripe_customer_id is required. Returns {"referred": false} or {"referred": true, "created": bool, "referral": {...}}.

GET/v1/referrals/lookupAll plans

Query with one of email, user_id or stripe_customer_id. Returns {"referred": bool, "referral": {...} | null}.

GET/v1/referralsGrowth+
QueryTypeDescription
limit1 to 100How many to return. Default 25.
starting_afteridReturn the page after this object. Pass the last id of the previous page.
affiliate_ididOnly this affiliate's referrals.
statuslead | customerLead: signed up. Customer: has paid.
GET/v1/referrals/{id}Growth+

The referral object

FieldTypeDescription
idstringref_...
affiliate_idstringWho referred them.
campaign_idstringThe campaign it earns under.
sourcelink | codeWhether a link or a creator code made the referral.
statuslead | customerBecomes customer on the first payment.
email, user_id, stripe_customer_idstring | nullWhat Kickrail knows about the person.
flaggedbooleanA self-referral check matched and hasn't been cleared.
rejectedbooleanYou rejected the referral; it earns nothing.
first_paid_attime | nullTheir first payment.
created_attime

Affiliates#

GET/v1/affiliatesGrowth+
QueryTypeDescription
limit1 to 100How many to return. Default 25.
starting_afteridReturn the page after this object. Pass the last id of the previous page.
statuspending | approved | rejected
emailstringFind one affiliate by email.
campaign_idid
GET/v1/affiliates/{id}Growth+
POST/v1/affiliatesGrowth+

Add an affiliate from your own app, for example when a customer opts into your partner program. People without a Kickrail login get one, and the welcome email carries a link to set their password. If they are already an affiliate of this program, that affiliate is returned with created: false.

FieldTypeDescription
emailstring, required
namestring, required1 to 100 characters.
campaign_ididDefaults to your default campaign.
statuspending | approvedDefault approved.
tokenstringTheir link token (?via= value): 3 to 32 of a-z, 0-9, -, _. Generated from the name if left out.
codestringA creator code: 3 to 20 letters and digits. Needs Starter or higher.
rate_overrideruleAn object: {"type": "percent", "percent": 40}, {"type": "flat", "flat_cents": 1500, "currency": "usd"} or {"type": "none"}. Replaces the campaign rate for this affiliate.
metadataobjectUp to 20 string keys and values, for your own ids.
send_welcome_emailbooleanDefault true.

curl

curl https://kickrail.com/api/v1/affiliates \
  -H "Authorization: Bearer $KICKRAIL_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","name":"Jane Doe","code":"JANE20","metadata":{"crm_id":"42"}}'
PATCH/v1/affiliates/{id}Growth+

Send only the fields you want to change. The response is the updated affiliate, plus a warning string when Stripe refused to create a promotion code for a new code.

FieldTypeDescription
statuspending | approved | rejectedApproving sends the approval email.
campaign_ididMove them to another campaign.
tokenstringChange their main link token.
codestringChange their main creator code.
rate_overrideruleAn object: {"type": "percent", "percent": 40}, {"type": "flat", "flat_cents": 1500, "currency": "usd"} or {"type": "none"}. Replaces the campaign rate for this affiliate.
clear_rate_overridebooleanSend true to go back to the campaign rate.
metadataobjectReplaces the stored metadata.

The affiliate object

JSON

{
  "id": "aff_2Lp...",
  "object": "affiliate",
  "status": "approved",
  "email": "[email protected]",
  "name": "Jane Doe",
  "campaign_id": "cmp_...",
  "token": "jane",
  "link": "https://yourapp.com/?via=jane",
  "code": "JANE20",
  "code_redeemable_at_checkout": true,
  "links": [
    { "id": "lnk_...", "token": "jane-yt", "url": "https://yourapp.com/pricing?via=jane-yt",
      "landing_url": "https://yourapp.com/pricing", "label": "YouTube", "created_at": "..." }
  ],
  "codes": [{ "id": "cod_...", "code": "JANEPOD", "redeemable_at_checkout": true, "created_at": "..." }],
  "rate_override": null,
  "metadata": { "crm_id": "42" },
  "created_at": "2026-10-17T15:04:05Z"
}

rate_override comes back in stored form: {"type": "percent", "bps": 4000} (basis points, so 4000 is 40%) or {"type": "flat", "cents": 1500, "currency": "usd"}.

Commissions#

GET/v1/commissionsGrowth+
QueryTypeDescription
limit1 to 100How many to return. Default 25.
starting_afteridReturn the page after this object. Pass the last id of the previous page.
affiliate_idid
referral_idid
statuspending | paid | void
GET/v1/commissions/{id}Growth+

The commission object

FieldTypeDescription
idstringcom_...
kindlead | sale | clawbackA lead bounty, a commission on a payment, or a negative amount after a refund of a paid commission.
affiliate_id, referral_id, campaign_idstring
amount_centsintegerWhat the affiliate earns. Negative for a clawback. Shrinks after a partial refund.
currencystring
sale_amount_centsintegerThe pre-tax payment it was worked out from.
statuspending | paid | void
held_for_reviewbooleanWaiting on a self-referral review.
sourcestringWhere it came from: inv:in_... (an invoice), cs:cs_... (a one-off checkout), lead:ref_... (a lead bounty) or clawback:....
due_attimeWhen the hold ends and it becomes payable.
paid_at, payout_idtime, string | nullSet once it is paid.
void_reasonstring | nullWhy it was voided, for example refunded.
created_attime

Payouts#

GET/v1/payoutsGrowth+
QueryTypeDescription
limit1 to 100How many to return. Default 25.
starting_afteridReturn the page after this object. Pass the last id of the previous page.
GET/v1/payouts/{id}Growth+
POST/v1/payoutsGrowth+

Record that you paid affiliates yourself, in cash or in kind (a discount, account credit). It works like a manual payout in the dashboard: their payable commissions become paid, they get the payout email if it's on, and payout.paid goes out. Commissions still in their hold aren't included. Returns the payout object, or 409 if nothing is payable.

FieldTypeDescription
affiliate_idsarray, required1 to 100 affiliate ids, for example the one from a payout.due event.
currencystringNeeded only when those affiliates have payable balances in more than one currency.
referencestringUp to 200 characters, such as a transfer or coupon id. Shown to you and the affiliate.

The payout object

FieldTypeDescription
idstringpo_...
methodmanual | paypal | stripe
statusstringawaiting_funding until a Stripe batch is paid for, processing while PayPal or Stripe settles, then paid, partial or failed. A manual payout you undo becomes undone.
currency, total_cents, fee_cents
referencestringThe reference you typed, or the provider's batch id.
itemsarrayOne per affiliate: affiliate_id, amount_cents, commission_ids, status, error.
paid_at, created_attime

Campaigns#

GET/v1/campaignsGrowth+

All campaigns, oldest first, in one list (no pagination). Each has id, name, rule, duration, hold_days, min_payout_cents, lead_bounty, product_rules, stripe_coupon_id, is_default, public, slug, join_url (null for a private campaign), landing_url, and a readable description such as "30% of every payment for 12 months". Create and edit campaigns in the dashboard.

Portal domains#

Serve the join page and affiliate portal from your own subdomain, as in Settings → Portal domains. Adding domains also needs room on your plan (Starter 1, Growth 3, Scale 10).

GET/v1/domainsGrowth+

Every domain on the program, in one list.

POST/v1/domainsGrowth+

Body: {"host": "partners.yourapp.com"}. Returns the domain with the dns_record to create, and checks it straight away. The first domain becomes primary.

POST/v1/domains/{host}/verifyGrowth+

Checks DNS and HTTPS again, like the Check DNS button. Call it after adding the CNAME; the first check after DNS is verified also requests the certificate.

POST/v1/domains/{host}/primaryGrowth+

Makes it the domain used in affiliate links and emails. Returns the full list.

DELETE/v1/domains/{host}Growth+

Removes it. If it was primary, the next verified domain takes over.

The domain object

FieldTypeDescription
hoststringpartners.yourapp.com
statusstringnot_verified until the CNAME is found, waiting_for_https while the certificate is issued, then live.
primarybooleanUsed in affiliate links and emails.
detailstringWhy it isn't live yet, when it isn't.
dns_recordobject{"type": "CNAME", "name": host, "value": "portals.kickrail.com"}
join_urlstringThe join page on this domain.
added_at, verified_at, checked_attime

Webhooks

To react to changes instead of polling, subscribe to webhooks. Their data uses the same objects as this API.