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