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-API-Key dw_<key>

Crea una API key desde el panel (Perfil → API keys). Envíala en cada petición con la cabecera X-API-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

Hasta 20 ficheros por lote. Requiere una cuenta autenticada con el correo verificado.

Analizar un lote son dos llamadas, no una: /upload guarda los ficheros y devuelve un upload_id por cada uno, y /analyze es la que encola los análisis de verdad.

POST /api/batch/upload

Sube varios APK 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:

{
  "uploaded": [
    {"upload_id": "a1b2c3d4", "filename": "app1.apk", "size": 4194304},
    {"upload_id": "e5f6a7b8", "filename": "app2.apk", "size": 8388608}
  ],
  "errors": [],
  "total_uploaded": 2,
  "total_errors": 0
}

Un fichero que el servidor rechace (extensión no admitida, por encima del límite de tamaño del plan) aparece en errors y no interrumpe el resto del lote.

POST /api/batch/analyze

Encola un análisis por cada subida. Esta es la llamada que devuelve el batch_id.

curl -X POST https://droidwatch.app/api/batch/analyze \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"uploads": [
        {"upload_id": "a1b2c3d4", "filename": "app1.apk"},
        {"upload_id": "e5f6a7b8", "filename": "app2.apk"}
      ]}'

Respuesta:

{
  "batch_id": "7f3a2b1c-...",
  "started": [
    {"job_id": "...", "run_id": "...", "upload_id": "a1b2c3d4",
     "filename": "app1.apk", "status": "queued"}
  ],
  "errors": [],
  "total_started": 2,
  "total_errors": 0
}

GET /api/batch/{batch_id}

Consulta el progreso de todos los análisis del lote.

curl "https://droidwatch.app/api/batch/7f3a2b1c-..." \
  -H "Authorization: Bearer $TOKEN"

Respuesta:

{
  "batch_id": "7f3a2b1c-...",
  "overall_status": "running",
  "progress_pct": 33,
  "counts": {"queued": 1, "running": 1, "done": 1, "failed": 0},
  "jobs": [{"job_id": "...", "run_id": "...", "status": "done", "progress": 100}]
}

Los lotes caducan 24 horas después de crearse; a partir de ahí esto devuelve 404.


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"])