Droidwatch API Reference

Base URL: https://droidwatch.app

All endpoints return JSON. Errors follow the format:

{ "detail": "Human-readable error message", "request_id": "rid-abc123" }

Authentication

Droidwatch supports two credential types:

Type Header Value
JWT Bearer Authorization Bearer <jwt_token>
API key X-Auth-Key dw_<key>

Create an API key from the dashboard (Profile → API keys). Send it on every
request as the X-Auth-Key header. Anonymous requests are allowed on a limited set
of endpoints (uploads are rate-limited by IP).

POST /api/auth/login

Obtain a JWT access token.

curl -X POST https://droidwatch.app/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "analyst",
    "password": "your-password"
  }'

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "expires_in": 28800,
  "username": "analyst",
  "role": "user",
  "plan": "pro",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

The token expires after expires_in seconds (default 8 hours). Use the
refresh_token with POST /api/auth/refresh-token to get a new access token
without re-entering credentials.

Authenticated request pattern:

TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
curl -H "Authorization: Bearer $TOKEN" https://droidwatch.app/api/runs

Upload and Analysis

POST /api/upload

Upload an APK file for analysis. Returns a run_id used in all subsequent calls.
Anonymous uploads are allowed (rate-limited by IP).

curl -X POST https://droidwatch.app/api/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@/path/to/app.apk"

Response:

{
  "run_id": "a3f8e2d1-7b4c-4e9a-8f1d-2c5a6b3d9e0f",
  "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "file_size": 4194304,
  "status": "uploaded",
  "message": "Upload accepted."
}

Limits by plan:

Plan Max file size Analyses/day
Anonymous 150 MB 3/hour
Free 200 MB 5/day
Pro 500 MB 100/day
Team 750 MB 500/day
Enterprise 1 GB Unlimited

POST /api/analyze

Start analysis for an uploaded file.

curl -X POST https://droidwatch.app/api/analyze \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"run_id": "a3f8e2d1-7b4c-4e9a-8f1d-2c5a6b3d9e0f"}'

GET /api/jobs/{run_id}

Poll analysis status. Returns immediately with current progress.

curl "https://droidwatch.app/api/jobs/a3f8e2d1-7b4c-4e9a-8f1d-2c5a6b3d9e0f" \
  -H "Authorization: Bearer $TOKEN"

Response:

{
  "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
}

status values: queued | running | completed | failed


Reports

GET /api/runs/{run_id}/report

Fetch the full analysis report for a completed run.

curl "https://droidwatch.app/api/runs/a3f8e2d1-7b4c-4e9a-8f1d-2c5a6b3d9e0f/report" \
  -H "Authorization: Bearer $TOKEN" | python -m json.tool

Response schema (top level):

{
  "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

Export as a PDF executive summary (Pro plan and above).

curl "https://droidwatch.app/api/runs/$RUN_ID/report/pdf" \
  -H "Authorization: Bearer $TOKEN" \
  -o report.pdf

GET /api/runs/{run_id}/report/stix

Export findings as a STIX 2.1 bundle for SIEM/SOAR ingestion.

curl "https://droidwatch.app/api/runs/$RUN_ID/report/stix" \
  -H "Authorization: Bearer $TOKEN" | python -m json.tool

Threat Feed

The threat feed is public — no authentication required.

GET /api/threat-feed

Paginated list of recent malicious/suspicious public samples.

curl "https://droidwatch.app/api/threat-feed?limit=20&verdict=Malicious"

Query parameters:

Parameter Type Default Description
limit int 50 Results per page (max 500)
offset int 0 Pagination offset
verdict string Filter: Malicious, High Risk, Suspicious
format string json Response format: json, jsonl, csv, stix

JSONL streaming (for large exports):

curl "https://droidwatch.app/api/threat-feed?format=jsonl&limit=500" \
  > feed.jsonl

STIX 2.1 bundle:

curl "https://droidwatch.app/api/threat-feed?format=stix" \
  | python -m json.tool

GET /api/threat-feed/lookup

Hash, IP, domain, or package name lookup.

# SHA-256 lookup
curl "https://droidwatch.app/api/threat-feed/lookup?q=e3b0c44298fc1c149afbf4c8996fb924"

# Package name lookup
curl "https://droidwatch.app/api/threat-feed/lookup?q=com.example.suspiciousapp"

# Domain lookup
curl "https://droidwatch.app/api/threat-feed/lookup?q=evil-c2.example.com"

Response:

{
  "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

Public app index — no authentication required.

GET /api/explore

Paginated list of known packages with aggregated analysis statistics.

curl "https://droidwatch.app/api/explore?limit=20&offset=0"

Query parameters:

Parameter Type Default Description
limit int 20 Results per page
offset int 0 Pagination offset
verdict string Filter by verdict
sort string latest Sort: latest, score, name

Response:

{
  "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

Prefix/substring search by package name or app name.

curl "https://droidwatch.app/api/explore/search?q=banking&limit=10"

Query parameters:

Parameter Type Description
q string Search term (package name or app name)
limit int Max results (default 20, max 100)

Batch Analysis

POST /api/batch/upload

Upload multiple APKs in a single request.

curl -X POST https://droidwatch.app/api/batch/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "[email protected]"

Response:

{
  "batch_id": "batch-7f3a2b1c",
  "run_ids": ["run-001", "run-002", "run-003"],
  "status": "queued"
}

GET /api/jobs?batch_id={batch_id}

Poll progress for all jobs in a batch.

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

Error codes

HTTP Status Meaning
400 Bad request — invalid parameters or body
401 Not authenticated — missing or expired token
403 Forbidden — insufficient role or plan
404 Resource not found
413 APK exceeds the size limit for your plan
429 Rate limit exceeded
500 Internal server error — include request_id when reporting

SDK / code examples

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})
run_id = r.json()["run_id"]

# Trigger analysis
session.post(f"{BASE}/api/analyze", json={"run_id": run_id})

# Poll
import time
while True:
    job = session.get(f"{BASE}/api/jobs/{run_id}").json()
    if job["status"] in ("completed", "failed"):
        break
    time.sleep(3)

# Fetch report
report = session.get(f"{BASE}/api/runs/{run_id}/report").json()
print(report["overview"]["verdict"], report["overview"]["score"])