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