Webhooks (1.4.0)
REVER webhooks provide real-time notifications for key events in the return process. They allow businesses to automate responses throughout the return lifecycle.
By subscribing, you receive instant updates when return statuses change — including creation, shipping updates, completion, and cancellations.
Enable webhooks from your REVER dashboard, under Configuration → Webhooks, where you generate the shared secret used for signature verification. The secret belongs to your account and you can view it there at any time. The page is available to administrators of your account; if you do not see it, ask your account manager. To register or change the destination URL, contact your account manager as well.
Replacing the secret takes effect on the next webhook we send, so update your endpoint before you replace it. Deleting it turns signing off: deliveries continue, without the signature header.
Webhooks are sent to public endpoints — your server is responsible for validating that each request genuinely comes from REVER.
Every webhook request includes an X-REVER-Signature header, so you can confirm the request came from REVER and has not been replayed:
X-REVER-Signature: t=1735689600,v1=2ff43fd45fd71088d4a6e835b06814bf6bca295753e17cbcf8ed0e0f64932c00The header has two components:
| Component | Meaning |
|---|---|
t | Unix timestamp, in seconds, of the moment we signed the request |
v1 | Hex-encoded HMAC-SHA256 signature |
The signature covers the timestamp and the body, joined by a single .:
signed_payload = t + "." + raw_request_body
v1 = hex( HMAC-SHA256( signed_payload, shared_secret ) )Signing the timestamp along with the body is what lets you reject a replayed request by age: an attacker replaying a captured request cannot move t forward without invalidating the signature.
- Read the
tandv1components from the header. Reject the request if either is missing or malformed — a well-formed header always carries both,v1being 64 hex characters. - Reject the request if
tis older than your tolerance — five minutes is a reasonable default. - Take the raw request body, exactly as received. Verify against it before any JSON parsing: re-serializing a parsed object changes key order or whitespace, and the signature will no longer match even though the payload is genuine.
- Compute
HMAC-SHA256(t + "." + raw_body, shared_secret)and hex-encode it. - Compare with
v1using a constant-time comparison.
import hashlib
import hmac
import time
def verify(header: str, raw_body: bytes, secret: bytes, max_age_seconds: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
if not parts.get("t", "").isdigit() or "v1" not in parts:
return False # malformed header: it did not come from us
if abs(time.time() - int(parts["t"])) > max_age_seconds:
return False # too old: treat as a replay
signed_payload = f'{parts["t"]}.'.encode() + raw_body
expected = hmac.new(secret, signed_payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), parts["v1"].encode())const crypto = require('crypto')
function verify(header, rawBody, secret, maxAgeSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)))
if (!/^\d+$/.test(parts.t ?? '') || !/^[0-9a-f]{64}$/.test(parts.v1 ?? '')) {
return false // malformed header: it did not come from us
}
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > maxAgeSeconds) {
return false // too old: treat as a replay
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.`)
.update(rawBody)
.digest('hex')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}You can check your implementation against this vector, which produces the header shown above:
| Secret | whsec_EXAMPLEexampleEXAMPLEexampleEXAMPLEexample |
| Body | {"event_name":"onReturnProcessCompleted","process_id":"retp_2NkQ"} |
t | 1735689600 |
v1 | 2ff43fd45fd71088d4a6e835b06814bf6bca295753e17cbcf8ed0e0f64932c00 |
A webhook we retry is signed again, with the timestamp of the retry — not the original. A retry is therefore a valid, fresh request rather than something your replay window should reject.