Descripción general
Los análisis de URL terminan en la solicitud; los de APK son asíncronos. Todos los endpoints comparten la ventana de frecuencia de la cuenta.
Enviar la solicitud o cargar el APK
Validar, deduplicar y analizar
Entregar el resultado por webhook firmado
La aceptación incluye job_id. Consulta GET /api/v1/jobs/{job_id} durante unas 24 horas. callback_url es opcional; si se indica, también se envía un webhook firmado.
{
"client_reference": "order-20260711-001",
"callback_url": "https://example.com/webhooks/fvl",
"use_cache": false
}Autenticación
Usa una de las cabeceras en cada solicitud. La clave solo se muestra completa al crearla y debe permanecer en el servidor.
Authorization: Bearer fvl_xxxX-API-Key: fvl_xxxNo incluyas claves API en navegadores, apps móviles, repositorios públicos ni registros.
Cuotas y frecuencia
La API está disponible para VIP+, SVIP, SVIP+ y administradores. Todas las claves de una cuenta comparten la frecuencia.
| Plan | Intervalo mínimo | Carga R2 diaria |
|---|---|---|
| VIP+ | 10 minutos | 2 GiB |
| Pro | 3 minutos | 5 GiB |
| SVIP | 5 minutos | 10 GiB |
| SVIP+ | 1 minuto | 30 GiB |
| Administrador | Sin límite | Sin límite |
Endpoints
Solicitudes y respuestas usan JSON salvo el PUT prefirmado de R2.
/api/v1/url-scanAnalizar una URL de forma síncronaEnvía url y recibe el riesgo y la quota posterior al cargo en la misma respuesta. No usa webhook.
curl -X POST https://www.fuckviruslabel.com/api/v1/url-scan \
-H "Authorization: Bearer fvl_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/path"}'{
"code": 0,
"message": "success",
"data": {
"result": {
"status": "completed",
"url": "https://example.com/path",
"domain": "example.com",
"flagged": false,
"mainType": 0,
"mainTypeName": "安全",
"provider": "OPPO",
"websiteType": 0,
"matchUrl": "",
"riskDetail": "",
"cacheStatus": "live",
"checkedAt": 1783765471000,
"createdAt": 1783765471000
},
"quota": {
"identity_label": "VIP+",
"daily_limit": 20,
"daily_used": 1,
"daily_remaining": 19,
"scan_credits": 0,
"total_remaining": 19,
"usage_date": "2026-07-11"
}
}
}/api/v1/apk/uploadsCrear una sesión de carga APKRequiere Idempotency-Key, nombre .apk, size, file_md5, expected_sha256 y campos de callback. La URL dura 30 minutos.
curl -X POST https://www.fuckviruslabel.com/api/v1/apk/uploads \
-H "Authorization: Bearer fvl_xxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: upload-20260711-001" \
-H "Content-Type: application/json" \
-d '{
"client_reference": "order-20260711-001",
"callback_url": "https://api.example.com/webhooks/fvl",
"use_cache": false,
"filename": "release.apk",
"size": 24117384,
"file_md5": "0123456789abcdef0123456789abcdef",
"expected_sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}'curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/vnd.android.package-archive" \
-H "Content-Length: 24117384" \
-H "Content-MD5: ASNFZ4mrze8BI0VniavN7w==" \
-H "If-None-Match: *" \
--upload-file ./release.apk/api/v1/apk/uploads/completeCompletar la carga y enviar a revisiónEnvía upload_token e Idempotency-Key. Se valida el objeto y se envía la revisión prepagada sin otro cargo.
curl -X POST https://www.fuckviruslabel.com/api/v1/apk/uploads/complete \
-H "Authorization: Bearer fvl_xxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: order-20260711-001" \
-H "Content-Type: application/json" \
-d '{"upload_token":"upl_xxxxxxxxxxxxxxxx"}'/api/v1/apk/fingerprintsEnviar huellas APKEnvía package_name, sha256, file_md5, cert_md5 y los campos de callback sin volver a cargar el APK.
curl -X POST https://www.fuckviruslabel.com/api/v1/apk/fingerprints \
-H "Authorization: Bearer fvl_xxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: fingerprint-20260711-001" \
-H "Content-Type: application/json" \
-d '{
"client_reference": "fingerprint-20260711-001",
"callback_url": "https://api.example.com/webhooks/fvl",
"use_cache": true,
"package_name": "com.example.app",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"file_md5": "0123456789abcdef0123456789abcdef",
"cert_md5": "abcdef0123456789abcdef0123456789",
"version_code": 108,
"version_name": "1.0.8",
"size": 24117384,
"filename": "release.apk"
}'/api/v1/packages/checkComprobar un paqueteComprueba rápidamente el riesgo por paquete. No es un análisis APK completo con hashes y certificado.
curl -X POST https://www.fuckviruslabel.com/api/v1/packages/check \
-H "X-API-Key: fvl_xxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: package-20260711-001" \
-H "Content-Type: application/json" \
-d '{
"client_reference": "package-20260711-001",
"callback_url": "https://api.example.com/webhooks/fvl",
"use_cache": false,
"package_name": "com.example.app"
}'{
"code": 0,
"message": "accepted",
"data": {
"client_reference": "order-20260711-001",
"job_id": "acd_xxxxxxxxxxxxxxxx",
"delivery": "webhook_and_poll",
"poll_path": "/api/v1/jobs/acd_xxxxxxxxxxxxxxxx"
}
}/api/v1/jobs/{job_id}Consultar un trabajo asíncronoUsa el job_id de la aceptación para leer pending / processing / completed / failed. Los resultados se conservan unas 24 horas.
curl -X POST https://www.fuckviruslabel.com/api/v1/packages/check \
-H "X-API-Key: fvl_xxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: package-20260711-001" \
-H "Content-Type: application/json" \
-d '{
"client_reference": "package-20260711-001",
"callback_url": "https://api.example.com/webhooks/fvl",
"use_cache": false,
"package_name": "com.example.app"
}'{
"code": 0,
"message": "accepted",
"data": {
"client_reference": "order-20260711-001",
"job_id": "acd_xxxxxxxxxxxxxxxx",
"delivery": "webhook_and_poll",
"poll_path": "/api/v1/jobs/acd_xxxxxxxxxxxxxxxx"
}
}Cargo antes de emitir; sin reembolso después
Se validan membresía, frecuencia, cuota, bytes diarios y sesiones antes de descontar un reanálisis y emitir la URL. No se reembolsa por cargas incompletas, caducadas, hashes incorrectos, APK no válidos ni fallos de cola. Los fallos transitorios conservan la muestra para reintento automático.
Los APK válidos se deduplican por SHA-256 y archivan; las muestras no válidas o incoherentes van a quarantine. Se conservan 90 días y las cargas incompletas se eliminan al día.
Semántica de use_cache
use_cache es un campo raíz y vale false por defecto. R2 se prepaga antes de emitir la URL sin importar el valor; solo controla la reutilización posterior.
falseForzar reanálisisfalse omite resultados existentes y consume un reanálisis cuando empieza el análisis real.
cache_status = bypasstruePermitir cachétrue devuelve una caché válida sin cargo; si falta o caducó, reanaliza.
cache_status = hit | missWebhook firmado
El webhook es el único canal para APK asíncronos. El servidor envía JSON original, marca de tiempo y firma HMAC-SHA256.
X-FVL-Timestamp: 1720000000X-FVL-Signature: sha256=<hex>- Calcula webhook_secret = lowercase_hex(SHA-256(raw_api_key)).
- Firma X-FVL-Timestamp + "." + raw_body.
- Usa los bytes UTF-8 de webhook_secret para HMAC-SHA256 y compara en tiempo constante.
- Valida la hora y devuelve cualquier 2xx; otros estados se reintentan.
{
"event": "apk.scan.completed",
"client_reference": "order-20260711-001",
"operation": "apk_upload",
"status": "completed",
"cache_status": "bypass",
"result": { "flagged": false, "result_type_name": "安全" },
"quota": { "refresh_daily_remaining": 29, "total_remaining": 29 },
"checked_at": "2026-07-11T10:24:31.000Z"
}import crypto from "node:crypto";
export function verifyFvlWebhook(rawBody, timestamp, signature, apiKey) {
const webhookSecret = crypto
.createHash("sha256")
.update(apiKey, "utf8")
.digest("hex");
const expected = "sha256=" + crypto
.createHmac("sha256", webhookSecret)
.update(timestamp + "." + rawBody)
.digest("hex");
const expectedBytes = Buffer.from(expected, "utf8");
const signatureBytes = Buffer.from(signature, "utf8");
return expectedBytes.length === signatureBytes.length &&
crypto.timingSafeEqual(expectedBytes, signatureBytes);
}Lee raw body antes de interpretar JSON. Volver a serializar cambia los bytes y rompe la firma.
Idempotencia y deduplicación
Crear y completar cargas, huellas y paquetes requieren Idempotency-Key.
Errores
Los errores síncronos usan estado HTTP y JSON uniforme. Los fallos APK asíncronos llegan por scan.failed.
| HTTP | Significado | Acción |
|---|---|---|
400 | Solicitud o campo no válido | Corrige JSON, hashes, paquete o callback. |
401 | Autenticación fallida | Comprueba la clave API. |
403 | Acceso denegado | Comprueba plan y vencimiento. |
409 | Conflicto de estado | Comprueba idempotencia o estado de carga. |
429 | Cuota o frecuencia excedida | Respeta Retry-After. |
503 | Servicio no disponible | Reintenta según el endpoint con la misma clave. |
const body = {
client_reference: "package-20260711-001",
callback_url: "https://api.example.com/webhooks/fvl",
use_cache: true,
package_name: "com.example.app"
};
const response = await fetch(
"https://www.fuckviruslabel.com/api/v1/packages/check",
{
method: "POST",
headers: {
Authorization: "Bearer " + process.env.FVL_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": body.client_reference
},
body: JSON.stringify(body)
}
);
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import os
import requests
payload = {
"client_reference": "package-20260711-001",
"callback_url": "https://api.example.com/webhooks/fvl",
"use_cache": True,
"package_name": "com.example.app",
}
response = requests.post(
"https://www.fuckviruslabel.com/api/v1/packages/check",
headers={
"Authorization": f"Bearer {os.environ['FVL_API_KEY']}",
"Idempotency-Key": payload["client_reference"],
},
json=payload,
timeout=20,
)
response.raise_for_status()
print(response.json())Registro de cambios
Versión inicial: URL, R2, huellas APK, paquetes, caché, idempotencia y webhooks firmados.