Documentation
Conversion postback
Server to server and signed. Your backend tells us about a conversion; the signature proves it was you, and the shared secret never travels over the wire.
The endpoint
Both call forms accept exactly the same parameters.
GET https://api.beet.media/pb/{your_token}?click_id=...&conversion_id=...&signature=...
POST https://api.beet.media/pb/{your_token} (application/x-www-form-urlencoded)The token identifies your account and appears in the path. The shared secret, which is what signs the request, is generated in the dashboard and is never sent. Both come from your account's postback section, and the secret can be rotated without changing the token.
The click identifier
We append it to your landing URL, preserving any query string and fragment it already had.
https://tienda.example/producto?utm_source=beet
-> https://tienda.example/producto?utm_source=beet&click_id=0198f2c1-9a3b-7e21-8c4d-1f2e3a4b5c6dParameters
Three always required, one required except in two dedupe modes, and three optional.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
| click_id | uuid | Yes | The identifier we appended to your landing URL. |
| timestamp | integer | Yes | Unix seconds for the current moment, not for the moment of the conversion. |
| signature | hex | Yes | The lowercase-hex HMAC-SHA256 of the canonical string. |
| conversion_id | string | No | Your own identifier for the conversion. Required except in the per-click and multiple dedupe modes. |
| value_micros | integer | No | The conversion's value in millionths of a currency unit. Integers only: 12.50 is 12500000. |
| currency | string | No | A three-letter uppercase ISO 4217 code. |
| event_name | string | No | The event name, up to 64 characters. For example purchase, signup or install. |
You may send extra parameters. They are signed along with everything else and the receiver ignores them, so you do not need to tell us before adding one.
The signature algorithm
Lowercase-hex HMAC-SHA256 over a canonical string, keyed with the shared secret.
- Take every parameter you are about to send except the signature itself.
- Sort them alphabetically by name.
- Render each one as name equals value, using the decoded value, and join them with ampersands.
- Compute the HMAC-SHA256 of that string with your secret and send the result as lowercase hex.
They are sorted because parameter order is not preserved by every HTTP client and proxy, and a signature that depended on order would fail intermittently and be very hard to diagnose.
The decoded values are used because two encoders can both be correct and still disagree about whether a space is written as a plus or as percent-twenty. Signing the decoded value makes that disagreement impossible.
Worked example
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefclick_id=0198f2c1-9a3b-7e21-8c4d-1f2e3a4b5c6d&conversion_id=order-4711¤cy=MXN&event_name=purchase×tamp=1787836800&value_micros=12500000dcad9bbd0d6114afb9e3cd6be465ef1b4a32fa2589140f46587fde74573ca8daprintf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1Examples
The same algorithm in two languages. Check your implementation against the worked example above before writing anything else.
import hashlib, hmac, time, urllib.parse, urllib.request
SECRET = "your-shared-secret"
TOKEN = "your-token"
params = {
"click_id": click_id,
"conversion_id": order_id,
"timestamp": str(int(time.time())),
"value_micros": "12500000",
"currency": "MXN",
"event_name": "purchase",
}
canonical = "&".join(f"{k}={params[k]}" for k in sorted(params))
params["signature"] = hmac.new(
SECRET.encode(), canonical.encode(), hashlib.sha256
).hexdigest()
url = f"https://api.beet.media/pb/{TOKEN}?" + urllib.parse.urlencode(params)
urllib.request.urlopen(url, timeout=10).read()<?php
$secret = 'your-shared-secret';
$token = 'your-token';
$params = [
'click_id' => $clickId,
'conversion_id' => $orderId,
'timestamp' => (string) time(),
'value_micros' => '12500000',
'currency' => 'MXN',
'event_name' => 'purchase',
];
ksort($params);
$canonical = [];
foreach ($params as $k => $v) { $canonical[] = $k . '=' . $v; }
$params['signature'] = hash_hmac('sha256', implode('&', $canonical), $secret);
$url = 'https://api.beet.media/pb/' . $token . '?' . http_build_query($params);
file_get_contents($url);Responses
Every call is stored as a row you can read in the dashboard. We do not drop postbacks silently.
{
"status": "accepted",
"accepted": true,
"message": "conversion recorded",
"conversion_id": "0198f2c1-0000-7000-8000-00000000d001",
"lag_seconds": 7200
}| Status | Meaning |
|---|---|
| accepted recorded | The conversion was recorded. |
| rejected_signature | The signature did not match, or the timestamp is outside the tolerance. |
| rejected_invalid | One of the parameters does not parse. |
| rejected_unknown_click | There is no click with that identifier for your account, or it is older than the correlation ceiling. |
| rejected_expired | The click is older than your attribution window. |
| rejected_duplicate | We had already recorded this conversion. This is the success case for a retry: nothing is wrong. |
The receiver answers 200 even when it rejects, because a retry loop against a 4xx would not make a rejected conversion valid and would only cost you an on-call rotation. Read the status field in the body, not the HTTP code.
There are two exceptions. A 404 means an unknown token or a deactivated endpoint, and no retry helps. A 500 is us, and that one is worth retrying.
Windows and limits
The three values that decide whether a conversion arrives in time, exactly as configured today.
Default window
168 hours
Default attribution window, configurable between 1 and 720 hours.
Correlation ceiling
30 days
How long a click stays resolvable. No window may exceed it, because a longer window would reject late conversions as unknown clicks while claiming to accept them.
Clock tolerance
5 minutes
How far the timestamp parameter may sit from our clock. Outside it the signature is rejected even when it is correct.
There are three dedupe modes. The default accepts one conversion per identifier you send. The second accepts at most one conversion per click. The third allows several, and only makes sense if you are counting repeat purchases against one click and your delivery is exactly-once, which it is not.
Attribution is click-through only. The view-through window exists in configuration and is unused.