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.
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.
https://cyanpro.app
X-Dev-Token header
Endpoints
| Method | Path | Purpose |
|---|---|---|
| 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.
| Field | Type | Notes |
|---|---|---|
| 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.
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-Typeyou declare. - Up to 3MB per file and 12 images per tool.
- External images must be https.
- Use
sort_orderto 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.
| Field | Type | Notes |
|---|---|---|
| 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
- You call
POST /api/v1/dev/releases. If the version is newer than the current one, the toollatest_versionmoves immediately. - The installed build calls
GET /api/v1/version?current=<running version>with its ownX-Api-Key— the only release-related endpoint a client key may call. - The response carries
update_available,latest_version,download_url,notesandis_mandatory. - 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.
| Code | HTTP | Meaning 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/versionwith a client API key and saw the version you just published.