Developer API for tool teams

Everything your dev team needs to run a tool on its own: edit the public details, post screenshots, write the user guide and publish new versions — without the admin panel, and without waiting for anyone to open a door.

This page is for whoever builds the releases.
If you are embedding licence checks into the build you ship to customers, your part is in the client integration docs: sign-in, licence checks, device management.
Base URL
https://cyanpro.app
Auth
X-Dev-Token header

Endpoints

MethodPathPurpose
GET /api/v1/dev/product Read tool details and the current release
PATCH /api/v1/dev/product Edit description, platforms, download link, user guide
GET /api/v1/dev/releases Release history (last 100)
POST /api/v1/dev/releases Publish a new version
GET /api/v1/dev/images List screenshots
POST /api/v1/dev/images Add a screenshot (upload a file or point at an https URL)
DELETE /api/v1/dev/images/:id Delete a screenshot
POST /api/v1/dev/icon Set the app icon (upload a file or point at an https URL)
DELETE /api/v1/dev/icon Remove the icon and fall back to initials
POST /api/v1/dev/cover Set the cover image (upload a file or point at an https URL)
DELETE /api/v1/dev/cover Remove the cover image

Developer key

Get it from Admin → Tools → Developer keys. The key is shown once at creation; if you lose it, revoke and create a new one — only its hash is stored.

  • Each key is bound to exactly one tool. The team behind one tool cannot touch another.
  • Never ship this key inside a customer build. It can change content and download links; anyone who extracts it from your executable can repoint your installer.
  • It belongs on developer machines and in build-system secrets (GitHub Actions secrets or equivalent).
  • This branch does not accept the client X-Api-Key, and a dev key cannot sign a customer in. The two doors are entirely separate.
  • Revoking takes effect immediately. Create a separate key per build system so revoking one does not break the others.

Quickstart

These three commands are enough to check the key and push a release.

1. Read the current tool details

curl -s https://cyanpro.app/api/v1/dev/product \
  -H "X-Dev-Token: $CYAN_DEV_TOKEN"

2. Change one field

curl -s -X PATCH https://cyanpro.app/api/v1/dev/product \
  -H "X-Dev-Token: $CYAN_DEV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tagline":"Bộ công cụ desktop cho công việc hằng ngày"}'

3. Publish a new version

curl -s -X POST https://cyanpro.app/api/v1/dev/releases \
  -H "X-Dev-Token: $CYAN_DEV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "version": "1.1.0",
        "notes": "- Sửa lỗi đăng nhập\n- Tăng tốc khởi động",
        "download_url": "https://example.com/setup-1.1.0.exe"
      }'

Editing tool details

Send only the fields you want to change; anything absent stays as it was. The response lists the fields that changed so your build system can log it.

FieldTypeNotes
name string Tool name, up to 120 characters. Cannot be empty.
tagline string One-line description, up to 200 characters. Shown under the name in the catalogue.
tagline_en string English one-liner. Left empty, the English site falls back to the Vietnamese one.
description string Long description, up to 4000 characters. Shown on the detail page.
description_en string English long description.
guide string Full user guide in Markdown, up to 40000 characters.
guide_en string English user guide.
icon string An emoji or 1–2 letters, used only until an icon image exists.
icon_url string Icon image, https only. To upload a file, use POST /dev/icon instead.
cover_url string Cover image, https only. To upload a file, use POST /dev/cover instead.
platforms string[] Array of strings. Accepted: Windows, macOS, Linux, Android, iOS, Web. Anything else is dropped.
download_url string Default download link. Must be https.
PATCH https://cyanpro.app/api/v1/dev/product
X-Dev-Token: cyd_live_xxxxxxxx
Content-Type: application/json

{
  "tagline": "Bộ công cụ desktop cho công việc hằng ngày",
  "tagline_en": "A desktop toolkit for everyday work",
  "description": "Mô tả dài hơn, hiện ở trang chi tiết.",
  "platforms": ["Windows", "macOS"],
  "download_url": "https://example.com/setup.exe"
}
200 OK
{
  "ok": true,
  "updated": ["tagline", "tagline_en", "description", "platforms", "download_url"],
  "product": { "slug": "cyan-studio", "name": "CYAN Studio", ... }
}

Fields the API will not change

Sending them is silently ignored. That is deliberate, not a missing feature:

  • slug — it is part of the public path /tools/<slug>. Changing it breaks every existing link and every indexed search result.
  • status — hiding or publishing a tool is a business decision, made in the admin panel where the warning lives.
  • kind, is_free, free_max_devices — classification and pricing policy, also admin-only.

