Postback and attribution
When a customer you referred buys, we credit the sale to your account. If you want to hear about it on your own systems, we also call a URL of yours: the postback. We set it up with you at onboarding, so there is nothing to register through the API.
How a sale is credited
Your account is mapped to your utm_campaign. Through the API you pass it in tracking.queryParams on
every search, and we merge it into the handoff URLs. On links you build yourself, it goes on the link.
Use the utm_source, utm_medium, and utm_campaign values we give you exactly: a different value
can stop the sale from being credited to you.
A sale is credited to you when:
- the customer first arrived at Chaiz through a link carrying your campaign,
- that first visit was within the 30 days before the purchase, and
- it is the customer's first purchase with Chaiz.
If you need to know which of your clicks converted, put your own identifier on the link. See your parameters.
Your parameters
Add your own parameters to the link, or to tracking.queryParams on the search, for example
acme_click=7f3a or acme_campaign=spring. Use your own prefix so nothing collides with ours.
At onboarding, tell us:
- which parameter carries your click id, if you use one, and
- which of your parameters you want back in the postback.
We store them when the customer arrives and send them back with the sale.
Do not put a customer's name, email, phone, or address in a parameter. We send back what you put on the link, and your postback endpoint will log it.
What we send
One postback per credited sale, once the payment is accepted and the contract is issued. Nothing fires on a checkout start, a failed payment, a cancellation, or a refund. With a click id, the postback is sent at most once per click id.
The postback is built from a template we agree with you. It carries your parameters and our order id, the id of the sale on our side. Quote the order id when you ask us about a sale.
You pick GET or POST at onboarding.
GET
Your URL, with your parameters and the order id in the query string:
GET https://example.com/conversions?acme_click=7f3a&acme_campaign=spring&order_id=3f2b8c1e-5d4a-4b7e-9c1f-2a6d8e0b4c71
POST
Your URL, with a JSON body in the shape you ask for:
POST https://example.com/conversions
Content-Type: application/json
{"acme_click":"7f3a","acme_campaign":"spring","order_id":"3f2b8c1e-5d4a-4b7e-9c1f-2a6d8e0b4c71"}
Parameter names in the examples are illustrative. Yours are whatever we agree in the template.
Delivery and retries
If your endpoint does not answer, or answers 408, 429, or a 5xx, we retry up to 5 times, about
1.5 seconds apart. Any other answer ends delivery, including a 4xx, so answer 2xx quickly and do
the work afterwards. A postback that fails every attempt is not queued for later. The sale is still
credited to you: the postback is a notification, not the record.
Retries mean you can receive the same postback twice. Deduplicate on the order id.
Production postbacks come from 52.228.228.36. We tell your technical contact before that changes.
Security
Pick a shared secret or HMAC at onboarding. Allowlisting our IP is a good addition to either.
| Option | How it works |
|---|---|
| IP allowlist | Accept postbacks from 52.228.228.36 only. |
| Shared secret | We append your secret to the URL as a query parameter named secret, on GET and POST alike. |
| HMAC-SHA256 | We sign {timestamp}.{path and query} with your secret and send the signature as lowercase hex in X-Chaiz-Signature, with the timestamp (Unix seconds) in X-Chaiz-Timestamp. Reject anything older than 300 seconds. |
The HMAC covers the URL path and query string only. On a POST the body is not part of the signature, so put anything you need to trust in the URL, or combine HMAC with the IP allowlist.
You give us the secret through a one-time link, never by email.
Verifying an HMAC signature (Node.js and Express)
const crypto = require("crypto");
function isValidPostback(req, secret) {
const timestamp = req.get("X-Chaiz-Timestamp");
const signature = req.get("X-Chaiz-Signature") ?? "";
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${req.originalUrl}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
return fresh
&& signature.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
Testing
UAT does not send postbacks. To test your endpoint end to end, make one real purchase in production through your own link, right after launch, and check that the postback arrives. Tell us the order and we cancel and refund it.
What the postback does not carry, and where it is
| You want | Where it is |
|---|---|
| Plan, provider, price of the sale | You stored it when you displayed it. Match on your own parameter. |
| Which of your clicks converted | Your click id or other parameters, in the postback |
| Funnel: landings, checkout starts, drop-off | Your Mixpanel dashboard, near real time, filtered to your campaign |
| Contract status, cancellations, payouts | The monthly report. See Going Live |
Checklist
- Your own prefix on every parameter.
- No customer data in parameters.
- The
utm_values exactly as we gave them to you. - Your endpoint answers
2xxfast and does the work afterwards. - Deduplicate on the order id.
- Shared secret or HMAC chosen, the secret exchanged through a one-time link, never by email.
- One real purchase checked right after launch.