All integrations

Any platform

One signed webhook away.

Published · Last reviewed

Written by Attribloom Engineering · Reviewed by Attribloom Product · Last reviewed

Live today

How it works

On a conversion your server fires one HMAC-SHA256-signed POST to our postback endpoint. We verify the signature, attribute the conversion to the affiliate whose link drove it, and record the commission.

We deduplicate on externalId, so retries and double-fires are safe. If a sale is later refunded we claw the commission back automatically, within your configured refund window.

This works for web checkout, games, SaaS, no-code site builders, or any custom platform where your server can make an outbound HTTPS request at the moment of conversion.

Setup

  1. 1

    Grab your signing secret

    Log in, go to Integrations, and copy the postback secret for your surface. Keep it server-side only.

  2. 2

    Sign the request

    HMAC-SHA256 sign "${timestamp}." + rawBody with the secret. Timestamp is unix seconds as a string. Prefix the hex digest with sha256=.

  3. 3

    POST to the conversions endpoint

    Send the JSON body with x-ea-surface (your surface id), x-ea-timestamp (unix seconds), and x-ea-signature (sha256= + hex). Include clickId from the tracking link when you have it.

  4. 4

    We attribute, dedupe, and clawback

    We respond 200 with attribution status. Retry on network errors: duplicate externalIds are safe.

Sign and send

Signed conversion postback

Timestamp is unix seconds (not ms). Signature is sha256= + hex of HMAC-SHA256 over "${timestamp}." + rawBody. The skew window is 300 seconds.

javascript
import crypto from "node:crypto";

const POSTBACK_URL = "https://api.attribloom.com/v1/conversions";
const SURFACE_ID   = "<your-surface-id>";
const SECRET       = process.env.ATTRIBLOOM_POSTBACK_SECRET; // keep server-side only

const ts   = Math.floor(Date.now() / 1000).toString();  // unix SECONDS (not ms)
const body = JSON.stringify({
  externalId: "order_12345",           // your stable id for this conversion (dedupe key)
  eventType:  "sale",                  // sale | subscribe | install | ...
  grossMinor: 4999,                    // 49.99 in minor units (integer)
  currency:   "USD",
  occurredAt: new Date().toISOString(),
  clickId:    "<click id from the tracking link>", // optional but improves attribution
});

// Sign "${timestamp}." + rawBody with the surface secret, prefixed with "sha256=".
const sig = "sha256=" + crypto
  .createHmac("sha256", SECRET)
  .update(`${ts}.`)
  .update(body)
  .digest("hex");

const res = await fetch(POSTBACK_URL, {
  method:  "POST",
  headers: {
    "content-type":   "application/json",
    "x-ea-surface":   SURFACE_ID,
    "x-ea-timestamp": ts,   // unix seconds string
    "x-ea-signature": sig,  // "sha256=" + hex
  },
  body,
});
// 200 = accepted (idempotent on externalId). 401 = bad signature/skew. 400 = bad body.

Seconds, not milliseconds

The timestamp must be unix seconds and the signature must carry the sha256= prefix. A millisecond timestamp or a bare hex digest fails the 300s skew or prefix check and returns 401.

Report a refund

POST to /v1/conversions/refund with the same surface signing headers. externalId identifies the original conversion. refundId is the stable idempotency key for this refund event. refundedMinor is the integer amount refunded by this event, so a partial refund sends only the partial amount.

Signed full or partial refund

javascript
const REFUND_URL = "https://api.attribloom.com/v1/conversions/refund";
const ts = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({
  externalId: "order_12345",  // the original conversion externalId
  refundId: "refund_67890",   // stable id for this refund event
  refundedMinor: 2500,         // this partial refund: 25.00 USD
  currency: "USD",
  eventType: "refund",         // or refund_reversed
});
const sig = "sha256=" + crypto
  .createHmac("sha256", SECRET)
  .update(`${ts}.`)
  .update(body)
  .digest("hex");

const res = await fetch(REFUND_URL, {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "x-ea-surface": SURFACE_ID,
    "x-ea-timestamp": ts,
    "x-ea-signature": sig,
  },
  body,
});
// Replaying the same refundId is safe. 404 means externalId was not found.
// 409 means the currency differs from the original conversion.
HTTPMeaning and action
200Applied, or the same refundId was already applied. Do not retry.
400Invalid body. Fix the request before retrying.
401Missing headers, bad secret/signature, or timestamp skew over 300 seconds.
404No conversion matches externalId on this surface.
409Refund currency does not match the original conversion currency.
5xxTransient failure. Retry with backoff, a fresh timestamp, and a fresh signature.

Refund reversal

If your platform reverses a previously reported refund, send the required refundId with eventType: refund_reversed. Replaying the reversal is safe because Attribloom only reverses a conversion still marked refunded. Do not use refund_reversed for a second refund.

The complete machine contract, including request and response schemas, is at OpenAPI 3.1.

Live today. Works for any platform.

Further reading

Get startedI'm an affiliateTalk to us