APK and URL Scan API

URL scans return JSON synchronously. APKs can be submitted by file upload, full fingerprints, or package name, with results delivered to a verified signed webhook.

Get started

Overview

URL scans finish inside the request. APK scans are asynchronous. Every scan endpoint shares the account-wide membership rate window.

01Submit

Send a scan request or upload an APK

02Review

Validate, deduplicate, and run the scan

03Poll / Callback

Poll GET /api/v1/jobs/{job_id} and/or receive a signed webhook

APK API supports polling

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.

JSON · common APK fields
{
  "client_reference": "order-20260711-001",
  "callback_url": "https://example.com/webhooks/fvl",
  "use_cache": false
}
Access control

Authentication

Use either header on every request. A key is shown in full only once and must remain on your server.

RecommendedAuthorization: Bearer fvl_xxx
CompatibleX-API-Key: fvl_xxx

Never embed API keys in browsers, mobile apps, public repositories, or logs.

Access control

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.

PlanMinimum intervalDaily R2 upload
VIP+10 minutes2 GiB
Pro3 minutes5 GiB
SVIP5 minutes10 GiB
SVIP+1 minute30 GiB
AdministratorUnlimitedUnlimited
URL scans, upload-session creation, APK fingerprints, and package checks share the account rate. Cache hits, idempotent replays, and merged in-flight work do not open another window.
Each user may have at most one active incomplete upload session. Creating an R2 session prepays one rescan and opens the rate window; the charge is not refunded after issuance.
API

Endpoints

Requests and responses use JSON except for the presigned R2 PUT step.

POST/api/v1/url-scanScan a URL synchronously

Send url and receive the risk result plus post-charge quota in the same response. No webhook is used.

cURL
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"}'
HTTP 200
{
  "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"
    }
  }
}
POST/api/v1/apk/uploadsCreate an APK upload session

Requires Idempotency-Key, an .apk filename, size, file_md5, expected_sha256, and the common callback fields. The upload URL lasts 30 minutes.

cURL
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"
  }'
PUT · R2 required_headers
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
POST/api/v1/apk/uploads/completeComplete the upload and submit review

Send upload_token with Idempotency-Key. The platform validates the object and submits the prepaid review without charging again.

cURL
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"}'
POST/api/v1/apk/fingerprintsSubmit APK fingerprints

Send package_name, sha256, file_md5, cert_md5, and the common callback fields without uploading the APK again.

cURL
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"
  }'
POST/api/v1/packages/checkCheck a package name

Quickly checks device-side package risk. This is not a full APK scan with file hashes and certificate data.

cURL
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"
  }'
HTTP 202
{
  "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"
  }
}
GET/api/v1/jobs/{job_id}Poll an async job

Use job_id from the acceptance response to read pending / processing / completed / failed. Results are kept for about 24 hours.

cURL
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"
  }'
HTTP 202
{
  "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

Prepaid before issuance; no refund after issuance

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.

Sample retention

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.

Request control

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 rescan

false bypasses an existing result and consumes one rescan when a scan actually starts.

cache_status = bypass
trueAllow cache

true returns a valid cached result without charging; a miss or expired result triggers a rescan.

cache_status = hit | miss
Result delivery

Signed 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-TimestampX-FVL-Timestamp: 1720000000
X-FVL-SignatureX-FVL-Signature: sha256=<hex>
  1. Compute webhook_secret = lowercase_hex(SHA-256(raw_api_key)).
  2. Sign X-FVL-Timestamp + "." + raw_body.
  3. Use the UTF-8 webhook_secret as the HMAC-SHA256 key and compare in constant time.
  4. Validate the timestamp and return any 2xx; non-2xx responses are retried.
Webhook JSON
{
  "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"
}
Node.js · HMAC-SHA256
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.

Request control

Idempotency and deduplication

Upload creation, upload completion, fingerprint scans, and package checks require Idempotency-Key.

The same key and request return the same result for 24 hours without another charge.
Using one key for a different request returns HTTP 409.
Concurrent rescans of the same asset merge into one scan while each caller still receives its own webhook.
Reference

Errors

Synchronous errors use HTTP status codes and a uniform JSON body. Asynchronous APK review failures arrive in a scan.failed webhook.

HTTPMeaningAction
400Invalid request or fieldCorrect the JSON, hashes, package name, or callback URL.
401Authentication failedCheck the API key.
403Access deniedCheck the plan and expiry.
409State conflictCheck the idempotency key or upload state.
429Quota or rate limitedHonor Retry-After and retry later.
503Service unavailableRetry with the same idempotency key as defined by the endpoint.
Node.js
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());
Python
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())
Machine-readable OpenAPI 3.1Generate clients, validate requests, or import the API into development tools.
Reference

Changelog

v1.0.0

Async APK jobs support job_id polling; callback_url is optional. Added the Pro membership tier.