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

Create an API key from the dashboard (Profile → API keys). Send it on every request as the X-API-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

Up to 20 files per batch. Requires an authenticated account with a verified email address.

Analysing a batch is two calls, not one: /upload stores the files and hands back an upload_id for each, and /analyze is what actually queues the jobs.

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:

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

A file the server rejects (wrong extension, over the plan's size limit) lands in errors and does not stop the rest of the batch.

POST /api/batch/analyze

Queue an analysis for each upload. This is the call that returns the 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"}
      ]}'

Response:

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

Poll progress for every job in the batch.

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

Response:

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

Batches expire 24 hours after they are created, after which this returns 404.


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