REST API

The affiliate API under /api/v1 – keys, limits, every endpoint with parameters and responses, pagination and error codes.

View as Markdown

The REST API gives your own scripts and integrations access to the same data as the affiliate dashboard: programs, rules, products, links, sites, clicks, sales, balance and payouts. Every endpoint maps to a tool on the MCP server – the same functions sit behind both.

The API is for affiliates only. Stores have no API.

Get started

  1. Open AI og API in the dashboard menu (under Tjen penge – the address is app.affilyflow.com/indstillinger/api).
  2. Create an API key and give it a name, e.g. "My site".
  3. Copy the key right away. It is shown only once, and we store only a fingerprint of it.

Send the key in the Authorization header on every request:

List your programsShell
curl https://app.affilyflow.com/api/v1/programs \
  -H "Authorization: Bearer $AFFILYFLOW_API_KEY"

The header X-API-Key: afk_… also works. A key always starts with afk_.

GET https://app.affilyflow.com/api/v1 needs no key and returns a machine-readable overview of every endpoint, parameter and limit.

OAuth connections (claude.ai, ChatGPT, Claude Code) only work on the MCP server – not on the REST API.

Conventions

TopicRule
Base URLhttps://app.affilyflow.com/api/v1
FormatJSON in and out. Send Content-Type: application/json with a body.
AmountsDanish kroner (DKK) excluding VAT. Sales in other currencies are converted at a locked ECB rate; the original is in original_currency and original_value.
Datesfrom and to as YYYY-MM-DD in Danish time (Europe/Copenhagen), both inclusive. Without them: the last 30 days. At most 366 days per request.
IDsPrograms, links, sites and sales have UUIDs. click_id is an integer.
Paginationlimit + cursor. The response has total, has_more and next_cursor. Pass next_cursor back as cursor with the same filters.
Errors{ "error": { "code": "…", "message": "…" } } with an HTTP status. Codes are stable identifiers; messages are in Danish.
LanguageData labels such as status_text and notes are in Danish.
CacheResponses are sent with Cache-Control: no-store.

Rejected sales never count as sales or commission in the numbers.

Limits

LimitValueError
Requests per minute per key120HTTP 429 rate_limited with Retry-After: 60
Write requests per minute per key60 (a batch request counts as one)HTTP 429 rate_limited
New links per 24 hours per account500 (every new link on the account in the last 24 hours counts)HTTP 429 daily_link_limit
Links per batch50HTTP 400 invalid_input

A reused identical link does not count as a new link. Append ?sid=<sub-id> (another sub-ID) or ?p=<path> (another page at the store) to an existing link instead of creating many near-identical links.

Endpoints

MethodPathUseMCP tool
GET/accountYour account, quota and dashboard linksget_account
GET/catalogSearch all open programssearch_programs
GET/programsPrograms you have joinedlist_programs
GET/programs/{id}One program with rules and guidelinesget_program
GET, POST/programs/{id}/complianceCheck a promotion against the rulescheck_compliance
GET/productsSearch the stores' productssearch_products
GET/websitesYour registered siteslist_websites
POST/websitesAdd a siteadd_site
PATCH/websites/{id}Edit a siteupdate_site
GET/linksYour linkslist_links
POST/linksCreate a linkcreate_link
POST/links/batchUp to 50 links in one requestcreate_links_batch
GET/links/{id}One link with numbersget_link
PATCH/links/{id}Change name, landing page or site labelupdate_link
POST/links/{id}/archiveArchive a linkarchive_link
DELETE/links/{id}/archiveRestore an archived linkrestore_link
GET/statsClicks, sales and commission for a periodget_stats
GET/salesIndividual saleslist_sales
GET/clicksIndividual clicks with page and sourcelist_clicks
GET/sitesMeasured sitesget_sites
GET/pagesPages on your sitesget_pages
GET/balanceBalance and next payoutget_balance
GET/payoutsYour payoutslist_payouts

Account

GET /account

Returns account (AF number, display name and how stores see you, country, niche, number of sites, links and joined programs, currency and timezone), access, quota (links in the last 24 hours, remaining, limits and requests used in the current minute) and dashboard with the addresses of the dashboard pages.

Programs

GET /catalog

The full catalog of open programs – including programs you have not joined.

