RefCampaign

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/json

Payload

{
  "sessionId": "string (optional)",
  "customerEmailHash": "string, SHA-256 hex (optional)",
  "amount": 49.9,
  "currency": "EUR",
  "metadata": { "orderId": "ord_42" },
  "isSynthetic": false
}
FieldTypeRequiredNotes
sessionIdstringone of twoTracking session id from the click cookie. Preferred when intact.
customerEmailHashstringone of twoSHA-256 hex of the lowercased customer email. Fallback.
amountnumberyesConversion value in the currency below. Must be positive.
currencystringyesISO 4217 3-letter code.
metadataobjectnoFree-form. Persisted on the conversion, surfaced in exports.
isSyntheticbooleannoMarks 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.

On this page