API Docs

Public download API documentation for web pages, scripts, clients and automatic updaters.

Download integration

Choose the download integration that fits your use case. For download buttons on web pages, homepages, forums, announcements or front-end pages, redirect to the main-site verification page. For scripts, clients, CI, auto-updaters or backend services, use the programmatic API flow.

Before integrating: stability and conventions

Only endpoints explicitly documented here are part of the stable third-party integration contract. New clients should prefer the project/asset lookup endpoints and download authorization protocol V2. A publicly reachable endpoint that is not documented here must not be treated as a long-term compatibility promise.

Stable developer endpoints

GET  /api/public/v1/projects
GET  /api/public/v1/projects/{project_id}/assets
POST /api/public/v2/api/challenges
POST /api/public/v2/api/authorizations
GET  /api/public/v2/authorizations/{authorization_id}
GET  /api/public/v1/blocklist.txt
GET  /api/public/v1/blocklist.json
GET  /api/public/v1/changelog
POST /api/developer/v1/projects/{project_id}/sync

The term “download authorization protocol V1” refers only to the legacy /api/public/v1/api/challenges and /api/public/v1/api/authorizations PoW/authorization endpoints. It does not mean that the entire /api/public/v1/ namespace is deprecated. V1 resource endpoints for projects, assets, blocklists and changelog remain current stable APIs.

Publicly reachable site-internal endpoints

