Authentication and signing
Each request carries three headers:
| Header | Value |
|---|---|
Authorization |
Bearer rvw_live_…: the key. It says who’s calling. |
X-Timestamp |
The current time in Unix seconds (1760630400). |
X-Signature |
sha256= + the HMAC-SHA256 in hex, with the signing secret, of the string {X-Timestamp}.{raw body}. |
How the signature is built
Section titled “How the signature is built”string = X-Timestamp + "." + bodysignature = "sha256=" + hex( HMAC_SHA256( secret, string ) )- The body is the exact text you send, byte for byte. If you sign a JSON and then serialize it again with different spacing, the signature won’t match.
- On a
GETor aPOSTwithout a body, the body is the empty string: you sign1760630400.. - The timestamp is part of the signature and we accept up to 5 minutes of difference from our clock. That way nobody can resend a captured request.
KEY="rvw_live_..."SECRET="rvwsig_..."TS=$(date +%s)BODY='{"order":{"shopify_order_id":"456"},"locale":"en"}'SIGNATURE=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl https://reviews.leadit.tech/api/v1/review-requests \ -H "Authorization: Bearer $KEY" \ -H "X-Timestamp: $TS" \ -H "X-Signature: sha256=$SIGNATURE" \ -H "Content-Type: application/json" \ -d "$BODY"import { createHmac } from "node:crypto";
const BASE = "https://reviews.leadit.tech/api/v1";
export async function leadit(method, path, data) { const body = data === undefined ? "" : JSON.stringify(data); const ts = Math.floor(Date.now() / 1000).toString(); const signature = createHmac("sha256", process.env.LEADIT_SECRET).update(`${ts}.${body}`).digest("hex");
const response = await fetch(BASE + path, { method, headers: { Authorization: `Bearer ${process.env.LEADIT_KEY}`, "X-Timestamp": ts, "X-Signature": `sha256=${signature}`, "Content-Type": "application/json", }, body: body || undefined, }); return response.json();}
// const request = await leadit("POST", "/review-requests", { order: { shopify_order_id: "456" } });import hashlib, hmac, json, os, timeimport requests
BASE = "https://reviews.leadit.tech/api/v1"
def leadit(method, path, data=None): body = "" if data is None else json.dumps(data, separators=(",", ":")) ts = str(int(time.time())) signature = hmac.new(os.environ["LEADIT_SECRET"].encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest() response = requests.request(method, BASE + path, data=body.encode() or None, headers={ "Authorization": f"Bearer {os.environ['LEADIT_KEY']}", "X-Timestamp": ts, "X-Signature": f"sha256={signature}", "Content-Type": "application/json", }) return response.json()<?phpfunction leadit(string $method, string $path, ?array $data = null): array { $body = $data === null ? '' : json_encode($data); $ts = (string) time(); $signature = hash_hmac('sha256', "$ts.$body", getenv('LEADIT_SECRET'));
$ch = curl_init('https://reviews.leadit.tech/api/v1' . $path); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => $body === '' ? null : $body, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('LEADIT_KEY'), "X-Timestamp: $ts", "X-Signature: sha256=$signature", 'Content-Type: application/json', ], ]); return json_decode(curl_exec($ch), true);}Each key’s permissions
Section titled “Each key’s permissions”Each key has a scope you choose when you create it, and it doesn’t change afterwards. To change it, create another one and revoke the old one. You can have up to 5 keys.
| Scope | In the app | What it can do |
|---|---|---|
read |
Read only | All GETs. |
moderate |
Requests and moderation | The above, plus getting, confirming, sending now and cancelling requests; creating, moderating and replying to reviews; uploading photos and videos. |
full |
Full access | Everything: also deleting reviews, changing settings, retrying webhooks and bulk import. |
A call outside the scope returns 403 insufficient_scope, with the scope it would need in details.
Idempotency
Section titled “Idempotency”POST /reviews and POST /reviews/import accept the Idempotency-Key header (a UUID, for example). If you repeat the same request with the same key within 24 hours, we return the same response with the Idempotent-Replayed: true header, without creating anything again.
- The same
Idempotency-Keywith a different body returns422 idempotency_key_reused. - While the first one is still in progress, the second returns
409 idempotency_in_progress. - A 5xx error isn’t stored: you can retry.
POST /review-requests doesn’t need the header: if the order already has a request, it returns the same one with 200.
Rate limit
Section titled “Rate limit”120 requests per minute per key. If you go over, we return 429 rate_limited with Retry-After: 60.