MMeerPartners docs

API: tracking and postback

MeerPartners tracking endpoints: redirect /r/{code}, pixel /p/{code}.gif and conversion intake POST /api/v1/postback with an HMAC signature.

Tracking is the "hot path" of attribution: a link click records a click and sets a cookie, and the conversion comes back via an S2S postback. The redirect and the pixel live outside the version (/r/..., /p/...) for speed; the postback is under /api/v1. Step-by-step guides: Tracking links and Postback.

GET /r/{code}

GET/r/{code}🔒

Redirect via a tracking link. Resolves the offer, records the click (asynchronously, without blocking the response), sets a signed attribution cookie aff_attr and returns a 302 to the landing. This is "always-302": even if the write fails, the click goes to a fallback log and the redirect still happens.

Path: code — the short link code (from POST /affiliate/links).

Query parameters (optional):

sub_id1stringoptional
Analytics label (truncated to 128 characters). Also accepted as sub1.
sub_id2stringoptional
Label. Also sub2.
sub_id3stringoptional
Label. Also sub3.
sub_id4stringoptional
Label. Also sub4.
sub_id5stringoptional
Label. Also sub5.

Response: 302 Found. The Location header points to the offer landing with an appended aff_click=<click_uid> (a server-side attribution fallback for Telegram/WebView, where the cookie is often lost). The aff_attr cookie is set (HMAC-signed, Max-Age = the offer's attribution window, SameSite=Lax). For bots the cookie is not set.

Response example
HTTP/1.1 302 Found
Location: https://acme.com/landing?utm=promo&aff_click=8f1c0a2e-...
Set-Cookie: aff_attr=eyJ...; Max-Age=2592000; Path=/; SameSite=Lax

Errors: TRACKING_LINK_NOT_FOUND (404 — unknown code), LINK_INACTIVE (410 — the link is disabled, no fallback), OFFER_INACTIVE (410 — the offer is inactive, no fallback), OFFER_NO_LANDING (503 — an active offer has no landing set), TRACKING_RATE_LIMITED (429). If a fallback_url is present, an inactive link/offer redirects to it instead of 410.

GET /p/{code}.gif

GET/p/{code}.gif🔒

Pixel fallback: the same click and cookie recording, but instead of a redirect it returns a transparent 1×1 GIF. Convenient to embed in an <img> where you cannot redirect (emails, widgets).

Path/Query — same as /r/{code} (sub_id1..5).

Response: 200 OK, Content-Type: image/gif, the header Cache-Control: no-store, no-cache, must-revalidate, private, body — a 43-byte GIF. The aff_attr cookie is set the same way (except for bots).

POST /api/v1/postback

POST/api/v1/postback🔒 HMAC

Conversion intake S2S from the business. Signed with the tenant_api_key with the postback scope. The HMAC is computed over the raw request body — do not reassemble the JSON after signing.

Headers

X-Api-Key-Idstringrequired
The public key identifier (key_id).
X-Timestampstringrequired
Unix time (epoch, sec). Must be within ±300 s of the server time, otherwise POSTBACK_STALE_TIMESTAMP.
X-Signaturestringrequired
hex(HMAC_SHA256(secret, signing_string)).
X-Tenant-Idstringoptional
Business ID. The source of trust is the key in the database; the tenant from the body is ignored.

signing_string = "{method}\n{path}\n{X-Timestamp}\n{sha256_hex(raw_body)}". A full breakdown and signature examples in different languages are on the Postback page.

Body

external_order_idstringrequired
The order identifier on the business side (1–128 characters). The deduplication/idempotency key.
event_typestringrequired
Event type: sale, lead, registration, refund, chargeback.
refstringoptional
click_id (a.k.a. click_uid) or the link code. Up to 128 characters.
sub_idstringoptional
An additional partner label (up to 128 characters).
amountstringoptional
The order amount (NUMERIC). Required for sale (together with currency).
currencystringoptional
Currency (ISO code). Required for sale.
statusstringoptional
approved or pending.
occurred_atstringoptional
Event time (ISO 8601). Defaults to the moment of intake.
metaobjectoptional
Arbitrary additional data.
Request
{
  "external_order_id": "ORD-100500",
  "event_type": "sale",
  "ref": "8f1c0a2e-7b3d-4c1a-9f2e-1a2b3c4d5e6f",
  "amount": "2990.00",
  "currency": "RUB",
  "status": "approved",
  "occurred_at": "2026-06-14T12:30:00Z"
}

Response

acceptedbooleanrequired
Whether the event was accepted for processing.
duplicatebooleanrequired
true if it is a repeat by external_order_id (an idempotent replay).
conversion_idintegeroptional
ID of the created/found conversion.
commissionobjectoptional
The accrued commission (if any).
reasonstringoptional
The reason on accepted:false (for example POSTBACK_UNKNOWN_REF, POSTBACK_REFUND_NO_CONVERSION).
200 response (conversion counted)
{
  "accepted": true,
  "duplicate": false,
  "conversion_id": 778,
  "commission": {
    "id": 9100,
    "amount": "750.00",
    "currency": "RUB",
    "status": "pending",
    "hold_until": "2026-06-28T12:30:00Z"
  }
}
200 response (repeat)
{ "accepted": true, "duplicate": true, "conversion_id": 778 }

2xx even on accepted:false

Logically processed events (including "no click found by ref" and a repeat) return 200 with the accepted/duplicate/reason flags. HTTP errors (4xx/5xx) are only for authentication, signature, rate-limit or validation problems. More on idempotency and dedup is on the Rate limits and idempotency page.

Errors: TENANT_API_KEY_INVALID (401), TENANT_API_KEY_SCOPE_DENIED (403 — no postback scope), POSTBACK_BAD_SIGNATURE (401), POSTBACK_STALE_TIMESTAMP (400), POSTBACK_REPLAY (409 — repeated signature), POSTBACK_TENANT_MISMATCH (403), POSTBACK_MISSING_FIELD (422 — a required field is missing), POSTBACK_RATE_LIMITED (429), TENANT_SUSPENDED (403).

What's next