ParameterTypeDescription
querytextFree text matched against store name, category, description and audience.
categorytextCategory (Danish label), e.g. Bolig. The response lists them in categories.
joinedtrue / falseOnly joined or only not joined. Omit for both.
min_commission_pctnumber 0–100Minimum percentage commission. Fixed-amount programs are excluded.
recurring_onlytrue / falseOnly programs that pay commission on subscription renewals.
channeltextOnly programs that allow the channel, e.g. TikTok.
sortepc, commission, newest, nameDefault: epc (network earnings per click).
limit1–50Default 20.
cursortextFrom next_cursor.

Each program includes program_id, store, joined, commission (type, value, text), cookie_days, approval_days, recurring_months, allowed_channels, paid_search (allowed, no_brand or forbidden), allow_coupon_sites, network (EPC, conversion, order value and approval rate – rejected sales excluded) and join_url. Joining happens in the dashboard via join_url – the API never joins programs.

GET /programs

The programs you have joined, with commission, cookie days, channels, the saved rules and the store guidelines (truncated). active: false means new sales give no commission; inactive_reason says why (program_paused or store_inactive). The field brand_bidding is deprecated – use paid_search. Parameter: search (store name).

GET /programs/{id}

Everything about one program: the store's description and audience, the guidelines verbatim, the rules that apply today (rules.now), the saved rules (rules.saved) and the date a tightening takes effect (rules.tightening_effective_from), a compliance_summary, the rule-watch mode (enforcement.mode: shadow or enforce), discount codes, creatives (banners with ready HTML and your link once joined), top products, network stats and your own numbers for the program (mine).

GET or POST /programs/{id}/compliance

Checks a planned promotion against the program's measurable rules. Send at least one parameter. Use POST with JSON when content is long.

ParameterDescription
channelPlanned channel, e.g. Instagram, SEO, E-mail.
placement_urlFull URL of the page where the link will be placed.
landing_pagePath or URL at the store.
ad_typepaid_search_brand, paid_search_generic, coupon_site, cashback_site, social_post, newsletter or website_content.
contentDraft text (max 20,000 characters) – checked for an advertising label.

The response has verdict (ok, warning or not_allowed), checks with a verdict and a reason per rule (plus effective_from when a tightening is coming), the store guidelines verbatim and the rule-watch mode. Only measurable rules are judged: channel, coupon and cashback sites from the placement's domain, paid search as declared, and the landing page. The advertising-label check is heuristic, and the store's free-text guidelines are not judged.

May I run Google Ads on generic keywords?Shell
curl "https://app.affilyflow.com/api/v1/programs/PROGRAM_ID/compliance?ad_type=paid_search_generic" \
  -H "Authorization: Bearer $AFFILYFLOW_API_KEY"

Products

GET /products

Searches the stores' products across open programs.

ParameterTypeDescription
querytextSearch words (the catalogs are mostly Danish), e.g. sofa. Empty = the products you earn most on.
program_idUUIDOnly one program.
min_commission_pctnumber 0–100Minimum percentage commission.
in_stock_onlytrue / falseHide out-of-stock products.
sortearnings, relevance, price, rateDefault: relevance with a query, otherwise earnings.
limit1–60Default 20.
cursortextFrom next_cursor.

Each product has product_id, title, url, image_url, price, currency, in_stock, store, program_id, commission, est_commission and joined. For joined programs, tracking_url is your existing link sent straight to the product (?p=). est_commission is an estimate: the rate applied to the price excluding VAT, or the fixed amount.

Sites

GET /websites

Your registered sites with website_id, domain, name, niche, monthly_traffic and created_at.

POST /websites

Adds a site with the same rules as Mine sites in the dashboard.

FieldDescription
domainRequired. E.g. mysite.com. A full URL is accepted and reduced to the domain.
nameDisplay name, max 120 characters. Default: the domain.
nicheE.g. Home, max 80 characters.
monthly_trafficApproximate monthly visitors.

A new site returns HTTP 201. If you already have the domain, nothing is created, and the response is the existing site with already_existed: true and HTTP 200. The response includes history with the clicks, sales and commission that already came from the domain.

PATCH /websites/{id}

Changes domain, name, niche or monthly_traffic. Send null or "" to clear name, niche or monthly_traffic. A domain you already use on another site is rejected with duplicate_domain. Sites can only be deleted in the dashboard.

GET /sites

Sites measured from clicks – registered and discovered – with clicks, sales, commission, EPC, page-tracking coverage and method, top stores and top pages. suggest_add: true marks a discovered site with at least 3 clicks or a sale that you have not added.

ParameterDescription
from, toOptional period. Without: all time.
pages_per_site0–25 top pages per site. Default 5.
include_minorInclude discovered sites with very few clicks.

GET /pages