/api/public/v1/catalog, /api/public/v1/stats, /api/public/v1/stats/details, /api/public/v2/web/* and web-verification endpoints are used by this site's front end or internal displays. They are publicly reachable but are not currently a stable third-party API contract, and their fields or behavior may change with the site implementation. Third-party programs should not depend on them.

JSON response envelope

Except for the TXT blocklist feed, download-node file responses and the site-internal data endpoints called out above, JSON APIs documented here use a common envelope. Read data on success; on failure branch on the HTTP status and stable code, and retain request_id for troubleshooting.

{
  "status": "success",
  "message": "...",
  "request_id": "req_...",
  "data": {}
}
{
  "status": "error",
  "message": "...",
  "request_id": "req_...",
  "code": "STABLE_ERROR_CODE"
}

POST endpoints use JSON request bodies. Explicitly send Content-Type: application/json; public download API JSON bodies are limited to 16 KiB.

Rate limits, retries and errors

Public endpoints are protected by configurable source-level resource limits and download abuse controls. For 429 or 503 responses, honor Retry-After when present instead of polling at a fixed high rate. The Developer API additionally returns X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.

HTTPcodeDescription
401DOWNLOAD_TOKEN_INVALIDThe download token is invalid, expired or does not belong to the source querying this authorization.
403CHALLENGE_FAILED / CLIENT_BLOCKEDThe challenge does not match, validation failed, or the current source is blocked.
409NO_ROUTABLE_NODE / CHALLENGE_IN_PROGRESSNo routable node is available, or the same challenge is still being processed.
410API_VERSION_RETIREDThe legacy download authorization protocol V1 is disabled in the current deployment.
429PUBLIC_RESOURCE_RATE_LIMITED / CLIENT_RATE_LIMITED / CHALLENGE_RATE_LIMITED / REQUEST_QUOTA_EXHAUSTED / TRAFFIC_LIMIT_EXCEEDEDA request, challenge or traffic quota was limited; honor Retry-After when present.
503CHALLENGE_CAPACITY_REACHED / VDF_BUSYThe challenge service is temporarily busy and normally returns Retry-After.
500PUBLIC_INTERNAL_ERRORInternal server error; retain request_id before retrying or reporting the issue.

Client-source binding

Challenge creation and authorization submission must be performed from the same client network prefix recognized by the server. Authorization-status queries must also use the source prefix that received the authorization. Do not split challenge creation, solution submission and status polling across machines with different egress IPs. After authorization succeeds, download_token is a Bearer credential; the actual download-node request may use a different egress, so protect the token like a password.

Method 1: redirect to the main-site verification page

This method is suitable for web download buttons. You do not need to handle challenges, PoW or download tokens yourself, and you do not need to know which download node currently serves the file.

Important: redirect to the main-site address, not a download-node address. External front ends should send users to the main-site verification page, where verification, authorization and node selection are completed before redirecting to the actual download node.

GET/{project_id}/{version}/{file_name}

Verification URL example

https://fyhub.cn/fcl/1.3.0.9/FCL-release-1.3.0.9-arm64-v8a.apk

https://fyhub.cn is the public entry point of Maple Mirror, not a download node's public_download_base_url.

If you do not know the project, query the project list first

This endpoint returns project_id, the project display name and availability. Front ends usually call it while initializing a download page, project selector or external download list.

GET/api/public/v1/projects

Example Request

GET /api/public/v1/projects
GET/api/public/v1/projects/{project_id}/assets

After obtaining project_id, query the versions and files for that project. The response includes fields such as version, file_name, architecture, system, size_bytes, digest_sha256 and available.

Example Request

GET /api/public/v1/projects/example/assets

Example Response

{
  "status": "success",
  "data": {
    "assets": [
      {
        "asset_id": "asset_123",
        "version": "v1.2.3",
        "file_name": "example-windows-amd64.zip",
        "architecture": "amd64",
        "system": "win",
        "available": true
      }
    ]
  }
}

The front end can use these fields to create a download button. When the user clicks it, redirect to the main-site verification page:

https://fyhub.cn/example/v1.2.3/example-windows-amd64.zip
  1. Show the download verification page.
  2. Solve the download challenge in the browser.
  3. Obtain short-lived download authorization from the main node.
  4. Choose an available download node.
  5. Redirect to the real download URL.

External sites must not construct node download URLs directly or replace this flow with the programmatic /api/public/v2/api/* endpoints.

Method 2: download through the programmatic API

This method is suitable for command-line tools, auto-updaters, CI scripts, downloaders and backend services. The program must query assets, complete API PoW verification and then use the download token to access a download node.

Step 1: query the project list

GET/api/public/v1/projects

Query the currently enabled projects and obtain project_id. A project may still appear while no routable download asset is available, so use available to decide whether it can be downloaded now.

ParameterTypeDescription
None

Example Request

GET /api/public/v1/projects

Example Response

{
  "status": "success",
  "message": "查询成功",
  "request_id": "req_...",
  "data": {
    "projects": [
      {
        "project_id": "example",
        "repository": "owner/example",
        "display_name": "示例项目",
        "description": "示例说明",
        "homepage_url": "https://fyhub.cn",
        "available": true
      }
    ]
  }
}

Step 2: query project assets

GET/api/public/v1/projects/{project_id}/assets

Choose an asset with available=true and record its asset_id. If available=false, no available download node currently holds the file, so retry later.

ParameterTypeDescription
project_idPathProject identifier

Example Request

GET /api/public/v1/projects/example/assets

Example Response

{
  "status": "success",
  "data": {
    "assets": [
      {
        "asset_id": "asset_123",
        "version": "v1.2.3",
        "download_path": "/example/v1.2.3/example.zip",
        "prerelease": false,
        "file_name": "example.zip",
        "architecture": "amd64",
        "system": "win",
        "size_bytes": 10485760,
        "digest_sha256": "0123456789abcdef...",
        "available": true,
        "unavailable_reason": ""
      }
    ]
  }
}

Step 3: create an API V2 sequential-work challenge

POST/api/public/v2/api/challenges

Create a 3072-bit RSA repeated-squaring challenge for an asset. The modulus and base in the response are unpadded base64url encodings of 384-byte unsigned big-endian integers.

ParameterTypeDescription
asset_idJSONAsset to download

Example Request

POST /api/public/v2/api/challenges
{"asset_id":"asset_123"}

Example Response

{
  "status": "success",
  "data": {
    "challenge_id": "challenge_123",
    "asset_id": "asset_123",
    "algorithm": "rsa-repeated-squaring-v1",
    "modulus_id": "模数标识",
    "modulus": "512 字符 base64url 整数",
    "base": "512 字符 base64url 整数",
    "iterations": 96000,
    "encoding": "base64url-uint-be-384",
    "expires_at": "2026-09-03T12:05:00Z"
  }
}

Step 4: compute the solution sequentially

Starting from y = base, perform y = y² mod modulus exactly iterations times, then encode y as a fixed-length 384-byte big-endian base64url value. Do not submit a decimal, hexadecimal or variable-length integer.

Learn how RSA repeated-squaring works

This step computes a sequential proof of work, not an ordinary password hash. The challenge provides an RSA modulus N, a starting base and a final iteration count; the client must continue modular squaring in order.

Computation

y = decode_unsigned_big_endian(base)
重复 iterations 次:
    y = (y × y) mod N
solution = base64url_no_padding(unsigned_big_endian_384(y))

modulus, base and solution must each be exactly 384-byte unsigned big-endian integers encoded as unpadded base64url, so the wire string is always 512 characters. Leading zero bytes must be retained.

Why execution must be sequential

Each square depends on the complete result of the previous one. Mathematically the final value is base^(2^iterations) mod N, but the client does not know the RSA trapdoor and cannot reduce the exponent with Euler's function. Constructing 2^iterations does not bypass the dependencies. Separate ranges cannot be merged independently, so use one sequential loop.

How the server verifies the result

The main node keeps an in-memory RSA trapdoor and can compute the same final value quickly. It stores a digest of the fixed-length result when creating the challenge. During authorization it checks the challenge version, algorithm, source, asset, client prefix, expiry and one-time state, then validates 0 < solution < N and the digest. The trapdoor and expected answer are never sent to the client or written to PoW telemetry.

Common implementation pitfalls

  • The loop count must exactly equal the final iterations in the response; do not use a local default.
  • Each round must square first and then reduce modulo N; do not replace it with hashing, multiplication by base or parallel nonce search.
  • Decode and encode as unsigned big-endian; left-pad the output to 384 bytes.
  • Use - and _ for base64url and omit = padding.
  • Challenges expire and bind the asset and client source. Create a new challenge after failure; do not reuse one across assets or API versions.

Step 5: submit the solution and obtain authorization

POST/api/public/v2/api/authorizations

Submit the sequential-work result to obtain short-lived authorization bound to one node. challenge_id, asset_id and solution must all belong to the same V2 challenge flow. The programmatic API currently has no telemetry fields that clients need to report.

ParameterTypeDescription
challenge_idJSONChallenge identifier
asset_idJSONAsset to download
solutionJSONFixed 512-character base64url repeated-squaring result

Example Request

POST /api/public/v2/api/authorizations
{"challenge_id":"challenge_123","asset_id":"asset_123","solution":"512 字符 base64url 整数"}

Example Response

{
  "status": "success",
  "data": {
    "authorization_id": "auth_123",
    "download_url": "由服务端返回的实际下载节点 URL",
    "download_token": "43 字符短时随机令牌",
    "expires_at": "2026-09-03T12:05:00Z",
    "range_concurrency_limit": 32,
    "max_bytes": 20971520
  }
}

Step 6: request the download node

Access download_url directly and send download_token in the request header. It normally points to a download node. After authorization succeeds, the actual file request may use a different egress from the challenge stage, but the token is a Bearer credential and can be used by whoever possesses it. Also honor range_concurrency_limit and max_bytes from the response.

GET{download_url}

Example Request

GET {download_url}
Authorization: Bearer <download_token>

For resumable downloads, use a single Range segment:

GET {download_url}
Authorization: Bearer <download_token>
Range: bytes=1048576-2097151

Other interfaces

These interfaces are not steps in the programmatic download flow; they are for subscriptions, status queries and troubleshooting.

Blocklist subscription

GET/api/public/v1/blocklist.txt
GET/api/public/v1/blocklist.json

Returns the active public download blocklist. It includes the static quota.yaml blocklist and manual or automatic local blocks, excludes remote subscription snapshots, and is cached by the server for 60 seconds.

TXT Example Response

# [枫源镜像封禁] 封禁原因: traffic_limit_exceeded, 来源: local_auto_ban, 封禁后尝试次数: 3
192.0.2.123

JSON Example Response

{
  "status": "success",
  "data": {
    "blocks": [
      {"entry": "192.0.2.123", "reason": "traffic_limit_exceeded", "attempts_after_block": 3, "blocked_at": "2026-06-21T12:00:00Z"}
    ]
  }
}

Project developer update trigger

POST/api/developer/v1/projects/{project_id}/sync

Lets a mirrored project owner trigger an immediate update check after publishing a GitHub Release. Each Developer API Token is bound to exactly one project and can be generated or reset from that project's card in the admin panel.

ParameterTypeDescription
project_idPathProject identifier
AuthorizationHeaderBearer project Developer API Token

Example Request

POST /api/developer/v1/projects/example/sync
Authorization: Bearer <project_token>

Each project allows 100 effective triggers per day by default. Responses include X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; exhausted quota returns 429.

Accepted requests return 202. If the same project is already being scanned, the API still returns 202 but does not start another scan or consume another daily request.

Example Response

{
  "status": "success",
  "message": "更新检测任务已接受",
  "data": {"project_id": "example", "accepted": true, "already_running": false}
}

Common status codes: 401 for missing or invalid tokens; 403 for a token/project mismatch; 404 for an unknown project; 409 for a disabled project; and 429 for exhausted daily quota or excessive authentication failures.

Changelog

GET/api/public/v1/changelog

Query the site's changelog in reverse chronological order. minimum_level accepts info, notice, warn or critical; q searches titles and visible descriptions; limit defaults to 20 and is capped at 50. When another page exists, the response returns an opaque next_cursor.

Example Request

GET /api/public/v1/changelog?minimum_level=notice&q=下载&limit=20
GET/api/public/v2/authorizations/{authorization_id}

Use the corresponding download_token to query V2 authorization state, expiry, public node name and accounted bytes sent. The old /api/public/v1/authorizations/{authorization_id} path is retained only as a compatibility alias; new clients should not use it.

ParameterTypeDescription
authorization_idPathAuthorization identifier
AuthorizationHeaderBearer <download_token>

node_name is the new explicit field. node_id is temporarily retained for backward compatibility and currently contains the same public node name; it does not expose the internal node ID. New clients should read node_name.

Example Request

GET /api/public/v2/authorizations/auth_123
Authorization: Bearer <download_token>

Example Response

{
  "status": "success",
  "data": {
    "authorization_id": "auth_123",
    "asset_id": "asset_123",
    "node_name": "public-node-name",
    "node_id": "public-node-name",
    "state": "issued",
    "expires_at": "2026-09-03T12:05:00Z",
    "bytes_accounting_enabled": true,
    "sent_bytes": 1048576,
    "first_transfer_at": "2026-09-03T12:01:10Z"
  }
}