Building an integration with an LLM or coding agent? Point it at the raw markdown source: /api.md.

API

A RESTful API for managing your affiliate program: associates, referral links, clicks, and conversions.

Organisations and Programs

An Organisation is your login — the email you use to sign in to the dashboard, via a passwordless emailed sign-in link.

A Program is a single affiliate program's configuration: its destination URL, commission rules, currency, cookie/payout windows, and its own API key.

One Organisation can run multiple Programs (e.g. one per product or site), and each Program's API key only ever sees that Program's own associates, links, clicks, and conversions — never another Program's, even under the same Organisation.

Signing up creates one Organisation and its first Program together.

Add more Programs from the dashboard at any time, each with its own independent API credentials.

Authentication

Endpoints under /api/* are authenticated with HTTP Basic Auth, using a program's API key as the username and API secret as the password:

Authorization: Basic base64(API_KEY:API_SECRET)

With curl, pass -u API_KEY:API_SECRET and it will encode this for you. A program's API key and secret are shown once when it's created (at signup, or when adding a new program from the dashboard) — or regenerated from that program's Credentials page.

Requests without valid credentials return 401:

{ "error": "Invalid or missing credentials" }

Sandbox environment

https://sandbox.affiliately.io is a separate deployment of the exact same application — identical behavior to production, but backed by its own database. Point an integration at it while you're building or testing, without touching real associates, links, clicks, or conversions.

  • Every endpoint in this document works the same way on both hosts — swap affiliately.io for sandbox.affiliately.io in the URL and nothing else changes.
  • Sandbox credentials are separate from production. A production API key/secret will not work against the sandbox host, and vice versa.
  • Sandbox is the right place to exercise flows that create real tracking data as a side effect — e.g. repeatedly hitting GET /r/:code — since none of it is real.

Manage a program

Requires auth, using the program's own API key. Lets a program manage its own configuration — not the organisation's login, which has no API-key-driven equivalent and is managed from the dashboard only.

An API key/secret only ever identifies one program, so you don't need to already know your program's idGET /api/programs/me looks it up for you:

curl -u API_KEY:API_SECRET https://affiliately.io/api/programs/me

Once you have the id from that (or from the dashboard), the same record is also reachable at /api/programs/:id — useful if you've cached it and don't want the extra lookup — and that's the form PATCH/DELETE use:

curl -u API_KEY:API_SECRET https://affiliately.io/api/programs/6

curl -u API_KEY:API_SECRET \
  -X PATCH https://affiliately.io/api/programs/6 \
  -H "Content-Type: application/json" \
  -d '{ "commissionRules": { "billing": "one-off", "calculation": "flat", "amount": 120 } }'

curl -u API_KEY:API_SECRET -X DELETE https://affiliately.io/api/programs/6

PATCH accepts any of name, destinationUrl, commissionRules, currency, cookieDurationDays, payoutHoldDays, attributionModel. DELETE is a soft delete, like everything else in this API, and only ever affects the program identified in the URL — never the organisation or its other programs.

attributionModel is first-touch or last-touch (default). It decides which associate gets credit when a customer clicks more than one referral link before completing an order:

  • last-touch — the most recent click wins.
  • first-touch — the original click wins, even if the customer clicked other links afterwards.

This is a declared intent, not something Affiliately enforces. Since /r/:code doesn't set a cookie (see Test a referral link), Affiliately never sees which afly value the customer's browser is actually carrying — only whichever one your backend sends to POST /api/conversions. Implementing attributionModel means writing your own cookie-setting logic to match: for last-touch, always overwrite your cookie with the newest afly; for first-touch, only set it if one isn't already there. This field exists so that's a documented, discoverable setting instead of tribal knowledge scattered across your codebase.

Commission rules

commissionRules is a single JSON object describing how much an associate earns, and when. Every conversion (and every renewal — see Subscriptions) reads this to work out its payout, so it's worth understanding the shape up front. It's a discriminated object — which fields are required or forbidden depends on billing and calculation:

Field Type Required Notes
billing string yes one-off or subscription
recurrence string only if billing is subscription once (commission on the first payment only) or every-cycle (commission on renewals too). Forbidden when billing is one-off
cycleLimit integer no, and only when recurrence is every-cycle Caps commission to the first N renewals; omit or send null for uncapped. Forbidden otherwise
calculation string yes flat or percentage
amount number only if calculation is flat The flat commission amount, in the program's currency. Forbidden when calculation is percentage
pct number only if calculation is percentage 0–100. Applied to orderValue (see Conversions). Forbidden when calculation is flat

Three example shapes:

Flat one-off — a fixed fee per sale:

{ "billing": "one-off", "calculation": "flat", "amount": 25 }

Percentage one-off — 10% of the order value, one-time:

{ "billing": "one-off", "calculation": "percentage", "pct": 10 }

Subscription — paid every renewal, capped at 12 cycles:

{ "billing": "subscription", "recurrence": "every-cycle", "cycleLimit": 12, "calculation": "flat", "amount": 15 }

Sending a field that doesn't apply to your combination (e.g. pct alongside calculation: "flat", or recurrence alongside billing: "one-off") is rejected with 400.

Create an associate

Registers a new associate (affiliate) under your program.

curl -u API_KEY:API_SECRET \
  -X POST https://affiliately.io/api/associates \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Jane Doe",
    "email": "jane@example.com",
    "metadata": { "externalId": "usr_9f2a", "tier": "gold" }
  }'
Field Type Required Notes
name string yes
email string yes
status string no pending (default) or approved — see below
metadata object no Arbitrary JSON for your own use — stored and returned as-is, never read or acted on by the API

Security note: metadata is stored and returned in plaintext — there's no encryption at rest and no masking in responses. If you're storing sensitive values here (bank details, government IDs, etc.), encrypt them yourself before sending and decrypt them on your side after fetching. Affiliately never reads, validates, or acts on metadata contents, so anything you don't protect yourself is kept exactly as sent.

An associate's status is one of:

  • pending — awaiting your review. The default if omitted, so associates are held for approval unless you opt in to approved.
  • approved — pre-approved and live immediately.
  • rejected — reviewed and declined; never went live.
  • suspended — was live, now pulled from the program (e.g. for policy violations, or on their own request).

rejected and suspended cannot be set on creation — only reachable via update. Every status is reversible via update (e.g. rejectedapproved on reconsideration, suspendedapproved to reinstate).

Response — 201 Created:

{
  "id": 16,
  "programId": 23,
  "name": "Jane Doe",
  "email": "jane@example.com",
  "status": "pending",
  "metadata": { "externalId": "usr_9f2a", "tier": "gold" },
  "createdAt": "2026-07-07T20:07:16.754Z",
  "updatedAt": "2026-07-07T20:07:16.754Z"
}

List / get associates — requires auth, list filters by email:

curl -u API_KEY:API_SECRET "https://affiliately.io/api/associates?email=jane@example.com"

Useful for looking up a specific associate by email without fetching every associate and filtering client-side.

Update an associate

Requires auth. Approve or reject a pending associate, suspend one, or change any other field:

curl -u API_KEY:API_SECRET \
  -X PATCH https://affiliately.io/api/associates/16 \
  -H "Content-Type: application/json" \
  -d '{ "status": "approved" }'

Accepts any of name, email, status (pending, approved, rejected, or suspended), metadata.

Create a link

Generates a trackable referral link for one of your associates. code is what shows up in the actual URL a customer clicks (https://affiliately.io/r/CODE) — omit it to get a random one, or supply your own vanity code.

curl -u API_KEY:API_SECRET \
  -X POST https://affiliately.io/api/links \
  -H "Content-Type: application/json" \
  -d '{
    "associateId": 17,
    "code": "SUMMER10",
    "discountPct": 20
  }'
Field Type Required Notes
associateId integer yes Must belong to your program
code string no Alphanumeric, dashes, and underscores, 3–32 chars. Auto-generated if omitted. Must be unique
discountPct integer no 0–100, default 0. Share of the flat commission passed on as a customer discount instead of associate payout
destinationUrl string no Overrides the program's default destination URL for this link

Response — 201 Created:

{
  "id": 16,
  "associateId": 17,
  "programId": 24,
  "code": "SUMMER10",
  "discountPct": 20,
  "createdAt": "2026-07-07T20:10:41.800Z",
  "updatedAt": "2026-07-07T20:10:41.800Z"
}

If associateId doesn't belong to your program (or doesn't exist), you get 400:

{ "error": "associateId does not reference an existing associate" }

Test a referral link

GET /r/:code is the public, unauthenticated endpoint a customer's browser actually hits when they click a referral link. It logs a click and redirects to the destination URL, appending the click's token as an afly query param. You can inspect this with curl.

Affiliately doesn't set a cookie of its own — the afly query param on the redirect is the only signal it hands you. It's on you to persist that value as the customer moves from the landing page to checkout (e.g. your own first-party cookie, session, or localStorage) so it's still available when you call checkout or report a conversion.

By default curl does not follow redirects, so -i (include headers) is enough to see the raw 302 response — the Location it would have sent a browser to:

curl -i https://affiliately.io/r/SUMMER10
HTTP/1.1 302 Found
Location: https://your-destination.example.com/?afly=7403952b-4b3e-40e5-aac0-11084b5cfcf1
...

To capture just the redirect target (e.g. for scripting a test), use -w:

curl -o /dev/null -w "status: %{http_code}\nredirected to: %{redirect_url}\n" \
  https://affiliately.io/r/SUMMER10

Two things worth knowing:

  • -L follows the redirect instead of just showing it — useful to confirm the destination page actually loads, but combine it with -i if you still want to see the 302 and its headers along the way.
  • This isn't idempotent. Every request logs a new Click and mints a fresh afly, exactly like a real visitor clicking the link — so repeated test runs create real tracking data, not just a preview.

Apply the discount at checkout

Once afly is on the cart/checkout page (carried over from the redirect's ?afly= query param — it's on you to persist it that far, see above), get a quote for the discount to apply before the order is placed. This is a read, not a report — it doesn't touch Click/Conversion data, so you can call it as many times as the cart re-renders.

curl -u API_KEY:API_SECRET "https://affiliately.io/api/checkout?aflyToken=7403952b-4b3e-40e5-aac0-11084b5cfcf1"
Query param Type Required Notes
aflyToken string yes The afly value from the referral redirect
orderValue number only if your program's calculation is percentage The order's total value, before any discount — the base the percentage is applied to
curl -u API_KEY:API_SECRET "https://affiliately.io/api/checkout?aflyToken=7403952b-4b3e-40e5-aac0-11084b5cfcf1&orderValue=200"

Response — 200 OK. The commission is first resolved from your program's commissionRules — either the flat amount, or pct% of orderValue — then split by the link's discountPct. This is the same two-step calculation POST /api/conversions repeats once the order is placed (see Conversions). You apply customerDiscount to the cart; associatePayout is informational, for display if you want it; currency is your program's currency (e.g. GBP/USD/EUR) — commissionAmount/customerDiscount/associatePayout mean nothing without it:

{
  "linkId": 16,
  "associateId": 17,
  "code": "SUMMER10",
  "discountPct": 20,
  "commissionAmount": 100,
  "currency": "GBP",
  "customerDiscount": 20,
  "associatePayout": 80
}

404 if aflyToken doesn't match a click for your program, or if the click's attribution window (cookieDurationDays) has already expired — treat either case as "no discount applies":

{ "error": "No click found for that aflyToken" }

400 if your program's calculation is percentage and orderValue was omitted:

{ "error": "orderValue is required to quote a percentage-based commission" }

Clicks

GET /r/:code (above) is how a real visitor's browser generates a click. The /api/clicks endpoints are for managing that data directly — POST is a raw, unauthenticated way to log a click without an actual browser redirect (e.g. from a mobile app or server-side integration); GET/DELETE are authenticated and scoped to your own program's links.

Create a click — public, no auth required:

curl -X POST https://affiliately.io/api/clicks \
  -H "Content-Type: application/json" \
  -d '{ "linkId": 17 }'
Field Type Required Notes
linkId integer yes
aflyToken string no UUID. Auto-generated if omitted
ipAddress string no Defaults to the requester's IP
userAgent string no Defaults to the request's User-Agent header

Response — 201 Created:

{
  "id": 42,
  "linkId": 17,
  "aflyToken": "600aaeb9-6115-4fbc-93d2-3ec216ec4a53",
  "ipAddress": "203.0.113.10",
  "userAgent": "curl/8.7.1",
  "createdAt": "2026-07-07T20:19:50.935Z"
}

List clicks — requires auth, filter by linkId and/or aflyToken:

curl -u API_KEY:API_SECRET "https://affiliately.io/api/clicks?linkId=17"
[
  {
    "id": 42,
    "linkId": 17,
    "aflyToken": "600aaeb9-6115-4fbc-93d2-3ec216ec4a53",
    "ipAddress": "203.0.113.10",
    "userAgent": "curl/8.7.1",
    "createdAt": "2026-07-07T20:19:50.000Z",
    "deletedAt": null
  }
]

Get one click — requires auth:

curl -u API_KEY:API_SECRET https://affiliately.io/api/clicks/42

Delete a click — requires auth. Clicks use soft deletes, so this hides the row from the API rather than erasing it:

curl -u API_KEY:API_SECRET -X DELETE https://affiliately.io/api/clicks/42

Response — 204 No Content.

Conversions

Reported by your own backend when an order completes, using the afly value your frontend picked up from the redirect and persisted through to checkout (see Test a referral link). The API looks up the matching click, attributes it to the right associate/link, and computes the payout split — you never send amounts yourself.

Report a conversion — requires auth:

curl -u API_KEY:API_SECRET \
  -X POST https://affiliately.io/api/conversions \
  -H "Content-Type: application/json" \
  -d '{
    "aflyToken": "9d2a19ed-54d7-486d-ad19-7561eaff4e02",
    "orderReference": "ORDER-1042"
  }'
Field Type Required Notes
aflyToken string yes The afly value from the referral redirect
orderReference string yes Your own order/invoice id. Must be unique per program
orderValue number only if your program's calculation is percentage The order's total value, before any discount — the base the percentage is applied to

Response — 201 Created. commissionAmount is resolved from your program's commissionRules — either the flat amount, or pct% of orderValue — then customerDiscount and associatePayout are split from that according to the link's discountPct:

{
  "id": 38,
  "programId": 26,
  "linkId": 18,
  "aflyToken": "9d2a19ed-54d7-486d-ad19-7561eaff4e02",
  "orderReference": "ORDER-1042",
  "commissionAmount": 100,
  "currency": "GBP",
  "associatePayout": 80,
  "customerDiscount": 20,
  "status": "pending",
  "createdAt": "2026-07-07T20:24:10.745Z",
  "updatedAt": "2026-07-07T20:24:10.745Z"
}

currency is captured from your program at the moment the conversion is reported and stays fixed from then on — same as commissionAmount — so a later change to your program's currency never relabels a historical payout.

If your program's commissionRules.billing is subscription, this call is also the signup moment: it creates a Subscription behind the scenes and stamps the conversion with subscriptionId and cycleNumber: 1. See Subscriptions for how renewal payments are reported from here on.

Error responses:

Status Cause
400 No click found for that aflyToken
400 Your program's calculation is percentage and orderValue was omitted
409 orderReference has already been reported for this program
410 The click's attribution window has expired (past the program's cookieDurationDays)

List / get conversions — requires auth, list filters by linkId, status, and/or orderReference:

curl -u API_KEY:API_SECRET "https://affiliately.io/api/conversions?status=pending"
curl -u API_KEY:API_SECRET "https://affiliately.io/api/conversions?orderReference=ORDER-1042"
curl -u API_KEY:API_SECRET https://affiliately.io/api/conversions/38

orderReference is how you go from your own order id to the conversion's internal id — useful before a PATCH (e.g. canceling one during the hold period), since that endpoint only accepts the id.

Find conversions ready to be paidreadyForPayment=true returns pending/approved conversions whose payoutHoldDays window has already elapsed (i.e. exactly the set you could successfully PATCH to paid right now, without doing that date math yourself):

curl -u API_KEY:API_SECRET "https://affiliately.io/api/conversions?readyForPayment=true"

This ignores status if both are passed together — readyForPayment already implies pending or approved.

Update status — requires auth. This is how you move a conversion through pendingapprovedpaid, or mark it rejected:

curl -u API_KEY:API_SECRET \
  -X PATCH https://affiliately.io/api/conversions/38 \
  -H "Content-Type: application/json" \
  -d '{ "status": "approved" }'

approved is purely informational — nothing in the API requires it. There's no enforced state machine beyond the two rules below, so a conversion can go straight from pending to paid once the hold period ends without ever passing through approved. In practice, you'd set approved whenever your own backend considers the sale verified (e.g. payment/fraud checks clear) — independent of the cancellation window — to distinguish "reviewed and legitimate, just waiting out the hold period" from "still unreviewed."

Two rules are enforced server-side, not left to the caller:

  • Payout hold period. You can't mark a conversion paid until payoutHoldDays have passed since it was created — this is your program's cancellation/returns window:

    { "error": "This conversion cannot be marked paid until its 14-day hold period ends (eligible at 2026-07-21T20:24:10.000Z)" }
    
  • Once paid, it's final. Any further PATCH or DELETE on a paid conversion returns 409:

    { "error": "This conversion has already been paid and cannot be modified" }
    

Delete a conversion — requires auth, soft delete, blocked once paid (see above):

curl -u API_KEY:API_SECRET -X DELETE https://affiliately.io/api/conversions/38

Cancel a conversion during the hold period

If a customer cancels before a conversion has been paid, look it up by your own order id, then mark it rejected:

curl -u API_KEY:API_SECRET "https://affiliately.io/api/conversions?orderReference=ORDER-1042"
curl -u API_KEY:API_SECRET \
  -X PATCH https://affiliately.io/api/conversions/38 \
  -H "Content-Type: application/json" \
  -d '{ "status": "rejected" }'

This works at any point before the conversion is paid — unlike paid, rejected isn't gated by payoutHoldDays, so it doesn't matter whether the cancellation happens on day 1 or day 13 of the window.

Subscriptions

Only relevant if your program's commissionRules.billing is subscription. A Subscription tracks one associate's recurring customer across billing cycles, so renewal payments can be credited without a fresh referral click — which won't exist by the time a renewal invoice comes due; the attribution window has long since expired.

The first payment still goes through POST /api/conversions, exactly as documented in Conversions — it's the signup moment, with a real aflyToken from the original click. For a subscription-billed program, that call also creates the Subscription for you and stamps the conversion with subscriptionId and cycleNumber: 1:

{
  "id": 40,
  "programId": 26,
  "linkId": 18,
  "subscriptionId": 5,
  "cycleNumber": 1,
  "aflyToken": "9d2a19ed-54d7-486d-ad19-7561eaff4e02",
  "orderReference": "ORDER-1042",
  "commissionAmount": 15,
  "currency": "GBP",
  "associatePayout": 15,
  "customerDiscount": 0,
  "status": "pending",
  "createdAt": "2026-07-07T20:24:10.745Z",
  "updatedAt": "2026-07-07T20:24:10.745Z"
}

Report a renewal — requires auth. There's no aflyToken for a renewal — you identify the subscription by its own id instead:

curl -u API_KEY:API_SECRET \
  -X POST https://affiliately.io/api/subscriptions/5/renewals \
  -H "Content-Type: application/json" \
  -d '{ "orderReference": "ORDER-1042-CYCLE-2", "orderValue": 50 }'
Field Type Required Notes
orderReference string yes Your own order/invoice id for this cycle. Must be unique per program
orderValue number only if your program's calculation is percentage Same rule as POST /api/conversions

Response — 201 Created, same shape as a conversion, with cycleNumber incremented from whatever this subscription is currently at. Whether this particular cycle actually earns commission depends on commissionRules:

  • recurrence: "once" — only cycle 1 (the original conversion) ever pays. Every renewal after that is still recorded here, for your own order history, but with commissionAmount, customerDiscount, and associatePayout all 0.
  • recurrence: "every-cycle", no cycleLimit — every renewal pays, indefinitely.
  • recurrence: "every-cycle", with a cycleLimit — renewals pay until the cycle count passes the limit, then 0 from then on (same zeroed-out shape as above).

A zeroed-out renewal is still a normal 201 — it's not an error, it's simply a cycle your commissionRules say shouldn't pay.

Error responses:

Status Cause
400 Your program's calculation is percentage and orderValue was omitted
404 No subscription with that id for your program
409 That orderReference has already been reported for this program
409 The subscription has been cancelled

Cancel a subscription — requires auth. Stops it from accepting further renewals:

curl -u API_KEY:API_SECRET -X POST https://affiliately.io/api/subscriptions/5/cancel

Response — 200 OK, the updated Subscription. Any renewal reported against a cancelled subscription from then on returns 409.

List / get subscriptions — requires auth, list filters by linkId and/or status:

curl -u API_KEY:API_SECRET "https://affiliately.io/api/subscriptions?status=active"
curl -u API_KEY:API_SECRET https://affiliately.io/api/subscriptions/5
{
  "id": 5,
  "programId": 26,
  "linkId": 18,
  "aflyToken": "9d2a19ed-54d7-486d-ad19-7561eaff4e02",
  "status": "active",
  "createdAt": "2026-07-07T20:24:10.000Z",
  "updatedAt": "2026-07-07T20:24:10.000Z"
}