REST API
The affiliate API under /api/v1 – keys, limits, every endpoint with parameters and responses, pagination and error codes.
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
- Open AI og API in the dashboard menu (under Tjen penge – the address is
app.affilyflow.com/indstillinger/api). - Create an API key and give it a name, e.g. "My site".
- 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:
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
| Topic | Rule |
|---|---|
| Base URL | https://app.affilyflow.com/api/v1 |
| Format | JSON in and out. Send Content-Type: application/json with a body. |
| Amounts | Danish kroner (DKK) excluding VAT. Sales in other currencies are converted at a locked ECB rate; the original is in original_currency and original_value. |
| Dates | from 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. |
| IDs | Programs, links, sites and sales have UUIDs. click_id is an integer. |
| Pagination | limit + 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. |
| Language | Data labels such as status_text and notes are in Danish. |
| Cache | Responses are sent with Cache-Control: no-store. |
Rejected sales never count as sales or commission in the numbers.
Limits
| Limit | Value | Error |
|---|---|---|
| Requests per minute per key | 120 | HTTP 429 rate_limited with Retry-After: 60 |
| Write requests per minute per key | 60 (a batch request counts as one) | HTTP 429 rate_limited |
| New links per 24 hours per account | 500 (every new link on the account in the last 24 hours counts) | HTTP 429 daily_link_limit |
| Links per batch | 50 | HTTP 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
| Method | Path | Use | MCP tool |
|---|---|---|---|
| GET | /account | Your account, quota and dashboard links | get_account |
| GET | /catalog | Search all open programs | search_programs |
| GET | /programs | Programs you have joined | list_programs |
| GET | /programs/{id} | One program with rules and guidelines | get_program |
| GET, POST | /programs/{id}/compliance | Check a promotion against the rules | check_compliance |
| GET | /products | Search the stores' products | search_products |
| GET | /websites | Your registered sites | list_websites |
| POST | /websites | Add a site | add_site |
| PATCH | /websites/{id} | Edit a site | update_site |
| GET | /links | Your links | list_links |
| POST | /links | Create a link | create_link |
| POST | /links/batch | Up to 50 links in one request | create_links_batch |
| GET | /links/{id} | One link with numbers | get_link |
| PATCH | /links/{id} | Change name, landing page or site label | update_link |
| POST | /links/{id}/archive | Archive a link | archive_link |
| DELETE | /links/{id}/archive | Restore an archived link | restore_link |
| GET | /stats | Clicks, sales and commission for a period | get_stats |
| GET | /sales | Individual sales | list_sales |
| GET | /clicks | Individual clicks with page and source | list_clicks |
| GET | /sites | Measured sites | get_sites |
| GET | /pages | Pages on your sites | get_pages |
| GET | /balance | Balance and next payout | get_balance |
| GET | /payouts | Your payouts | list_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.
| Parameter | Type | Description |
|---|---|---|
query | text | Free text matched against store name, category, description and audience. |
category | text | Category (Danish label), e.g. Bolig. The response lists them in categories. |
joined | true / false | Only joined or only not joined. Omit for both. |
min_commission_pct | number 0–100 | Minimum percentage commission. Fixed-amount programs are excluded. |
recurring_only | true / false | Only programs that pay commission on subscription renewals. |
channel | text | Only programs that allow the channel, e.g. TikTok. |
sort | epc, commission, newest, name | Default: epc (network earnings per click). |
limit | 1–50 | Default 20. |
cursor | text | From 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.
| Parameter | Description |
|---|---|
channel | Planned channel, e.g. Instagram, SEO, E-mail. |
placement_url | Full URL of the page where the link will be placed. |
landing_page | Path or URL at the store. |
ad_type | paid_search_brand, paid_search_generic, coupon_site, cashback_site, social_post, newsletter or website_content. |
content | Draft 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.
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.
| Parameter | Type | Description |
|---|---|---|
query | text | Search words (the catalogs are mostly Danish), e.g. sofa. Empty = the products you earn most on. |
program_id | UUID | Only one program. |
min_commission_pct | number 0–100 | Minimum percentage commission. |
in_stock_only | true / false | Hide out-of-stock products. |
sort | earnings, relevance, price, rate | Default: relevance with a query, otherwise earnings. |
limit | 1–60 | Default 20. |
cursor | text | From 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.
| Field | Description |
|---|---|
domain | Required. E.g. mysite.com. A full URL is accepted and reduced to the domain. |
name | Display name, max 120 characters. Default: the domain. |
niche | E.g. Home, max 80 characters. |
monthly_traffic | Approximate 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.
| Parameter | Description |
|---|---|
from, to | Optional period. Without: all time. |
pages_per_site | 0–25 top pages per site. Default 5. |
include_minor | Include 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.
| Parameter | Description |
|---|---|
from, to | Optional period. Without: all time. |
site | Only pages on this site (domain without www). |
known_only | Hide the rows without a known page. |
min_clicks | Minimum clicks. |
limit | 1–200. Default 50. |
cursor | From next_cursor. |
Links
GET /links
Your links, newest first.
| Parameter | Description |
|---|---|
program_id | Only links for this program. |
sub_id | Only links with this sub-ID. |
search | Text in name, store, destination or sub-ID. |
archived | exclude (default), include or only. |
include_stats | true adds clicks, sales, commission, EPC and conversion per link. |
limit | 1–200. Default 50. |
cursor | From 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.
| Field | Description |
|---|---|
program_id | Required. From /catalog or /programs. |
landing_page | Path or full URL on the store's domain, e.g. /products/sofa. Default: the front page. |
sub_id | Max 80 characters. |
campaign | Max 80 characters. Sent as utm_campaign and used as the link name. |
channel | One of the program's allowed channels: SEO, Blog, Instagram, TikTok, YouTube, Facebook, Pinterest, E-mail, Website. |
utm_content, utm_term | Max 100 characters each. |
website_id | Optional site label from /websites. |
placement_url | Full URL of the page where the link will be placed (https://…). |
name | The link's name in the dashboard, max 120 characters. |
include_qr | true adds a QR code as PNG in qr_png_base64. |
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.
{
"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.
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.
GET /links/{id}
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.
PATCH /links/{id}
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.
| Parameter | Description |
|---|---|
from, to | Period. Default: the last 30 days. |
group_by | total (default), sub_id, program, link, day, site, page, source, device or country. |
program_id | Only this program. |
sub_id | Only this sub-ID. |
compare | true adds the previous period of equal length with change in percent. |
limit | 1–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.
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.
| Parameter | Description |
|---|---|
from, to | Period by sale date. Default: the last 30 days. |
status | pending, approved or rejected. |
program_id | Only this program. |
limit | 1–200. Default 50. |
cursor | From 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.
| Parameter | Description |
|---|---|
from, to | Period. Default: the last 30 days. |
site | Only clicks from this site (domain without www). |
page | Only clicks from pages starting with this URL – or unknown for clicks without a known page. |
link_id, program_id, sub_id | Filters. |
source | Only this source, e.g. Google, Instagram, Direkte. |
outcome | sale, no_sale or rejected (only rejected sales). |
limit | 1–200. Default 50. |
cursor | From 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
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_input | A parameter is missing or invalid. The message says which. |
| 400 | invalid_json | The body is not valid JSON. |
| 400 | invalid_cursor | cursor is invalid. Start over without it. |
| 400 | channel_not_allowed | The channel is not allowed in the program. The message lists the allowed ones. |
| 400 | landing_page_other_host | The landing page is not on the store's domain. |
| 400 | invalid_landing_page | The landing page is not a valid path or URL. |
| 400 | unknown_website | website_id does not exist on your account. |
| 400 | immutable_field | Sub-ID, campaign, channel, program or UTM cannot be changed on an existing link. |
| 400 | invalid_domain | Write the domain as mysite.com. |
| 401 | missing_key | No key was sent. |
| 401 | invalid_key | The key is invalid or revoked. |
| 403 | not_joined | You have not joined the program. The message has the join link to the dashboard. |
| 403 | inactive | The program or the store is not active right now. |
| 403 | account_suspended | The account is suspended. |
| 404 | not_found | The program, link or site does not exist on your account. |
| 409 | duplicate_domain | You already have a site with that domain. |
| 429 | rate_limited | Too many requests. Wait and try again. |
| 429 | daily_link_limit | 500 new links in the last 24 hours. Reuse a link with ?sid= or ?p=. |
| 500 | server_error, save_failed | Something went wrong on our side. Try again. |
| 503 | unavailable | The 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.