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