REST API¶
Packing over HTTP. Every endpoint except GET / requires the API key.
Send it as the X-API-Key header. The SSE stream also accepts ?api_key= in the
query string, because the browser EventSource API cannot set headers.
Interactive docs are served at /docs (Swagger) and /redoc.
Pack an APK¶
Packing runs in the background; the response returns immediately.
Form fields¶
| Field | Type | Default | Description |
|---|---|---|---|
file |
file | — | The .apk to pack (required) |
app_class |
string | auto-detect | Original Application class |
webhook_url |
string | — | Comma-separated URLs to POST to on completion |
exclude_files |
string | — | Comma-separated entry names to leave unencrypted |
encrypt_native |
bool | true |
Encrypt .so libraries |
encrypt_assets |
bool | true |
Encrypt user assets |
encrypt_strings |
bool | env | XOR-obfuscate DEX strings |
root_detection |
bool | env | Block rooted devices |
emulator_detection |
bool | env | Block emulators |
Omitting encrypt_strings, root_detection or emulator_detection defers to the
corresponding environment variable; sending an
explicit value always wins.
Stream progress¶
text/event-stream, one JSON object per event:
{"status": "running", "step": "encrypting_dex", "pct": 40}
{"status": "done", "step": "done", "pct": 100, "result": {"app_id": "...", "...": "..."}}
{"status": "error", "step": "error", "pct": 0, "error": "APK does not contain classes.dex"}
The stream closes after a done or error event.
One consumer per stream
Events are consumed from a queue, so two clients watching the same job split the
events between them rather than each seeing all. A client that connects after
the job finished will not receive the terminal event either — poll
GET /jobs/{id} instead.
Poll status¶
{
"job_id": "9f0c1d3e-...",
"status": "done",
"step": "done",
"pct": 100,
"result": { "app_id": "...", "package_name": "com.example.app", "apk_signature": "...", "analysis": {} },
"error": null
}
Polling is the durable path: job state is mirrored to the database, so it survives a server restart and in-memory eviction. Use it rather than SSE for automation.
Download the packed APK¶
$APP_ID comes from result.app_id on a completed job.
List packed apps¶
[
{
"app_id": "...",
"package_name": "com.example.app",
"apk_signature": "3f2a...",
"analysis": {},
"has_mapping": true,
"created_at": "2026-08-01T09:12:44"
}
]
ProGuard mapping¶
Keep the mapping.txt for a packed build alongside it, so crash reports stay
deobfuscatable.
# Upload
curl -X POST "http://localhost:8000/apps/$APP_ID/mapping/upload" \
-H "X-API-Key: $FUIN_API_KEY" -F "file=@mapping.txt"
# Download
curl -OJ "http://localhost:8000/apps/$APP_ID/mapping" -H "X-API-Key: $FUIN_API_KEY"
Delete¶
Removes the database row, the packed APK and the mapping file.
Analyze without packing¶
curl -sX POST http://localhost:8000/analyze \
-H "X-API-Key: $FUIN_API_KEY" \
-F "file=@MyApp.apk" | jq .summary
Same data as fuin-pack analyze. Nothing is stored.
Webhooks¶
Pass webhook_url on POST /pack, or set
FUIN_WEBHOOK_URL globally, and fuin POSTs on
completion:
Delivery is fire-and-forget: failures are logged, not retried.
Full endpoint list¶
See API reference for status codes and error shapes.