Self Help
Version Route
Version Route adds releases and licensing to your website. Your GitLab CI build goes to your website privately. Your software asks your website whether a newer version is out. You sell your software in your own store, every buyer gets a license, and a license is checked before any paid download.
Versioning and updates
Send every build from GitLab CI to your own website. Your software can then ask your website whether a newer version is out.
GitLab CI example
Add these two files to your project. Set the two variables, push a version tag, and your build is on your website.
1. In GitLab, open Settings > CI/CD > Variables and add:
| Variable | Value |
|---|---|
VOVO_SITE |
Your website, such as https://example.com |
VOVO_API_KEY |
An API key with the hub:write scope. Mark it masked. |
2. Add .gitlab-ci.yml:
stages:
- build
- release
variables:
PRODUCT_KEY: my-plugin
build:
stage: build
script:
- mkdir -p dist
- zip -r "dist/my-plugin-$CI_COMMIT_TAG.zip" src README.md
artifacts:
paths:
- dist/
expire_in: 1 day
rules:
- if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/'
send_to_website:
stage: release
needs:
- job: build
artifacts: true
script:
- sh ci/send-to-website.sh "dist/my-plugin-$CI_COMMIT_TAG.zip"
rules:
- if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/'
3. Add ci/send-to-website.sh:
#!/bin/sh
# Sends one built file to your website's File Hub, then publishes the release.
# Needs curl, jq, split and sha256sum on the runner.
set -eu
FILE="$1"
NAME=$(basename "$FILE")
SIZE=$(wc -c < "$FILE" | tr -d ' ')
SHA=$(sha256sum "$FILE" | cut -d ' ' -f 1)
VERSION=${CI_COMMIT_TAG#v}
AGENT="$PRODUCT_KEY-ci/$VERSION"
HUB="$VOVO_SITE/api/v1/hub/uploads"
SOFTWARE="$VOVO_SITE/api/modules/software/v1"
PIECES=$(mktemp -d)
call() {
curl -fsS -A "$AGENT" -H "Authorization: Bearer $VOVO_API_KEY" "$@"
}
# Start a private upload.
UPLOAD=$(printf '{"filename":"%s","size":%s,"sha256":"%s","visibility":"private"}' "$NAME" "$SIZE" "$SHA" \
| call -H 'Content-Type: application/json' -d @- "$HUB")
UPLOAD_ID=$(echo "$UPLOAD" | jq -r .id)
CHUNK_SIZE=$(echo "$UPLOAD" | jq -r .chunk_size)
# Send it in pieces, in the order your website asked for.
split -b "$CHUNK_SIZE" -a 4 "$FILE" "$PIECES/piece."
INDEX=0
for PIECE in "$PIECES"/piece.*; do
call -T "$PIECE" -H 'Content-Type: application/octet-stream' "$HUB/$UPLOAD_ID/chunks/$INDEX" > /dev/null
INDEX=$((INDEX + 1))
done
# Finish, and read what your website saved.
DONE=$(call -X POST "$HUB/$UPLOAD_ID/complete")
FILE_ID=$(echo "$DONE" | jq -r .file_id)
SAVED_SHA=$(echo "$DONE" | jq -r .sha256)
if [ "$SAVED_SHA" != "$SHA" ]; then
echo "The website saved different bytes than this job built." >&2
exit 1
fi
# Publish the release.
printf '{"version":"%s","commit":"%s","notes":"%s","files":[{"file_id":%s,"kind":"app","needs_license":true}]}' \
"$VERSION" "$CI_COMMIT_SHA" "See the changelog." "$FILE_ID" \
| call -H 'Content-Type: application/json' -d @- "$SOFTWARE/products/$PRODUCT_KEY/releases"
Push a tag:
git tag v1.2.0
git push origin v1.2.0
When the job is green, open Version Route > Products in your admin. The new version is listed.
Good to know
- Send your own User-Agent. It makes your calls easy to recognize in your logs, and the server refuses some default client names, such as Go's.
- Run it again safely. Sending the same bytes for the same version does nothing. Sending different bytes for a version already published is refused. Change the version instead.
- It is private. Builds sent this way are kept privately in your File Hub. Do not upload a paid build through the File Hub page. Anyone with the address can open a file added there.
- Size. A build can be up to 500 MB for an archive. Builds count toward your website's storage.
Set up
- You need a verified developer account. This is the same account check as all VoVo developer tools. If you are not verified when you go to buy Version Route, you are taken through verification first, then brought back to the purchase. You are not charged before that.
- Buy Version Route for your website in the Marketplace. It runs on VoVo, so there is nothing to download, and it installs itself on that website. If the install does not finish, you are told. Open My Purchases and choose Enable for this website to try again.
- In your admin, open Version Route > Products and add a product. Its key, such as
my-plugin, goes in your CI file and your software. It cannot change later. - Create an API key with the
hub:writescope.
Check for a new version
No key is needed, so a free copy can check too.
GET https://example.com/api/modules/software/v1/products/my-plugin/latest?current=1.1.0
Answer:
{
"product": "my-plugin",
"update_available": true,
"version": "1.2.0",
"released_at": "2026-11-04T18:00:00Z",
"notes": "See the changelog.",
"files": [
{
"name": "my-plugin-v1.2.0.zip",
"kind": "app",
"size": 48211200,
"sha256": "9f2c...",
"needs_license": true
}
]
}
Optional filters: channel (default stable), platform and engine.
GET .../products/my-plugin/releases lists past versions, newest first.
Check when your software starts and no more than once a day. Do not check on every action.
Download an update
A free file can be downloaded by anyone. A file with needs_license: true needs a license. See Store and licensing.
Always compare the file's SHA-256 with the answer above before installing.
Publishing more than one file
Upload each file, then list them all in one release:
{
"version": "1.2.0",
"commit": "a1b2c3d...",
"notes": "Fixes a crash on startup.",
"engine": "5.8",
"platforms": ["Win64"],
"files": [
{"file_id": 512, "kind": "plugin", "platform": "Win64", "needs_license": true},
{"file_id": 513, "kind": "server", "needs_license": false}
]
}
| Field | Needed | Meaning |
|---|---|---|
version |
Yes | Three numbers, such as 1.2.0 |
files |
Yes | The files you uploaded |
kind |
Yes, on each file | What the file is, such as plugin, app or server. It sets the download link's words on the buyer's page. See What buyers get. |
needs_license |
Yes, on each file | true for paid files, false for free ones |
channel |
No | stable by default. Use beta for a test group. |
notes |
No | Shown to your buyers |
commit, engine, platforms, released_at |
No | Kept with the release and returned by latest |
known_hashes |
No | SHA-256 of your program files. See Tamper flags. |
Withdraw a bad release
Open the product in your admin and choose Withdraw on the release. It stops being offered and downloaded, and latest falls back to the version before it. Nothing is deleted. To replace it, publish a new version.
Errors
| Answer | Meaning | What to do |
|---|---|---|
401 |
The key is wrong or missing | Check VOVO_API_KEY |
403 |
The key lacks hub:write, belongs to another account, or its owner is not a verified developer |
Use a hub:write key from a verified developer on this website's account |
404 |
Unknown product, or the module is not on this website | Check PRODUCT_KEY and VOVO_SITE |
422 file_not_private |
A file in the release was not sent to this website through the upload calls | Upload it with the calls above, never through the File Hub page |
409 version_exists |
That version has different bytes | Use a new version |
413 |
The file or a piece is too large | Use the chunk_size your website gave you |
Store and licensing
Sell your software in your own store. Every buyer gets a license. Your software checks that license on your own website before any download is given.
How it works
- A buyer pays in your store.
- They get a license key by email. The key and the download also show under My licenses in their account, and on that order's page under Purchases. They download their first copy from either place. Both need a signed-in account with a verified email, and the account's email must be the one on the order.
- They enter the key in your software.
- Your software calls your website. If the license is good, it gets a download.
Every address is on your own domain.
Set up a product for sale
1. Take payments. Your store needs a connected payment account. Sales go to your account, less the platform fee.
2. Create a store product for each way you sell. For example, one for 3 months, one for 12 months and one for lifetime. Set the type to Digital and set your price.
Each Digital product must have a download file. Attach a short Getting started PDF, not your software. Your software stays private in Version Route, where each download is checked. A file attached to a store product stays with that order and goes out of date.
3. Add a plan for each store product. Open Version Route > Products, choose your product and add a plan:
| Setting | Choices |
|---|---|
| Store product | The product you just made |
| Length | A number of months, or lifetime |
4. Choose what happens when a license ends. Set this once per product:
| Setting | What happens |
|---|---|
| Downloads stop | Your software keeps running. The buyer cannot download anything. |
| Software stops | Downloads stop, and your software is told to stop. Your software must do this itself. See Stopping your software. |
If you choose Software stops, also set Offline days: how long a paid-up copy keeps working with no connection.
5. Publish your license terms. Add your terms to the store product with AgreeRoute, so each buyer accepts them before paying. If your software stops when a license ends, say so plainly on the store page and in your terms.
What buyers get
| Situation | Result |
|---|---|
| Buys for the first time | A new key by email |
| Buys again with time left | New time is added to the time they have. The key stays the same. |
| Buys again after it ended | The same key works again, starting from the purchase |
| Buys a lifetime plan | The end date is removed |
| Time runs out | All downloads stop, including older versions |
| Full refund | That order's time comes off the license. A license with nothing paid left is cancelled at once. |
| Chargeback | The license is cancelled at once |
Both places show the same table: the software, the key with a Copy button, its status, its end date, the computer in use and the downloads. The newest stable version is offered. A file of kind plugin or app reads Download plugin or Download app. Any other file, or a second file of the same kind, shows its file name.
One license works on one computer and is locked to it. To move to a new computer, a buyer frees the old one from My licenses, up to 3 times in 12 months, so a new PC does not need a support ticket. You can free one for them at any time.
Under Version Route > Licenses you can search licenses, issue one by hand, restore one, and see which computers are in use.
Cancel a license
Open the license and choose Cancel license. Give a reason. The action is logged. Downloads stop at once, and the buyer's copy is told at its next check. A copy that stays offline hears nothing until its lease runs out.
Add licensing to your software
All calls go to your own website:
https://example.com/api/modules/software/v1
Send the key in a header. Never put a key in an address.
Authorization: Bearer LIC-XXXXX-XXXXX-XXXXX-XXXXX
User-Agent: my-plugin/1.2.0
Content-Type: application/json
Send your own User-Agent, so your software is easy to recognize in your logs. The server refuses some default client names, such as Go's.
Activate
Call this when a buyer enters their key. It locks the license to this computer.
machine_id must stay the same on one computer and must not identify the person. Hash a machine id with your product key, and never send the raw value.
curl -fsS -A "my-plugin/1.2.0" -H "Authorization: Bearer $LICENSE_KEY" -H "Content-Type: application/json" -d '{"product":"my-plugin","machine_id":"3f1c...","machine_name":"Studio PC"}' https://example.com/api/modules/software/v1/licenses/activate
Answer:
{
"status": "active",
"plan": "Until Nov 4, 2027",
"ends_at": "2027-11-04T18:00:00Z",
"moves_left": 3,
"when_ended": "software_stops",
"lease": "eyJ2Ijox...",
"lease_expires_at": "2026-11-11T18:00:00Z"
}
ends_at is null for a lifetime license, and plan then says Lifetime. moves_left is how many more moves this buyer has in the current 12 months.
Check
Call /licenses/check with the same body when your software starts, and once a day. It returns the same answer and a fresh lease. It does not use a move.
You can also send file_hash, the SHA-256 of your running program. See Tamper flags.
Do not make a person wait for this call. Run it in the background.
Free a computer
Call /licenses/deactivate with the same body to free this computer. It uses one of the buyer's moves.
Download
Ask for a link, then fetch it. The link lasts 10 minutes and can be used again in that time, so an interrupted download can resume.
curl -fsS -A "my-plugin/1.2.0" -H "Authorization: Bearer $LICENSE_KEY" -H "Content-Type: application/json" -d '{"product":"my-plugin","version":"1.2.0","file":"my-plugin-v1.2.0.zip","machine_id":"3f1c..."}' https://example.com/api/modules/software/v1/downloads
Answer:
{
"url": "https://example.com/api/modules/software/v1/downloads/8Qz1...",
"expires_at": "2026-11-04T18:10:00Z",
"size": 48211200,
"sha256": "9f2c..."
}
Fetch the url with no key. Compare the SHA-256 before installing.
Answers when it fails
Show message to the person. Use error in your code.
| Answer | error |
Meaning |
|---|---|---|
401 |
license_invalid |
The key is wrong, or for another product |
403 |
license_expired |
Time ran out. renew_url is included. |
403 |
license_cancelled |
Refunded, charged back or cancelled by you |
403 |
other_computer_active |
This license is on another computer. manage_url is included. |
403 |
move_limit_reached |
No moves left. Send the buyer to support. |
403 |
computer_not_activated |
Call activate first |
404 |
file_not_found |
That version or file is not available |
410 |
link_expired |
The download link ended, or the license stopped. Ask for a new link. |
422 |
invalid_machine_id |
Send a machine_id of 16 to 128 letters and numbers |
429 |
too_many_requests |
Wait and try again |
429 |
report_limit_reached |
This license has sent its reports for today |
Do not treat a network failure as a bad license. A paid buyer on a plane has done nothing wrong. Use the lease.
The lease
A lease lets a paid-up copy run with no connection. It is a signed note from your website saying this license is good until a set time.
base64url(payload) + "." + base64url(signature)
The payload is JSON:
| Field | Meaning |
|---|---|
v |
Format version, 1 |
kid |
Which signing key was used |
lic |
The license's public id |
prd |
Your product key |
mch |
The machine_id |
sts |
active |
iat |
When it was issued, in Unix time |
exp |
When it stops being valid, in Unix time |
To check a lease in your software:
- Split the lease at the dot and decode both parts. Verify the Ed25519 signature over the decoded JSON bytes with your public key. It is on the product page in your admin. Build it into your software.
- Check that
prdandmchmatch. - Check that the time now is before
exp.
exp is never later than the license's end date, so a copy stops on time even with no connection.
Stopping your software
Only if you chose Software stops.
Your website does not reach into a buyer's computer. It tells your software the license has ended, and your software must act. A good stop:
- Never crashes, and never blocks anything else from opening.
- Never touches the buyer's own work.
- Says clearly why it stopped, and gives a link to renew.
- Starts working again as soon as a new lease arrives.
Be honest about what this does. If you ship source code or an unprotected app, a determined person can remove your check. The check on your website is the real gate: nobody downloads without a license.
Tamper flags
A flag tells you something looks wrong. A flag never cancels a license on its own, because a false match would shut off someone who paid.
A flag is raised when file_hash is not in the known_hashes of any release, or when a license keeps being tried on other computers. Flags show on the license and on any bug report from it. If you agree it is a problem, cancel the license yourself. Say in your terms whether that gets a refund.
Bug reports
Your software can file a support ticket on your website. It sends the report with the license key. Your website checks the license and adds the buyer's name and email from their order, so your software never holds them and never holds a VoVo token.
curl -fsS -A "my-plugin/1.2.0" -H "Authorization: Bearer $LICENSE_KEY" -H "Content-Type: application/json" -d '{"product":"my-plugin","machine_id":"3f1c...","version":"1.2.0","signature":"crash-7a41","summary":"Crash while saving","details":"Stack and log text"}' https://example.com/api/modules/software/v1/reports
Answer:
{
"ticket": 1042,
"repeat": false,
"count": 1
}
signature identifies the bug. Build it from the call stack or the error. One signature on one license makes one ticket, and repeats add to its count. Each license has a daily cap. The buyer gets one email for each new bug and none for repeats.
Never send keys, passwords or your buyer's files. Clean log text before sending it, and keep a report under 10 MB.
Automatic reports are on by default. Show a notice on first run, give the buyer a setting that turns them off, and say so on your privacy page.
Your buyers' data
Version Route keeps each buyer's email, their license, the computers they use, a record of downloads and their bug reports. Download records are removed after 90 days. These are part of your website's customer export. Mention them in your privacy page.
Need help?
Open Get Help on Version Route in your admin to reach the support ticket center.