The user guide

The guide field is the content of /tools/<slug>/guide. Write Markdown; the table of contents is generated from your headings, so structure the page with headings rather than bold text.

Supported: headings, lists, code blocks, bold, italic, links, tables, quotes.

Raw HTML in Markdown is escaped, not executed — even for content you wrote yourself, because a dev key can leak and a page that can run scripts is a page that can read a customer session cookie.

App icon

The icon is the first thing anyone sees on the product page and in the catalogue. Without one the system shows the initials of the name — workable, but it reads as an unfilled box.

Upload a file

curl -s -X POST https://cyanpro.app/api/v1/dev/icon \
  -H "X-Dev-Token: $CYAN_DEV_TOKEN" \
  -F "file=@icon-512.png"
201 Created
{ "ok": true, "icon_url": "/media/3b7e…a1.png", "uploaded": true }

Or point at an existing image

POST https://cyanpro.app/api/v1/dev/icon
X-Dev-Token: cyd_live_xxxxxxxx
Content-Type: application/json

{ "url": "https://cdn.example.com/icon-512.png" }

Remove the icon

DELETE https://cyanpro.app/api/v1/dev/icon
X-Dev-Token: cyd_live_xxxxxxxx
  • Square, at least 256px per side. It renders at 88px on the product page and 48px in the catalogue, so anything smaller is upscaled and goes soft.
  • Transparent PNG or a solid background both work — the 20px rounded frame crops to shape.
  • Setting a new icon replaces the old one. The old file is cleaned up unless it is also one of the screenshots.
  • Same file rules as screenshots: PNG, JPEG, WebP, GIF, up to 3MB, type determined from the contents rather than the name.

Cover image

The wide banner at the top of the product page, and the tool thumbnail in the catalogue. Unlike a screenshot this is artwork made for the job — name, breathing room — so it survives any crop.

Upload a file

curl -s -X POST https://cyanpro.app/api/v1/dev/cover \
  -H "X-Dev-Token: $CYAN_DEV_TOKEN" \
  -F "file=@cover-1024x500.png"
201 Created
{ "ok": true, "cover_url": "/media/8d12…7c.png", "uploaded": true }

Remove the cover

DELETE https://cyanpro.app/api/v1/dev/cover
X-Dev-Token: cyd_live_xxxxxxxx
  • Export at exactly 1920×640 (3:1). Other ratios are accepted but get cropped to fit, because the frame ratio is fixed — letting the frame follow the image gives every product a different height.
  • Keep text and logo inside the central 1140px. The frame narrows with the viewport (3:1 → 2:1 → 16:9) and always crops inward from the sides, so the centre is the only region guaranteed to survive.
  • Without one the product page has no banner, and the catalogue borrows the first screenshot.

Screenshots

Screenshots appear on the tool detail page, and the first one becomes the cover image in the catalogue. Two ways to add one: upload the file, or point at an image on your own CDN.

Upload a file

curl -s -X POST https://cyanpro.app/api/v1/dev/images \
  -H "X-Dev-Token: $CYAN_DEV_TOKEN" \
  -F "file=@screenshot-1.png" \
  -F "caption=Màn hình chính" \
  -F "caption_en=Main window" \
  -F "sort_order=1"

Point at an external image

POST https://cyanpro.app/api/v1/dev/images
X-Dev-Token: cyd_live_xxxxxxxx
Content-Type: application/json

{
  "url": "https://cdn.example.com/shots/main.png",
  "caption": "Màn hình chính",
  "caption_en": "Main window",
  "sort_order": 1
}
201 Created
{
  "ok": true,
  "image": {
    "id": "img_7f3a…",
    "url": "/media/9c1f…c4.png",
    "caption": "Màn hình chính",
    "width": 1920,
    "height": 1200,
    "bytes": 184320,
    "sort_order": 1
  }
}
  • PNG, JPEG, WebP and GIF only. The type is determined from the file contents, not from the extension or the Content-Type you declare.
  • Up to 3MB per file and 12 images per tool.
  • External images must be https.
  • Use sort_order to control the order; lower numbers come first.
  • Captions should say what the image shows — they double as alt text for screen-reader users.
DELETE https://cyanpro.app/api/v1/dev/images/img_7f3a…
X-Dev-Token: cyd_live_xxxxxxxx

Delete by the id from GET /api/v1/dev/images. A dev key can only delete images belonging to its own tool.

Publishing a version

Call this from your build system once the installer is packaged and uploaded. Each version number can be published only once.

