Server-to-server postback
Track conversions by calling the REST API directly from your backend, without the SDK.
The SDK is the recommended path for JavaScript and TypeScript stacks. If you're on a different runtime — PHP, Python, Ruby, Go, Elixir — or you just don't want a dependency, call the public REST endpoints directly. This guide covers the pattern most merchants use: a server-to-server postback fired from your checkout backend after a successful payment.
When to use postback
Postback is the right pattern when:
- The conversion happens server-side (Stripe webhook, PayPal IPN, custom payment processor).
- You need delivery guarantees stricter than what a browser pixel can offer.
- You operate on a non-JS stack.
- The browser session may be lost between the click and the conversion (Safari ITP, cross-device, mobile app).
If the conversion is tied to a page the customer is currently visiting, the SDK is simpler.
Endpoint
POST /api/v1/conversions/postback
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/jsonPayload
{
"externalId": "ord_42",
"conversionType": "SALE",
"amount": 4990,
"currency": "EUR",
"sessionId": "rcs_7fKj2...",
"metadata": { "plan": "pro" }
}| Field | Type | Required | Notes |
|---|---|---|---|
externalId | string (1–255) | yes | Your order / transaction id. Acts as the idempotence key. |
conversionType | string | yes | One of SALE, LEAD, TRIAL, CUSTOM. |
amount | integer | yes | Conversion value in minor units (cents). e.g. €49.90 → 4990. Decimals are rejected with 422. |
currency | string | yes | ISO 4217 3-letter code (upper-cased server-side). |
sessionId / rcsid | string | one of | Tracking session id from the click cookie. Preferred when intact. |
clickId / cid | string | one of | Click id returned by the tracking URL. Alternative to sessionId. |
customerEmailHash | string | one of | SHA-256 hex of the lowercased customer email. Fallback when session is lost. |
customerEmail | string | one of | Customer email in plaintext (hashed server-side). Fallback. |
customerId | string | no | Your internal customer id. |
convertedAt | string | no | ISO 8601 datetime of the conversion. Defaults to now. |
customParams | object | no | Up to 5 custom string params: { s1, s2, s3, s4, s5 }. |
metadata | object | no | Free-form. Persisted on the conversion, surfaced in exports. |
At least one attribution identifier (sessionId/rcsid, clickId/cid, customerEmailHash, or customerEmail) must be present. If multiple are passed, clickId → sessionId → customerEmailHash → customerEmail in priority order.
Compute the email hash
Hash on the server, not the client
The hash is a fallback identifier; if you compute it in the browser, an attacker can replay it to attribute conversions to themselves. Always hash on your server, after you've verified the email belongs to the paying customer.
import hashlib
email = customer_email.lower().strip()
email_hash = hashlib.sha256(email.encode()).hexdigest()$emailHash = hash('sha256', strtolower(trim($email)));require 'digest'
email_hash = Digest::SHA256.hexdigest(email.downcase.strip)The hash is what we store. The plaintext email never leaves your infrastructure.
Example: Stripe webhook handler
A typical pattern: your server already receives a Stripe checkout.session.completed webhook. Extend the handler to fire a RefCampaign postback.
# Python / Flask
import os
import requests
REFCAMPAIGN_KEY = os.environ['REFCAMPAIGN_API_KEY']
REFCAMPAIGN_URL = 'https://app.refcampaign.com/api/v1/conversions/postback'
def on_stripe_completed(session):
requests.post(
REFCAMPAIGN_URL,
headers={
'Authorization': f'Bearer {REFCAMPAIGN_KEY}',
'Content-Type': 'application/json',
},
json={
'externalId': session.id,
'conversionType': 'SALE',
'amount': session.amount_total, # already in cents
'currency': session.currency.upper(),
},
timeout=10,
)Idempotency and retries
The endpoint is idempotent on externalId — a repeat call with the same externalId for the same merchant returns the existing conversion ({ success: true, conversionId, message: "Conversion already tracked" }) without creating a duplicate. If the same externalId is submitted by a different merchant, the server returns 409.
If your webhook handler retries on transient failure, this is safe — sending the same externalId again never creates a duplicate conversion.
For network-level retries: respect the Retry-After header on 429, treat 5xx as transient and back off, treat 4xx as terminal (no retry, log + alert).
Verifying delivery
The merchant dashboard shows incoming conversions in real time. For automated pipelines, query the conversion list to confirm the row landed:
curl https://app.refcampaign.com/api/v1/conversions?campaignId=cmp_xxx&limit=10 \
-H "Authorization: Bearer $REFCAMPAIGN_API_KEY"Security
- Send the API key only over HTTPS.
- Store it in a secret manager — never in source code.
- Use a dedicated key per environment (production / staging / local).
- See authentication for the full checklist.