Meridian handbook
Destination platform (engine-dmo)

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

This feature is part of the DMO bundle (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.

KeyNetworkSite account settingsProgramme settingsTracks hostsReporting API
awinAwinpublisher_idadvertiser_idyes
bookingBooking.comaidbooking.com
cjCJwebsite_idad_id, tracking_domain (default www.anrdoezrs.net)
directA direct deal with one businessclick_param (default ref), or template with {url} and {click}
getyourguideGetYourGuidepartner_idgetyourguide.com and its country domains
impactImpactmedia_partner_id, account_sid (for the API)tracking_domain, ad_id, campaign_idyes
partnerizePartnerizecamref
tiqetsTiqetspartnertiqets.comyes
trackedA link built in the network’s own dashboard, sent on as it is; clicks are counted, conversions imported by hand and match no click
viatorViatorpid, mcidviator.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.

Staff with affiliates.manage set up programmes on the admin’s Programmes screen. A programme has:

SettingMeaning
NetworkOne of the keys above
NameAs staff see it, and as link.affiliate.programme
MerchantOptionally a CRM organisation; a direct deal with a business always is one. Its name is link.affiliate.merchant.
HostsThe 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 settingsThe network’s programme settings above
Statusactive, paused or ended
Starts, endsOptional dates bounding when it tracks
TermsFree 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 isAffiliate true and affiliate set.
Link propertyMeaning
link.isAffiliateWhether the link goes through an affiliate programme
link.affiliate.programmeThe programme’s name
link.affiliate.merchantThe merchant: the organisation’s name, or the programme’s
link.affiliate.networkThe network’s name
GlobalMeaning
hasAffiliateLinks(value)Whether a value (an entry, with its relations) holds any affiliate link: use it to print the disclosure a page needs
templates/_shared/listing/entry.html
{% 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.

StatusWords the module reads as it (any case, spaces, underscores and hyphens ignored)
pendingpending, open, new, booked, confirmed
approvedapproved, validated, locked, completed, cleared
declineddeclined, rejected, reversed, cancelled, canceled, deleted, refunded
paidpaid

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
FlagMeaning
--networkThe network the export is from; its column mapping is used
--fileThe CSV file. Comma, semicolon or tab separated, detected from the first line. At most 100,000 rows.
--siteThe site the conversions are for (default: the default site)
--column field=ColumnName 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-layoutA 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.
--programmeThe programme id a conversion with no known click is credited to
--dry-runReport 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:

brewery-october.csv
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"}
]}
PartRule
Meridian-Signaturet=<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.
TimeWithin 5 minutes of the server’s clock
BodyAt 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.
RateAt most 60 accepted postbacks a minute per site and network
ReplayA request with a signature already received is refused
ResponseMeaning
200{"import": "...", "inserted": n, "updated": n, "unmatched": n, "problems": [...]}
400The body is not a postback
401The signature is missing, malformed, stale or wrong
404No such site or network, or no secret for it
409This postback was already received
413The body is larger than 1 MB
429Too 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):

seed/affiliates/booking.yaml
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" }
KeyMeaning
network, nameRequired
siteThe site; the default site when absent
organisationA seed organisation’s key: the merchant
hosts, params, auto_linkHosts, programme settings, and host tracking (default true)
accountThe site’s account settings for the network
status, starts, endsactive, paused or ended; relative dates such as -200d
termsFree text
linksManaged links: slug, name, destination, retired
trafficGenerated clicks and conversions for a demonstration site

Commands

CommandWhat it does
engine-dmo affiliates networksEvery 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

PermissionAllowsBuilt-in roles
affiliates.viewThe programmes, conversions, report and networks screensadministrator, partnership-manager, analyst
affiliates.manageChanging programmes, links and accounts, importing, connectingadministrator, partnership-manager