Pages on your sites ranked by commission, with clicks, sales, EPC, conversion, best-selling product, stores and 30-day trend. Clicks without a known page appear as one row per site with known: false.

ParameterDescription
from, toOptional period. Without: all time.
siteOnly pages on this site (domain without www).
known_onlyHide the rows without a known page.
min_clicksMinimum clicks.
limit1–200. Default 50.
cursorFrom next_cursor.

Your links, newest first.

ParameterDescription
program_idOnly links for this program.
sub_idOnly links with this sub-ID.
searchText in name, store, destination or sub-ID.
archivedexclude (default), include or only.
include_statstrue adds clicks, sales, commission, EPC and conversion per link.
limit1–200. Default 50.
cursorFrom next_cursor.

Each link has link_id, url, program_id, store, active, name, landing_page, destination, sub_id, channel, utm, website_id, placement_url, archived, created_at and html.

POST /links

Creates a link – exactly like the link generator in the dashboard. You must have joined the program.

FieldDescription
program_idRequired. From /catalog or /programs.
landing_pagePath or full URL on the store's domain, e.g. /products/sofa. Default: the front page.
sub_idMax 80 characters.
campaignMax 80 characters. Sent as utm_campaign and used as the link name.
channelOne of the program's allowed channels: SEO, Blog, Instagram, TikTok, YouTube, Facebook, Pinterest, E-mail, Website.
utm_content, utm_termMax 100 characters each.
website_idOptional site label from /websites.
placement_urlFull URL of the page where the link will be placed (https://…).
nameThe link's name in the dashboard, max 120 characters.
include_qrtrue adds a QR code as PNG in qr_png_base64.
Create a linkShell
curl -X POST https://app.affilyflow.com/api/v1/links \
  -H "Authorization: Bearer $AFFILYFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"program_id":"PROGRAM_ID","landing_page":"/products/sofa","sub_id":"blog-best-sofas-oct"}'

A new link returns HTTP 201. If an identical link exists, it is reused with reused: true and HTTP 200.

Response (shortened)JSON
{
  "link": {
    "url": "https://app.affilyflow.com/go/3f9a1c07be",
    "short_code": "3f9a1c07be",
    "reused": false,
    "program_id": "PROGRAM_ID",
    "store": "Store",
    "landing_page": "/products/sofa",
    "sub_id": "blog-best-sofas-oct",
    "html": "<a href=\"https://app.affilyflow.com/go/3f9a1c07be\" rel=\"sponsored noopener\" target=\"_blank\" referrerpolicy=\"no-referrer-when-downgrade\">Store</a>",
    "variants": {
      "with_sub_id": "https://app.affilyflow.com/go/3f9a1c07be?sid={sub_id}",
      "deeplink": "https://app.affilyflow.com/go/3f9a1c07be?p={path_on_store}"
    },
    "warnings": []
  },
  "warnings": [],
  "quota": {
    "links_created_last_24h": 1,
    "links_remaining_24h": 499,
    "daily_link_limit": 500,
    "requests_per_minute": 120,
    "requests_used_this_minute": 1,
    "write_calls_per_minute": 60
  }
}

warnings holds rule warnings for the program – e.g. a channel or placement that is not allowed, or a tightening that is coming. Use html on your site: its referrerpolicy makes every click report the exact page it came from (Chrome, Edge and Firefox; Safari and some apps send only the domain).

POST /links/batch

Up to 50 links in one request. The top-level fields (program_id, sub_id, campaign, channel, website_id, utm_content, utm_term) are defaults that each element in items can override. An element can also have landing_page, placement_url and name.

Links to three productsShell
curl -X POST https://app.affilyflow.com/api/v1/links/batch \
  -H "Authorization: Bearer $AFFILYFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"program_id":"PROGRAM_ID","sub_id":"article-sofas-oct","items":[{"landing_page":"/products/sofa-a"},{"landing_page":"/products/sofa-b"},{"landing_page":"/products/sofa-c"}]}'

The response has a result per element (index, ok and either link or error with code and message), a summary (requested, created, reused, failed) and the remaining quota. A failing element does not stop the others.

One link with numbers, performs_on (the pages, sites or channels its clicks came from), the program status and the rules that apply today, and variants. ?include_qr=true adds a QR code.

Changes name, landing_page (on the store's domain) or website_id (null removes the label). The URL never changes.

Sub-ID, campaign, channel, program and UTM values cannot be changed on an existing link, because its clicks are recorded with them. If you try, the API returns HTTP 400 immutable_field. Create a new link, or append ?sid= to the existing one.

POST and DELETE /links/{id}/archive

POST archives the link, DELETE restores it. An archived link keeps redirecting and tracking – it is only hidden from the list.

Results

GET /stats

Clicks, sales and commission for a period.

ParameterDescription
from, toPeriod. Default: the last 30 days.
group_bytotal (default), sub_id, program, link, day, site, page, source, device or country.
program_idOnly this program.
sub_idOnly this sub-ID.
comparetrue adds the previous period of equal length with change in percent.
limit1–500 rows when grouped. Default 100. day always returns every day.

totals and each row in rows have clicks, sales, sales_pending, sales_approved, sales_rejected, revenue, commission, commission_pending, commission_approved, conversion_rate_pct and epc. Rows also have key and label. A sale counts on the day it was registered. With site, page, source, device and country, a sale is grouped by the click it came from.

Last month per program, comparedShell
curl "https://app.affilyflow.com/api/v1/stats?from=2026-09-01&to=2026-09-30&group_by=program&compare=true" \
  -H "Authorization: Bearer $AFFILYFLOW_API_KEY"

GET /sales

Individual sales, newest first.

ParameterDescription
from, toPeriod by sale date. Default: the last 30 days.
statuspending, approved or rejected.
program_idOnly this program.
limit1–200. Default 50.
cursorFrom next_cursor.

Each sale has sale_id, created_at, store, order_ref (only the last characters of the order number), order_value, commission_base (when commission was calculated from another amount than the order), commission (after any partial refunds), commission_before_refund, refunded_value, original currency and rate, status and status_text, rejection_reason (e.g. Regelbrud: … for a rule breach), approval_due_at for pending sales, items, and the click: click_id, clicked_at, link_id, sub_id and landing_page.

GET /clicks

Individual clicks with their full trail: time, the page the link was on and how it is known (page_basis), site, source with basis, link and sub-ID, landing page, device, country, the commission locked on the click and the sales the click led to. The period follows the click date.

ParameterDescription
from, toPeriod. Default: the last 30 days.
siteOnly clicks from this site (domain without www).
pageOnly clicks from pages starting with this URL – or unknown for clicks without a known page.
link_id, program_id, sub_idFilters.
sourceOnly this source, e.g. Google, Instagram, Direkte.
outcomesale, no_sale or rejected (only rejected sales).
limit1–200. Default 50.
cursorFrom next_cursor.

Balance and payouts

Read-only. Responses never contain an IBAN (not even masked), CPR, tax ID or address, and the API cannot request a payout.

GET /balance

balance (available, pending sales, in transit, total earned and paid out – all excluding VAT), payout (the next automatic payout day – the first Danish bank day of the month, minimum 100 DKK – whether you can request a payout now, and what is missing, with code, text and you_can_fix) and the 5 latest payouts.

GET /payouts

All payouts, newest first: status, automatic or requested, amount excluding VAT, VAT, amount transferred, dates and the self-billing invoice number. Parameters: limit (1–100, default 24) and cursor.

Error codes

HTTPCodeMeaning
400invalid_inputA parameter is missing or invalid. The message says which.
400invalid_jsonThe body is not valid JSON.
400invalid_cursorcursor is invalid. Start over without it.
400channel_not_allowedThe channel is not allowed in the program. The message lists the allowed ones.
400landing_page_other_hostThe landing page is not on the store's domain.
400invalid_landing_pageThe landing page is not a valid path or URL.
400unknown_websitewebsite_id does not exist on your account.
400immutable_fieldSub-ID, campaign, channel, program or UTM cannot be changed on an existing link.
400invalid_domainWrite the domain as mysite.com.
401missing_keyNo key was sent.
401invalid_keyThe key is invalid or revoked.
403not_joinedYou have not joined the program. The message has the join link to the dashboard.
403inactiveThe program or the store is not active right now.
403account_suspendedThe account is suspended.
404not_foundThe program, link or site does not exist on your account.
409duplicate_domainYou already have a site with that domain.
429rate_limitedToo many requests. Wait and try again.
429daily_link_limit500 new links in the last 24 hours. Reuse a link with ?sid= or ?p=.
500server_error, save_failedSomething went wrong on our side. Try again.
503unavailableThe API is temporarily unavailable.

Asking for a link or site that belongs to another affiliate returns not_found – the API does not reveal that it exists.

What the API cannot do

  • Join programs – that happens in the dashboard, where the store is notified.
  • Delete links or sites – links can be archived, sites are deleted only in the dashboard.
  • See or change bank details, tax profile or profile – or request a payout.

Missing something?

Write to us – we answer quickly, and the page gets fixed.

Contact support