MMeerPartners docs

Postback (S2S conversions)

The MeerPartners server-to-server postback: how to report a conversion to the platform. Request fields, idempotency by external_order_id, responses and rejection codes. Examples in cURL, Node.js and Python.

A postback is a server-side notification from your backend to the platform that a conversion has occurred on your side: a sale, lead, registration, refund or chargeback. This is the core integration mechanism for a business: it is from the postback that the platform creates the conversion, attributes it to the partner and accrues the commission.

POST/api/v1/postback🔒 HMAC

The request is signed with HMAC over the secret of an API key with the postback scope. The signing algorithm and headers (X-Api-Key-Id, X-Timestamp, X-Signature, and the optional X-Tenant-Id) are on the API keys and signing page. Without a valid signature the request is not processed.

The click comes first

For a conversion to have something to be attributed to, you must have the user's click_id. The partner's tracking link issues it as aff_click during the redirect. Store it next to the order and pass it to the postback as ref. See Tracking links and macros.

Request fields

The body is JSON. Content-Type: application/json.

external_order_idstringrequired

A unique order/event ID in your system (1–128 characters). This is the idempotency and deduplication key — the platform uses it to tell a retry apart from a new event.

event_typestringrequired

The event type: sale, lead, registration, refund, chargeback.

refstringoptional

Attribution: a click_id (the aff_click value from the redirect) or the tracking link code. Required for sale/lead/registration; not needed for refund/chargeback (the event finds the original conversion by external_order_id).

sub_idstringoptional

The partner's pass-through label (up to 128 characters), if you passed it through and want to keep it in the conversion.

amountstringoptional

The order amount as a string (to avoid precision loss), e.g. "19.99". Required for sale together with currency.

currencystringoptional

The amount's currency, ISO-4217, 3 letters — "USD", "RUB". Required for sale.

statusstringoptional

approved or pending. Affects how the conversion starts; by default a conversion starts in pending and goes through the hold period.

occurred_atstringoptional

The event time on your side in RFC 3339 format (2026-06-01T10:15:00Z). Defaults to the moment of receipt.

metaobjectoptional

Arbitrary data (plan, promo code, etc.). Stored in the conversion for analytics and investigation.

Required fields depend on event_type

For sale you need external_order_id, event_type, ref, amount, currency. For lead/registrationexternal_order_id, event_type, ref (amount is optional). For refund/chargeback — only external_order_id and event_type. If a required field is missing — POSTBACK_MISSING_FIELD (422).

Idempotency

The platform deduplicates events by the pair (your tenant, external_order_id) and the event type — under an advisory lock, so even a race between two identical postbacks will not create two conversions.

  • The first successful postback creates the conversion and the commission.
  • A retry with the same external_order_id → the same result and "duplicate": true, with no side effects.

This means it is safe to retry: resending will not double the accrual.

Responses

The response is a "bare" JSON object (no data wrapper). HTTP code 200 on a successful or duplicate processing.

Success — sale attributed
{
  "accepted": true,
  "duplicate": false,
  "conversion_id": 7741,
  "commission": {
    "id": 7741,
    "amount": "2.50",
    "currency": "USD",
    "status": "pending",
    "hold_until": "2026-06-15T10:15:00Z"
  }
}
Idempotent retry
{ "accepted": true, "duplicate": true, "conversion_id": 7741 }
Accepted, but not attributed (organic)
{ "accepted": false, "reason": "POSTBACK_UNKNOWN_REF" }
Response fieldValue
acceptedtrue if the event was taken for processing; false for organic/non-applicable refund
duplicatetrue if this is a retry of an already-processed external_order_id
conversion_idThe ID of the created (or found) conversion
commissionBrief info on the zero-level commission: id, amount, currency, status, hold_until (only on successful attribution to a partner)
reasonThe reason when accepted: false (e.g. POSTBACK_UNKNOWN_REF)

accepted:false is not a retryable error

A 200 response with accepted: false (no attribution, or a refund for a non-existent conversion) is final. It is not a connectivity failure: it means the sale is organic or the click was not found. Do not retry such responses in a loop — handle them as a normal outcome. A retry is justified only on 5xx/timeout.

Rejection codes

CodeHTTPWhenReaction
TENANT_API_KEY_INVALID401Key is unknown, revoked or expiredCheck X-Api-Key-Id/secret, do not retry
TENANT_API_KEY_SCOPE_DENIED403The key lacks the postback scopeIssue a key with the postback scope
POSTBACK_BAD_SIGNATURE401The HMAC signature did not matchCheck the signing algorithm and that you signed the same bytes
POSTBACK_STALE_TIMESTAMP400X-Timestamp outside the ±300 s windowSync the clock (NTP), rebuild the request
POSTBACK_REPLAY409The same signature replayed within the memory windowBuild a new request with a current X-Timestamp
POSTBACK_TENANT_MISMATCH403X-Tenant-Id did not match the key's tenantRemove/fix X-Tenant-Id
POSTBACK_MISSING_FIELD422A required field for the event_type is missingAdd the missing field
POSTBACK_UNKNOWN_REF200No attributable click (accepted:false)Normal outcome — do not retry
POSTBACK_REFUND_NO_CONVERSION200Refund for a non-existent conversion (accepted:false)Normal outcome — do not retry
POSTBACK_RATE_LIMITED429Too many postbacks per keySlow down, retry with backoff
SERVICE_UNAVAILABLE503The DB/store is unavailableRetry with exponential backoff

The full error envelope format ({ "error": { "code", "message", "details" } }) is on the API error codes page.

Retry strategy

Retry only 5xx and network timeouts — with exponential backoff. On any 2xx (including accepted:false) — stop. On 4xx (except 409 POSTBACK_REPLAY, where you must rebuild the signature) — stop and alert: this is usually an integration bug, not a transient failure.

Full example

A sale conversion with signature computation. The base URL is {API_BASE} (the exact domain is provided by the operator).

API_BASE="https://api.meerpartners.com"
KEY_ID="key_3f9a1c"
SECRET="sk_live_9f2a…"
 
BODY='{"external_order_id":"ORD-558123","event_type":"sale","ref":"clk_5f2a9e","amount":"19.99","currency":"USD","status":"approved"}'
TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')
SIGNING=$(printf '%s\n%s\n%s\n%s' "POST" "/api/v1/postback" "$TS" "$BODY_HASH")
SIG=$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
 
curl -sS -X POST "$API_BASE/api/v1/postback" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key-Id: $KEY_ID" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  --data "$BODY"

Test before going live

Before connecting a live stream, run the test postback from the cabinet (POST /api/v1/merchant/integration/test-postback) — it goes through the real flow but is safe for checking the key ↔ offer binding.

What's next