FieldTypeNotes
version string Required. Letters, digits and . - + only, and must contain at least one digit. Up to 40 characters.
notes string Release notes, up to 20000 characters. Your software shows these when it reports an update.
notes_en string English release notes.
download_url string Download link for this build. Must be https. Empty falls back to the tool default.
is_mandatory boolean true if this update is mandatory. Your software should block use until it is applied — reserve it for security fixes or protocol changes.
201 Created
{
  "ok": true,
  "release": {
    "version": "1.1.0",
    "is_latest": true,
    "is_mandatory": false,
    "download_url": "https://example.com/setup-1.1.0.exe",
    "released_at": 1792470000
  }
}

Versions are compared numerically

Each numeric segment is compared as a number, so 1.10.0 is newer than 1.9.0 (plain string comparison gets this backwards). Pre-releases sort below the release: 1.0.0-beta.1 < 1.0.0. Anything after + is ignored.

Publishing something OLDER than the current version is still recorded in history but does not move the pointer back: you get is_latest: false plus a note explaining why. That way a patch for an old branch never invites customers on a newer build to downgrade.

Release history

GET https://cyanpro.app/api/v1/dev/releases
X-Dev-Token: cyd_live_xxxxxxxx

How a release reaches customers

  1. You call POST /api/v1/dev/releases. If the version is newer than the current one, the tool latest_version moves immediately.
  2. The installed build calls GET /api/v1/version?current=<running version> with its own X-Api-Key — the only release-related endpoint a client key may call.
  3. The response carries update_available, latest_version, download_url, notes and is_mandatory.
  4. There is no push channel: the client asks. Call it at startup, and every few hours for software that stays open.

Wiring it into your build

A GitHub Actions example: after packaging, publish the release using a key from secrets. Do not print the key — curl takes it from an environment variable rather than the command line.

# .github/workflows/release.yml
- name: Phát hành lên cyanpro.app
  env:
    CYAN_DEV_TOKEN: ${{ secrets.CYAN_DEV_TOKEN }}
    VERSION: ${{ github.ref_name }}
  run: |
    curl -sS --fail-with-body -X POST https://cyanpro.app/api/v1/dev/releases \
      -H "X-Dev-Token: $CYAN_DEV_TOKEN" \
      -H "Content-Type: application/json" \
      -d "$(jq -n \
            --arg v "${VERSION#v}" \
            --arg notes "$(cat CHANGELOG-latest.md)" \
            --arg url "https://downloads.example.com/setup-${VERSION#v}.exe" \
            '{version: $v, notes: $notes, download_url: $url}')"

The command above exits non-zero when the server returns an error, thanks to --fail-with-body: a failed publish should turn the whole build red instead of passing in silence.

Error codes

Every error response has the same shape: { "ok": false, "error": "code", "message": "..." }. Branch on error, never on message — the wording can change, the code cannot.

CodeHTTPMeaning and what to do
missing_dev_token 401 Missing X-Dev-Token. Note that a client API key does not work on this branch.
invalid_dev_token 401 Wrong or revoked key. Check the secret in your build system.
product_not_found 404 The tool this key belonged to was deleted. Create a key for the current tool.
invalid_json 400 The request body is not valid JSON.
nothing_to_update 400 No valid field in the body. Check the field names.
invalid_name 400 The tool name was empty.
invalid_platforms 400 platforms must be an array of strings.
invalid_download_url 400 The download link must be a full https URL.
missing_version 400 The version field is missing.
invalid_version 400 The version string is not valid.
version_exists 409 That version was already published. Use a new number — never republish over one.
image_too_large 413 The image is over 3MB. Compress it before uploading.
not_an_image 400 The file is not one of the accepted image types.
invalid_icon_url 400 The icon URL must be https.
invalid_cover_url 400 The cover URL must be https.
too_many_images 409 This tool already has the maximum number of images. Delete one first.
image_not_found 404 No image with that id belongs to this tool.
rate_limited 429 Too many calls. Wait the number of seconds in the Retry-After header.

Rate limit

120 calls per hour per developer key. Over the limit you get HTTP 429 with a Retry-After header in seconds. The limit is deliberately low because publishing is not something you do in a loop; if your build system hits it, it is almost certainly retrying because of another bug.

Before your first release

  • The dev key lives in build-system secrets — not in source, not in the shipped build.
  • The download link is https and downloads from outside your network.
  • The version number is higher than the current latest_version.
  • Release notes are written for end users, not a list of commits.
  • You called GET /api/v1/version with a client API key and saw the version you just published.

Client integration docs →