Affiliates
How a site earns as an affiliate publisher: programmes with networks or direct deals, tracked links under /go/, the disclosure templates print, importing conversions by CSV, network connectors or a signed postback, the postback's signature and body, and the affiliate commands.
engine-dmo
The affiliates module makes the site an affiliate publisher. Staff set up programmes with merchants, through an affiliate network or as a direct deal with a business. A link to a merchant, in a link field or in rich text, then becomes a tracked first-party redirect under /go/, marked rel="sponsored", which templates can tell apart to print a disclosure. The conversions a network reports are imported and matched to the clicks that led to them, so each programme’s and each page’s earnings are known.
Editors write ordinary links; nothing in the content changes. The tracking is added as the page renders.
Networks
The engine knows these networks. engine-dmo affiliates networks prints each one with the settings it needs and the export columns it reads.
| Key | Network | Site account settings | Programme settings | Tracks hosts | Reporting API |
|---|---|---|---|---|---|
awin | Awin | publisher_id | advertiser_id | yes | |
booking | Booking.com | aid | booking.com | ||
cj | CJ | website_id | ad_id, tracking_domain (default www.anrdoezrs.net) | ||
direct | A direct deal with one business | click_param (default ref), or template with {url} and {click} | |||
getyourguide | GetYourGuide | partner_id | getyourguide.com and its country domains | ||
impact | Impact | media_partner_id, account_sid (for the API) | tracking_domain, ad_id, campaign_id | yes | |
partnerize | Partnerize | camref | |||
tiqets | Tiqets | partner | tiqets.com | yes | |
tracked | A link built in the network’s own dashboard, sent on as it is; clicks are counted, conversions imported by hand and match no click | ||||
viator | Viator | pid, mcid | viator.com |
A site’s account settings for a network are entered once on the admin’s Networks screen (under Affiliates). Each network passes the click id the way it expects (as a label, campaign or parameter), which is how a reported conversion finds its click.
Programmes and links
Staff with affiliates.manage set up programmes on the admin’s Programmes screen. A programme has:
| Setting | Meaning |
|---|---|
| Network | One of the keys above |
| Name | As staff see it, and as link.affiliate.programme |
| Merchant | Optionally a CRM organisation; a direct deal with a business always is one. Its name is link.affiliate.merchant. |
| Hosts | The merchant’s hosts. Defaults to the network’s own hosts. |
| Track links to these hosts | (auto_link, on by default) Any link to one of the hosts, or a subdomain, is tracked through the programme |
| Programme settings | The network’s programme settings above |
| Status | active, paused or ended |
| Starts, ends | Optional dates bounding when it tracks |
| Terms | Free text: the commission agreed |
and its managed links, each a slug, a name and a destination. A managed link is written in content as /go/<slug> (or the site’s full address of it), and a link whose destination is exactly a managed link’s destination is treated as that managed link. Retire a managed link when it should stop being offered.
A link is tracked when it is a managed link, or when it goes to a host of an active programme with host tracking on; the most specific host wins. A programme that is paused, ended or outside its dates still sends visitors on, to the merchant, untracked.
On the public site
Every link a page renders, from a link field or inside rich text, passes through the module. A tracked link:
- points at the site’s own
/go/<slug>(a managed link) or/go/u/<token>(a host-tracked link, signed so it cannot be forged or credited to another page); - carries
rel="sponsored"; - has
isAffiliatetrue andaffiliateset.
| Link property | Meaning |
|---|---|
link.isAffiliate | Whether the link goes through an affiliate programme |
link.affiliate.programme | The programme’s name |
link.affiliate.merchant | The merchant: the organisation’s name, or the programme’s |
link.affiliate.network | The network’s name |
| Global | Meaning |
|---|---|
hasAffiliateLinks(value) | Whether a value (an entry, with its relations) holds any affiliate link: use it to print the disclosure a page needs |
{% if hasAffiliateLinks(listing) %}<p class="affiliate-disclosure">{{ t("affiliate.disclosure") }}</p>{% endif %}
{% if listing.website %}
<p><a href="{{ listing.website.url|url }}"{% if listing.website.rel %} rel="{{ listing.website.rel }}"{% endif %}>{{ listing.website.text }}</a>
{% if listing.website.isAffiliate %}<span class="affiliate-badge">{{ t("affiliate.badge", "merchant", listing.website.affiliate.merchant) }}</span>{% endif %}</p>
{% endif %} Print rel from the link, as above, so sponsored reaches the page.
When a visitor follows the link, the redirect builds the network’s tracked link with a fresh click id, records the click against the programme and the page it was on, and sends the visitor on. One visitor’s clicks on one programme are counted at most 20 times an hour; past that they are still sent on and nothing is counted. Clicks are kept for 400 days, long enough for a network to report a stay booked a year ahead.
From the click to the earnings it led to:
sequenceDiagram participant V as Visitor participant S as Your site participant M as Merchant (via network) participant N as Network V->>S: GET /go/<slug> or /go/u/<token> S->>S: record the click with a new click id S-->>V: redirect to the network's tracked link V->>M: books or buys M->>N: reports the sale with the click id N->>S: conversion by CSV import, API sync or signed postback S->>S: match the click id, credit the programme and page
Conversions
A conversion is a sale a network reports: a transaction id, the click id it came from (when it came through a tracked link), a status, the commission, and optionally the sale amount, currency and when it happened. Importing the same transaction again updates it in place, so a later report that moves pending to approved never counts it twice.
| Status | Words the module reads as it (any case, spaces, underscores and hyphens ignored) |
|---|---|
pending | pending, open, new, booked, confirmed |
approved | approved, validated, locked, completed, cleared |
declined | declined, rejected, reversed, cancelled, canceled, deleted, refunded |
paid | paid |
Each network may add words of its own. A conversion whose click id matches a click is credited to that click’s programme and page; one with no known click is unmatched, or credited to a fallback programme if the import names one. The admin’s Conversions screen lists them, and the Report screen shows earnings by programme and by page.
Conversions arrive in three ways.
1. A CSV export
Upload a network’s transaction export on the Conversions screen, or:
engine-dmo affiliates import --network awin --file transactions.csv --site main --dry-run
engine-dmo affiliates import --network awin --file transactions.csv --site main | Flag | Meaning |
|---|---|
--network | The network the export is from; its column mapping is used |
--file | The CSV file. Comma, semicolon or tab separated, detected from the first line. At most 100,000 rows. |
--site | The site the conversions are for (default: the default site) |
--column field=Column | Name a column by hand, repeatable: --column id="Order Ref" --column occurred_at=Created. The fields are id, click, status, amount, commission, currency, occurred_at. |
--date-layout | A Go time layout for dates that are not ISO 8601, such as 02/01/2006 for day first. A date without a zone is UTC. Day-first and month-first dates are never guessed. |
--programme | The programme id a conversion with no known click is credited to |
--dry-run | Report what the export holds and write nothing |
The transaction id, the status and the commission are required in every row; a row that cannot be read is reported with its line and the rest are imported. Amounts may carry currency signs and grouping separators (1,234.50 and 1.234,50 are both read as 1234.50).
A direct deal’s business reports in the module’s own format, which any network’s export can also be reshaped into:
id,click,status,amount,commission,currency,occurred_at
T-1042,8f3c2a91d0,approved,45.00,4.50,GBP,2026-10-03T14:20:00Z
T-1043,,pending,30.00,3.00,GBP,2026-10-05 2. A network’s reporting API
Awin, Impact and Tiqets can be connected, after which the engine reads their conversions every 6 hours, looking back 45 days. Store the site’s secret for the network (Awin’s token, Impact’s auth token with the Account SID set on the Networks screen, or Tiqets’ token):
AWIN_TOKEN=... engine-dmo affiliates connect --network awin --site main --secret-env AWIN_TOKEN
engine-dmo affiliates sync --network awin --site main --days 45 connect reads the secret from the named environment variable or from standard input, never from an argument, and stores it sealed with the install’s key; it is never printed. The Networks screen can also connect and sync. Booking.com, GetYourGuide and Viator have no usable reporting API and stay on CSV.
3. The signed postback
A network or a business’s own system can push conversions as they happen. Make a secret for the site and network:
engine-dmo affiliates postback-key --network direct --site main The secret is printed once and stored sealed; running the command again replaces it, which is how a leaked secret is rotated. Until a site has a secret for a network, its postback address answers 404.
The sender posts JSON to /affiliates/postback/<site>/<network> on the site’s host:
POST /affiliates/postback/main/direct HTTP/1.1
Content-Type: application/json
Meridian-Signature: t=1793611200,v1=5d41402abc4b2a76b9719d911017c592...
{"conversions": [
{"id": "T-1042", "click": "8f3c2a91d0", "status": "approved", "amount": "45.00",
"commission": "4.50", "currency": "GBP", "occurred_at": "2026-10-03T14:20:00Z"}
]} | Part | Rule |
|---|---|
Meridian-Signature | t=<unix seconds>,v1=<signature>, where the signature is the lowercase hex HMAC-SHA256, keyed with the secret, of the time, a full stop and the raw body (t + "." + body). Several v1= values may be given. |
| Time | Within 5 minutes of the server’s clock |
| Body | At most 1 MB; conversions holds 1 to 1000 objects with exactly the keys id, click, status, amount, commission, currency, occurred_at (strings). Unknown keys are refused. |
| Rate | At most 60 accepted postbacks a minute per site and network |
| Replay | A request with a signature already received is refused |
| Response | Meaning |
|---|---|
200 | {"import": "...", "inserted": n, "updated": n, "unmatched": n, "problems": [...]} |
400 | The body is not a postback |
401 | The signature is missing, malformed, stale or wrong |
404 | No such site or network, or no secret for it |
409 | This postback was already received |
413 | The body is larger than 1 MB |
429 | Too many postbacks, or too many failed signatures from the address; retry after a minute |
Signing in a shell, for a test:
body='{"conversions":[{"id":"T-1","click":"","status":"pending","amount":"","commission":"2.50","currency":"GBP","occurred_at":"2026-11-01T10:00:00Z"}]}'
t=$(date +%s)
sig=$(printf '%s.%s' "$t" "$body" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST "https://www.example.org/affiliates/postback/main/direct" \
-H "Content-Type: application/json" -H "Meridian-Signature: t=$t,v1=$sig" --data "$body" Seeding programmes
A seed set may hold programmes under affiliates/<key>.yaml, which engine-dmo seed apply creates (see Seed data):
network: booking
name: Booking.com
account: { aid: "1234567" }
terms: "4% of each stay's value, confirmed after checkout."
links:
- { slug: wharf-hotel, name: The Wharf Hotel on Booking.com, destination: "https://www.booking.com/hotel/gb/the-wharf.html" } | Key | Meaning |
|---|---|
network, name | Required |
site | The site; the default site when absent |
organisation | A seed organisation’s key: the merchant |
hosts, params, auto_link | Hosts, programme settings, and host tracking (default true) |
account | The site’s account settings for the network |
status, starts, ends | active, paused or ended; relative dates such as -200d |
terms | Free text |
links | Managed links: slug, name, destination, retired |
traffic | Generated clicks and conversions for a demonstration site |
Commands
| Command | What it does |
|---|---|
engine-dmo affiliates networks | Every network, its settings and the export columns it reads (no database) |
engine-dmo affiliates import ... | Import a CSV export |
engine-dmo affiliates connect --network <n> [--site] [--secret-env <VAR>] | Store a reporting API secret |
engine-dmo affiliates sync [--network] [--site] [--days 45] | Read the connected networks’ conversions now |
engine-dmo affiliates postback-key --network <n> [--site] | Make or replace the postback secret, printed once |
Permissions
| Permission | Allows | Built-in roles |
|---|---|---|
affiliates.view | The programmes, conversions, report and networks screens | administrator, partnership-manager, analyst |
affiliates.manage | Changing programmes, links and accounts, importing, connecting | administrator, partnership-manager |
Member reports
What the member report tells a business about its listings, where members read it (portal, member API, PDF, monthly digest), how to configure the digest, the peer comparison and the improvement prompts, scheduled organisation reports for a board, and the report commands.
Paid listings
How to sell membership tiers and promotions: prices in config/paidlistings.yaml, Stripe Checkout or staff invoices, the portal's billing screen, the Stripe webhook, orders and their statuses, and how a paid order grants, renews, refunds and lapses a membership or a promotion.