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/track
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/jsonPayload
{
"sessionId": "string (optional)",
"customerEmailHash": "string, SHA-256 hex (optional)",
"amount": 49.9,
"currency": "EUR",
"metadata": { "orderId": "ord_42" },
"isSynthetic": false
}| Field | Type | Required | Notes |
|---|---|---|---|
sessionId | string | one of two | Tracking session id from the click cookie. Preferred when intact. |
customerEmailHash | string | one of two | SHA-256 hex of the lowercased customer email. Fallback. |
amount | number | yes | Conversion value in the currency below. Must be positive. |
currency | string | yes | ISO 4217 3-letter code. |
metadata | object | no | Free-form. Persisted on the conversion, surfaced in exports. |
isSynthetic | boolean | no | Marks the row as a test (excluded from billing and reports). |
Either sessionId or customerEmailHash must be present. If both are passed, sessionId wins.
Compute the email hash
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
import hashlib
REFCAMPAIGN_KEY = os.environ['REFCAMPAIGN_API_KEY']
REFCAMPAIGN_URL = 'https://app.refcampaign.com/api/v1/conversions/track'
def on_stripe_completed(session):
email_hash = hashlib.sha256(
session.customer_details.email.lower().encode()
).hexdigest()
requests.post(
REFCAMPAIGN_URL,
headers={
'Authorization': f'Bearer {REFCAMPAIGN_KEY}',
'Content-Type': 'application/json',
},
json={
'customerEmailHash': email_hash,
'amount': session.amount_total / 100,
'currency': session.currency.upper(),
'metadata': {'stripeSessionId': session.id},
},
timeout=10,
)Idempotency and retries
The endpoint is idempotent on sessionId — two calls with the same sessionId for the same merchant within five minutes return the same conversion (the second call is a no-op). When using customerEmailHash only, a soft dedup matches the most recent click within the campaign's attribution window.
If your webhook handler retries on transient failure, this is safe — duplicate fires don't create duplicate conversions.
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.