Referencia de la API de Droidwatch
URL base: https://droidwatch.app
Todos los endpoints devuelven JSON. Los errores siguen el formato:
{ "detail": "Human-readable error message", "request_id": "rid-abc123" }
Autenticación
Droidwatch admite dos tipos de credenciales:
| Tipo | Cabecera | Valor |
|---|---|---|
| JWT Bearer | Authorization |
Bearer <jwt_token> |
| API key | X-Auth-Key |
dw_<key> |
Crea una API key desde el panel (Perfil → API keys). Envíala en cada petición
con la cabecera X-Auth-Key. Las peticiones anónimas se permiten en un conjunto
limitado de endpoints (las subidas tienen rate-limit por IP).
POST /api/auth/login
Obtén un token de acceso JWT.
curl -X POST https://droidwatch.app/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "analyst",
"password": "your-password"
}'
Respuesta:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 28800,
"username": "analyst",
"role": "user",
"plan": "pro",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
El token expira tras expires_in segundos (por defecto 8 horas). Usa el
refresh_token con POST /api/auth/refresh-token para obtener un nuevo access token
sin volver a introducir credenciales.
Patrón de petición autenticada:
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
curl -H "Authorization: Bearer $TOKEN" https://droidwatch.app/api/runs
Subida y análisis
POST /api/upload
Sube un archivo APK para analizar. Devuelve un upload_id usado en las llamadas
siguientes. Las subidas anónimas se permiten (rate-limit por IP).
curl -X POST https://droidwatch.app/api/upload \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/path/to/app.apk"
Respuesta:
{
"upload_id": "a3f8e2d1-7b4c-4e9a-8f1d-2c5a6b3d9e0f",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"file_size": 4194304,
"file_type": "apk",
"message": "Upload accepted."
}
Límites por plan:
| Plan | Tamaño máx. de archivo | Análisis/día |
|---|---|---|
| Anónimo | 150 MB | 3/hora |
| Free | 200 MB | 5/día |
| Pro | 500 MB | 100/día |
| Team | 750 MB | 500/día |
| Enterprise | 1 GB | Ilimitado |
POST /api/analyze
Inicia el análisis de un archivo subido.
curl -X POST https://droidwatch.app/api/analyze \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"upload_id": "a3f8e2d1-7b4c-4e9a-8f1d-2c5a6b3d9e0f", "filename": "app.apk"}'
GET /api/jobs/{job_id}
Consulta el estado del análisis. Devuelve de inmediato el progreso actual.
curl "https://droidwatch.app/api/jobs/job_91a2…" \
-H "Authorization: Bearer $TOKEN"
Respuesta:
{
"run_id": "a3f8e2d1-7b4c-4e9a-8f1d-2c5a6b3d9e0f",
"status": "running",
"stage": "dex_analysis",
"progress": 45,
"started_at": "2026-05-16T10:23:01Z",
"estimated_remaining_sec": 12
}
Valores de status: queued | running | done | failed
Reportes
GET /api/runs/{run_id}/artifact/report.json
Obtén el reporte completo de análisis de un run terminado.
curl "https://droidwatch.app/api/runs/a3f8e2d1-7b4c-4e9a-8f1d-2c5a6b3d9e0f/artifact/report.json" \
-H "Authorization: Bearer $TOKEN" | python -m json.tool
Esquema de respuesta (nivel superior):
{
"metadata": {
"run_id": "a3f8e2d1-...",
"app_name": "My Banking App",
"package": "com.example.banking",
"version_name": "3.2.1",
"version_code": 421,
"sha256": "e3b0c44298fc1c...",
"file_size": 4194304,
"min_sdk": 24,
"target_sdk": 34,
"analyzed_at": "2026-05-16T10:23:45Z"
},
"overview": {
"score": 63,
"verdict": "High Risk",
"severities": { "critical": 1, "high": 3, "medium": 7, "low": 4, "info": 12 },
"attack_tactics": ["TA0006", "TA0009"],
"masvs_score": 42
},
"sections": [
{
"id": "permissions",
"title": "Permissions",
"findings": [...]
}
],
"mitre_attack": [...],
"network_indicators": [...],
"certificates": {...},
"yara_matches": [...]
}
GET /api/runs/{run_id}/report/pdf
Exporta como un resumen ejecutivo en PDF (plan Pro o superior).
curl "https://droidwatch.app/api/runs/$RUN_ID/report/pdf" \
-H "Authorization: Bearer $TOKEN" \
-o report.pdf
GET /api/runs/{run_id}/report/stix
Exporta los hallazgos como un bundle STIX 2.1 para ingesta en SIEM/SOAR.
curl "https://droidwatch.app/api/runs/$RUN_ID/report/stix" \
-H "Authorization: Bearer $TOKEN" | python -m json.tool
Threat Feed
El threat feed es público — no requiere autenticación.
GET /api/threat-feed
Lista paginada de muestras públicas recientes maliciosas/sospechosas.
curl "https://droidwatch.app/api/threat-feed?limit=20&verdict=Malicious"
Parámetros de query:
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
limit |
int | 50 | Resultados por página (máx. 500) |
offset |
int | 0 | Offset de paginación |
verdict |
string | — | Filtro: Malicious, High Risk, Suspicious |
format |
string | json |
Formato de respuesta: json, jsonl, csv, stix |
Streaming JSONL (para exports grandes):
curl "https://droidwatch.app/api/threat-feed?format=jsonl&limit=500" \
> feed.jsonl
Bundle STIX 2.1:
curl "https://droidwatch.app/api/threat-feed?format=stix" \
| python -m json.tool
GET /api/threat-feed/lookup
Búsqueda por hash, IP, dominio o nombre de paquete.
# Búsqueda por SHA-256
curl "https://droidwatch.app/api/threat-feed/lookup?q=e3b0c44298fc1c149afbf4c8996fb924"
# Búsqueda por nombre de paquete
curl "https://droidwatch.app/api/threat-feed/lookup?q=com.example.suspiciousapp"
# Búsqueda por dominio
curl "https://droidwatch.app/api/threat-feed/lookup?q=evil-c2.example.com"
Respuesta:
{
"query": "com.example.suspiciousapp",
"type": "package",
"matches": [
{
"run_id": "a3f8e2d1-...",
"sha256": "e3b0c44...",
"verdict": "Malicious",
"score": 87,
"created_at": "2026-05-10T08:14:22Z",
"share_url": "/r/a3f8e2d1-..."
}
]
}
App Explorer
Índice público de apps — no requiere autenticación.
GET /api/explore
Lista paginada de paquetes conocidos con estadísticas agregadas de análisis.
curl "https://droidwatch.app/api/explore?limit=20&offset=0"
Parámetros de query:
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
limit |
int | 20 | Resultados por página |
offset |
int | 0 | Offset de paginación |
verdict |
string | — | Filtrar por veredicto |
sort |
string | latest |
Orden: latest, score, name |
Respuesta:
{
"total": 1482,
"items": [
{
"package": "com.example.banking",
"app_name": "Fake Banking App",
"latest_verdict": "Malicious",
"latest_score": 87,
"run_count": 3,
"last_seen": "2026-05-15T14:22:11Z"
}
]
}
GET /api/explore/search
Búsqueda por prefijo/subcadena por nombre de paquete o de app.
curl "https://droidwatch.app/api/explore/search?q=banking&limit=10"
Parámetros de query:
| Parámetro | Tipo | Descripción |
|---|---|---|
q |
string | Término de búsqueda (nombre de paquete o de app) |
limit |
int | Máx. de resultados (por defecto 20, máx. 100) |
Análisis por lotes
POST /api/batch/upload
Sube varios APKs en una sola petición.
curl -X POST https://droidwatch.app/api/batch/upload \
-H "Authorization: Bearer $TOKEN" \
-F "[email protected]" \
-F "[email protected]" \
-F "[email protected]"
Respuesta:
{
"batch_id": "batch-7f3a2b1c",
"run_ids": ["run-001", "run-002", "run-003"],
"status": "queued"
}
GET /api/jobs?batch_id={batch_id}
Consulta el progreso de todos los jobs de un lote.
curl "https://droidwatch.app/api/jobs?batch_id=batch-7f3a2b1c" \
-H "Authorization: Bearer $TOKEN"
Códigos de error
| Estado HTTP | Significado |
|---|---|
| 400 | Petición inválida — parámetros o body incorrectos |
| 401 | No autenticado — token ausente o expirado |
| 403 | Prohibido — rol o plan insuficiente |
| 404 | Recurso no encontrado |
| 413 | El APK supera el límite de tamaño de tu plan |
| 429 | Límite de tasa excedido |
| 500 | Error interno del servidor — incluye request_id al reportar |
SDK / ejemplos de código
Python
import requests
BASE = "https://droidwatch.app"
TOKEN = "eyJhbGci..."
session = requests.Session()
session.headers["Authorization"] = f"Bearer {TOKEN}"
# Upload
with open("suspicious.apk", "rb") as f:
r = session.post(f"{BASE}/api/upload", files={"file": f})
upload_id = r.json()["upload_id"]
# Trigger analysis
job = session.post(f"{BASE}/api/analyze",
json={"upload_id": upload_id, "filename": "suspicious.apk"}).json()
# Poll
import time
while True:
status = session.get(f"{BASE}/api/jobs/{job['job_id']}").json()
if status["status"] in ("done", "failed"):
break
time.sleep(3)
# Fetch report
report = session.get(f"{BASE}/api/runs/{job['run_id']}/artifact/report.json").json()
print(report["overview"]["verdict"], report["overview"]["score"])