Intégralo en tu app
Firma de solicitudes
#Cuándo la necesitas
Una app sin backend propio tiene que incluir la clave dentro de la app, donde cualquiera que capture su tráfico puede leerla. Activa la firma de solicitudes en esa clave (Consola → Claves de API → Editar → Firma de solicitudes): así cada solicitud debe llevar también una firma hecha con un secreto de firma aparte, de modo que la clave sk- sola no sirve y una solicitud capturada no se puede reproducir.
#Los tres encabezados
Sigue enviando Authorization como antes y añade tres encabezados. El texto firmado es el timestamp, el nonce, el método HTTP y la ruta (sin query string) unidos por saltos de línea; la clave HMAC es el secreto sig_… exactamente como se muestra en la consola.
X-Timestamp: 1759312345 # unix seconds, within 5 minutes of server time
X-Nonce: 9f2c4e7a1b3d5f60 # 16-128 chars of A-Z a-z 0-9 - _, new for every request
X-Signature: hex(HMAC-SHA256(secret, "1759312345\n9f2c4e7a1b3d5f60\nPOST\n/v1/images/tasks"))#Firmar una solicitud
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
const apiKey = 'sk-YOUR_KEY';
const signingSecret = 'sig_YOUR_SIGNING_SECRET';
int clockOffset = 0; // server time - device time, from X-Server-Timestamp
Map<String, String> signedHeaders(String method, String path) {
final ts = (DateTime.now().millisecondsSinceEpoch ~/ 1000 + clockOffset).toString();
final random = Random.secure();
final nonce = List.generate(16, (_) => random.nextInt(256).toRadixString(16).padLeft(2, '0')).join();
final signature = Hmac(sha256, utf8.encode(signingSecret))
.convert(utf8.encode('$ts\n$nonce\n$method\n$path'))
.toString();
return {
'Authorization': 'Bearer $apiKey',
'X-Timestamp': ts,
'X-Nonce': nonce,
'X-Signature': signature,
};
}
// headers: signedHeaders('POST', '/v1/images/tasks')
// GET /v1/images/tasks/{id} is signed with its own path, e.g. '/v1/images/tasks/task_123'import crypto from "node:crypto";
const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString("hex");
const signature = crypto
.createHmac("sha256", process.env.SIGNING_SECRET)
.update(`${ts}\n${nonce}\nPOST\n/v1/images/tasks`)
.digest("hex");
await fetch("https://www.aisplice.store/v1/images/tasks", {
method: "POST",
headers: {
Authorization: "Bearer sk-YOUR_KEY",
"Content-Type": "application/json",
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Signature": signature,
},
body: JSON.stringify(payload),
});#Errores y reglas
- Las solicitudes rechazadas responden 401 con error.code signature_required, signature_expired, signature_invalid o signature_replayed.
- signature_expired también devuelve X-Server-Timestamp. Los teléfonos con el reloj desajustado deben guardar la diferencia, sumarla a cada timestamp y reintentar una vez con un nonce nuevo.
- Los reintentos necesitan un nonce nuevo y una firma nueva; un nonce se recuerda durante 10 minutos.
- Reemplazar el secreto en la consola invalida el anterior en menos de 30 segundos. Desactivar y volver a activar la firma conserva el mismo secreto.
- La firma protege frente al tráfico capturado, no frente a quien desempaqueta la app. Ofusca la compilación (Flutter: --obfuscate) y mantén un tope de cuota en la clave.
¿Sigues con el problema?
Envíanos el ID de solicitud o el mensaje de error y lo revisaremos.