Autenticación y firma
Cada petición lleva tres cabeceras:
| Cabecera | Valor |
|---|---|
Authorization |
Bearer rvw_live_…: la llave. Dice quién llama. |
X-Timestamp |
La hora actual en segundos Unix (1760630400). |
X-Signature |
sha256= + el HMAC-SHA256 en hexadecimal, con el secreto de firma, de la cadena {X-Timestamp}.{body crudo}. |
Cómo se arma la firma
Sección titulada «Cómo se arma la firma»cadena = X-Timestamp + "." + bodyfirma = "sha256=" + hex( HMAC_SHA256( secreto, cadena ) )- El body es el texto exacto que mandas, byte por byte. Si firmas un JSON y luego lo vuelves a serializar con otro espacio, la firma no coincide.
- En un
GETo unPOSTsin body, el body es la cadena vacía: firmas1760630400.. - El timestamp va dentro de la firma y aceptamos hasta 5 minutos de diferencia con nuestro reloj. Así nadie puede reenviar una petición capturada.
LLAVE="rvw_live_..."SECRETO="rvwsig_..."TS=$(date +%s)BODY='{"order":{"shopify_order_id":"456"},"locale":"es"}'FIRMA=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRETO" | sed 's/^.* //')
curl https://reviews.leadit.tech/api/v1/review-requests \ -H "Authorization: Bearer $LLAVE" \ -H "X-Timestamp: $TS" \ -H "X-Signature: sha256=$FIRMA" \ -H "Content-Type: application/json" \ -d "$BODY"import { createHmac } from "node:crypto";
const BASE = "https://reviews.leadit.tech/api/v1";
export async function leadit(metodo, ruta, datos) { const body = datos === undefined ? "" : JSON.stringify(datos); const ts = Math.floor(Date.now() / 1000).toString(); const firma = createHmac("sha256", process.env.LEADIT_SECRETO).update(`${ts}.${body}`).digest("hex");
const respuesta = await fetch(BASE + ruta, { method: metodo, headers: { Authorization: `Bearer ${process.env.LEADIT_LLAVE}`, "X-Timestamp": ts, "X-Signature": `sha256=${firma}`, "Content-Type": "application/json", }, body: body || undefined, }); return respuesta.json();}
// const solicitud = 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(metodo, ruta, datos=None): body = "" if datos is None else json.dumps(datos, separators=(",", ":")) ts = str(int(time.time())) firma = hmac.new(os.environ["LEADIT_SECRETO"].encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest() respuesta = requests.request(metodo, BASE + ruta, data=body.encode() or None, headers={ "Authorization": f"Bearer {os.environ['LEADIT_LLAVE']}", "X-Timestamp": ts, "X-Signature": f"sha256={firma}", "Content-Type": "application/json", }) return respuesta.json()<?phpfunction leadit(string $metodo, string $ruta, ?array $datos = null): array { $body = $datos === null ? '' : json_encode($datos); $ts = (string) time(); $firma = hash_hmac('sha256', "$ts.$body", getenv('LEADIT_SECRETO'));
$ch = curl_init('https://reviews.leadit.tech/api/v1' . $ruta); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $metodo, CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => $body === '' ? null : $body, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('LEADIT_LLAVE'), "X-Timestamp: $ts", "X-Signature: sha256=$firma", 'Content-Type: application/json', ], ]); return json_decode(curl_exec($ch), true);}Permisos de cada llave
Sección titulada «Permisos de cada llave»Cada llave tiene un alcance que eliges al crearla y no cambia después. Para cambiarlo, crea otra y revoca la vieja. Puedes tener hasta 5 llaves.
| Alcance | En la app | Qué puede hacer |
|---|---|---|
read |
Solo lectura | Todos los GET. |
moderate |
Solicitudes y moderación | Lo anterior, más pedir, confirmar, enviar ahora y cancelar solicitudes; crear, moderar y responder reseñas; subir fotos y videos. |
full |
Acceso total | Todo: también borrar reseñas, cambiar ajustes, reintentar webhooks e importar en lote. |
Una llamada fuera del alcance responde 403 insufficient_scope, con el alcance que haría falta en details.
Idempotencia
Sección titulada «Idempotencia»POST /reviews y POST /reviews/import aceptan la cabecera Idempotency-Key (un UUID, por ejemplo). Si repites la misma petición con la misma llave dentro de 24 horas, te devolvemos la misma respuesta con la cabecera Idempotent-Replayed: true, sin crear nada otra vez.
- La misma
Idempotency-Keycon otro body responde422 idempotency_key_reused. - Mientras la primera sigue en curso, la segunda responde
409 idempotency_in_progress. - Un error 5xx no se guarda: puedes reintentar.
POST /review-requests no necesita la cabecera: si la orden ya tiene solicitud, devuelve la misma con 200.
Límite de peticiones
Sección titulada «Límite de peticiones»120 peticiones por minuto por llave. Si te pasas, respondemos 429 rate_limited con Retry-After: 60.