RefCampaign/docs

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

Payload

{
  "externalId": "ord_42",
  "conversionType": "SALE",
  "amount": 4990,
  "currency": "EUR",
  "sessionId": "rcs_7fKj2...",
  "metadata": { "plan": "pro" }
}
FieldTypeRequiredNotes
externalIdstring (1–255)yesYour order / transaction id. Acts as the idempotence key.
conversionTypestringyesOne of SALE, LEAD, TRIAL, CUSTOM.
amountintegeryesConversion value in minor units (cents). e.g. €49.90 → 4990. Decimals are rejected with 422.
currencystringyesISO 4217 3-letter code (upper-cased server-side).
sessionId / rcsidstringone ofTracking session id from the click cookie. Preferred when intact.
clickId / cidstringone ofClick id returned by the tracking URL. Alternative to sessionId.
customerEmailHashstringone ofSHA-256 hex of the lowercased customer email. Fallback when session is lost.
customerEmailstringone ofCustomer email in plaintext (hashed server-side). Fallback.
customerIdstringnoYour internal customer id.
convertedAtstringnoISO 8601 datetime of the conversion. Defaults to now.
customParamsobjectnoUp to 5 custom string params: { s1, s2, s3, s4, s5 }.
metadataobjectnoFree-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, clickIdsessionIdcustomerEmailHashcustomerEmail 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.

On this page