Any platform
One signed webhook away.
Published · Last reviewed
Written by Attribloom Engineering · Reviewed by Attribloom Product · Last reviewed
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
Grab your signing secret
Log in, go to Integrations, and copy the postback secret for your surface. Keep it server-side only.
- 2
Sign the request
HMAC-SHA256 sign
"${timestamp}." + rawBodywith the secret. Timestamp is unix seconds as a string. Prefix the hex digest withsha256=. - 3
POST to the conversions endpoint
Send the JSON body with
x-ea-surface(your surface id),x-ea-timestamp(unix seconds), andx-ea-signature(sha256=+ hex). IncludeclickIdfrom the tracking link when you have it. - 4
We attribute, dedupe, and clawback
We respond
200with attribution status. Retry on network errors: duplicateexternalIds 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.
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
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.| HTTP | Meaning and action |
|---|---|
200 | Applied, or the same refundId was already applied. Do not retry. |
400 | Invalid body. Fix the request before retrying. |
401 | Missing headers, bad secret/signature, or timestamp skew over 300 seconds. |
404 | No conversion matches externalId on this surface. |
409 | Refund currency does not match the original conversion currency. |
5xx | Transient 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