Overview
URL scans finish inside the request. APK scans are asynchronous. Every scan endpoint shares the account-wide membership rate window.
Send a scan request or upload an APK
Validate, deduplicate, and run the scan
Poll GET /api/v1/jobs/{job_id} and/or receive a signed webhook
Acceptance responses include job_id. Poll GET /api/v1/jobs/{job_id} for about 24 hours. callback_url is optional; when set, a signed webhook is also delivered.
{
"client_reference": "order-20260711-001",
"callback_url": "https://example.com/webhooks/fvl",
"use_cache": false
}Authentication
Use either header on every request. A key is shown in full only once and must remain on your server.
Authorization: Bearer fvl_xxxX-API-Key: fvl_xxxNever embed API keys in browsers, mobile apps, public repositories, or logs.
Quota and rate limits
The API is available to VIP+, Pro, SVIP, SVIP+, and administrators. All API keys on one account share the same rate window.
| Plan | Minimum interval | Daily R2 upload |
|---|---|---|
| VIP+ | 10 minutes | 2 GiB |
| Pro | 3 minutes | 5 GiB |
| SVIP | 5 minutes | 10 GiB |
| SVIP+ | 1 minute | 30 GiB |
| Administrator | Unlimited | Unlimited |
Endpoints
Requests and responses use JSON except for the presigned R2 PUT step.
/api/v1/url-scanScan a URL synchronouslySend url and receive the risk result plus post-charge quota in the same response. No webhook is used.
curl -X POST https://www.fuckviruslabel.com/api/v1/url-scan \
-H "Authorization: Bearer fvl_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/path"}'{
"code": 0,
"message": "success",
"data": {
"result": {
"status": "completed",
"url": "https://example.com/path",
"domain": "example.com",
"flagged": false,
"mainType": 0,
"mainTypeName": "安全",
"provider": "OPPO",
"websiteType": 0,
"matchUrl": "",
"riskDetail": "",
"cacheStatus": "live",
"checkedAt": 1783765471000,
"createdAt": 1783765471000
},
"quota": {
"identity_label": "VIP+",
"daily_limit": 20,
"daily_used": 1,
"daily_remaining": 19,
"scan_credits": 0,
"total_remaining": 19,
"usage_date": "2026-07-11"
}
}
}/api/v1/apk/uploadsCreate an APK upload sessionRequires Idempotency-Key, an .apk filename, size, file_md5, expected_sha256, and the common callback fields. The upload URL lasts 30 minutes.
curl -X POST https://www.fuckviruslabel.com/api/v1/apk/uploads \
-H "Authorization: Bearer fvl_xxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: upload-20260711-001" \
-H "Content-Type: application/json" \
-d '{
"client_reference": "order-20260711-001",
"callback_url": "https://api.example.com/webhooks/fvl",
"use_cache": false,
"filename": "release.apk",
"size": 24117384,
"file_md5": "0123456789abcdef0123456789abcdef",
"expected_sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}'curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/vnd.android.package-archive" \
-H "Content-Length: 24117384" \
-H "Content-MD5: ASNFZ4mrze8BI0VniavN7w==" \
-H "If-None-Match: *" \
--upload-file ./release.apk/api/v1/apk/uploads/completeComplete the upload and submit reviewSend upload_token with Idempotency-Key. The platform validates the object and submits the prepaid review without charging again.
curl -X POST https://www.fuckviruslabel.com/api/v1/apk/uploads/complete \
-H "Authorization: Bearer fvl_xxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: order-20260711-001" \
-H "Content-Type: application/json" \
-d '{"upload_token":"upl_xxxxxxxxxxxxxxxx"}'/api/v1/apk/fingerprintsSubmit APK fingerprintsSend package_name, sha256, file_md5, cert_md5, and the common callback fields without uploading the APK again.
curl -X POST https://www.fuckviruslabel.com/api/v1/apk/fingerprints \
-H "Authorization: Bearer fvl_xxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: fingerprint-20260711-001" \
-H "Content-Type: application/json" \
-d '{
"client_reference": "fingerprint-20260711-001",
"callback_url": "https://api.example.com/webhooks/fvl",
"use_cache": true,
"package_name": "com.example.app",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"file_md5": "0123456789abcdef0123456789abcdef",
"cert_md5": "abcdef0123456789abcdef0123456789",
"version_code": 108,
"version_name": "1.0.8",
"size": 24117384,
"filename": "release.apk"
}'/api/v1/packages/checkCheck a package nameQuickly checks device-side package risk. This is not a full APK scan with file hashes and certificate data.
curl -X POST https://www.fuckviruslabel.com/api/v1/packages/check \
-H "X-API-Key: fvl_xxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: package-20260711-001" \
-H "Content-Type: application/json" \
-d '{
"client_reference": "package-20260711-001",
"callback_url": "https://api.example.com/webhooks/fvl",
"use_cache": false,
"package_name": "com.example.app"
}'{
"code": 0,
"message": "accepted",
"data": {
"client_reference": "order-20260711-001",
"job_id": "acd_xxxxxxxxxxxxxxxx",
"delivery": "webhook_and_poll",
"poll_path": "/api/v1/jobs/acd_xxxxxxxxxxxxxxxx"
}
}/api/v1/jobs/{job_id}Poll an async jobUse job_id from the acceptance response to read pending / processing / completed / failed. Results are kept for about 24 hours.
curl -X POST https://www.fuckviruslabel.com/api/v1/packages/check \
-H "X-API-Key: fvl_xxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: package-20260711-001" \
-H "Content-Type: application/json" \
-d '{
"client_reference": "package-20260711-001",
"callback_url": "https://api.example.com/webhooks/fvl",
"use_cache": false,
"package_name": "com.example.app"
}'{
"code": 0,
"message": "accepted",
"data": {
"client_reference": "order-20260711-001",
"job_id": "acd_xxxxxxxxxxxxxxxx",
"delivery": "webhook_and_poll",
"poll_path": "/api/v1/jobs/acd_xxxxxxxxxxxxxxxx"
}
}Prepaid before issuance; no refund after issuance
The platform checks membership, account rate, rescan quota, daily upload bytes, and active sessions before charging one rescan and issuing the URL. Incomplete or expired uploads, hash mismatches, invalid APKs, and queue failures are not refunded. Transient enqueue failures retain the sample for durable automatic retry.
Valid APKs are deduplicated by SHA-256 and archived; invalid or inconsistent samples go to quarantine. Both remain for 90 days. Incomplete uploads are removed after one day.
use_cache semantics
use_cache is a top-level JSON field and defaults to false. R2 uploads are prepaid before URL issuance regardless of this value; it only controls result reuse after completion.
falseForce rescanfalse bypasses an existing result and consumes one rescan when a scan actually starts.
cache_status = bypasstrueAllow cachetrue returns a valid cached result without charging; a miss or expired result triggers a rescan.
cache_status = hit | missSigned webhook
The webhook is the only result channel for asynchronous APK scans. The server sends raw JSON bytes, a timestamp, and an HMAC-SHA256 signature.
X-FVL-Timestamp: 1720000000X-FVL-Signature: sha256=<hex>- Compute webhook_secret = lowercase_hex(SHA-256(raw_api_key)).
- Sign X-FVL-Timestamp + "." + raw_body.
- Use the UTF-8 webhook_secret as the HMAC-SHA256 key and compare in constant time.
- Validate the timestamp and return any 2xx; non-2xx responses are retried.
{
"event": "apk.scan.completed",
"client_reference": "order-20260711-001",
"operation": "apk_upload",
"status": "completed",
"cache_status": "bypass",
"result": { "flagged": false, "result_type_name": "安全" },
"quota": { "refresh_daily_remaining": 29, "total_remaining": 29 },
"checked_at": "2026-07-11T10:24:31.000Z"
}import crypto from "node:crypto";
export function verifyFvlWebhook(rawBody, timestamp, signature, apiKey) {
const webhookSecret = crypto
.createHash("sha256")
.update(apiKey, "utf8")
.digest("hex");
const expected = "sha256=" + crypto
.createHmac("sha256", webhookSecret)
.update(timestamp + "." + rawBody)
.digest("hex");
const expectedBytes = Buffer.from(expected, "utf8");
const signatureBytes = Buffer.from(signature, "utf8");
return expectedBytes.length === signatureBytes.length &&
crypto.timingSafeEqual(expectedBytes, signatureBytes);
}Read the raw body before parsing JSON. Re-serializing changes the bytes and breaks verification.
Idempotency and deduplication
Upload creation, upload completion, fingerprint scans, and package checks require Idempotency-Key.
Errors
Synchronous errors use HTTP status codes and a uniform JSON body. Asynchronous APK review failures arrive in a scan.failed webhook.
| HTTP | Meaning | Action |
|---|---|---|
400 | Invalid request or field | Correct the JSON, hashes, package name, or callback URL. |
401 | Authentication failed | Check the API key. |
403 | Access denied | Check the plan and expiry. |
409 | State conflict | Check the idempotency key or upload state. |
429 | Quota or rate limited | Honor Retry-After and retry later. |
503 | Service unavailable | Retry with the same idempotency key as defined by the endpoint. |
const body = {
client_reference: "package-20260711-001",
callback_url: "https://api.example.com/webhooks/fvl",
use_cache: true,
package_name: "com.example.app"
};
const response = await fetch(
"https://www.fuckviruslabel.com/api/v1/packages/check",
{
method: "POST",
headers: {
Authorization: "Bearer " + process.env.FVL_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": body.client_reference
},
body: JSON.stringify(body)
}
);
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import os
import requests
payload = {
"client_reference": "package-20260711-001",
"callback_url": "https://api.example.com/webhooks/fvl",
"use_cache": True,
"package_name": "com.example.app",
}
response = requests.post(
"https://www.fuckviruslabel.com/api/v1/packages/check",
headers={
"Authorization": f"Bearer {os.environ['FVL_API_KEY']}",
"Idempotency-Key": payload["client_reference"],
},
json=payload,
timeout=20,
)
response.raise_for_status()
print(response.json())Changelog
Async APK jobs support job_id polling; callback_url is optional. Added the Pro membership tier.