REST API
The REST API publishes, lists and manages spaces on a creator's behalf. Every endpoint answers JSON. An error answers with a code in error, except the hosting endpoints, whose error is a sentence and code the code. The calls the Velven page makes for a space, such as sign-in and saves, are not part of it.
Authentication
Send your API token in the authorization header as Bearer <token>. The examples on these pages keep it in $VELVEN_TOKEN. An agent gets one through the device-code sign-in, which the creator approves in their browser; the token lasts 90 days. A 401 unauthorized means it is missing, expired or revoked: sign in again.
curl -s https://velven.ai/api/agent/me \ -H "authorization: Bearer $VELVEN_TOKEN"A token works only on its own creator's spaces. For another creator's space, the board endpoints answer 404.
Endpoints
Every endpoint, with the credential it takes and a link to its details:
| Endpoint | Auth | What it does |
|---|---|---|
POST /api/agent/login | None | Starts a login; answers a device_code and a link for the creator. Details |
POST /api/agent/token | None | Exchanges the device_code for a token once the creator approves. Details |
GET /api/agent/me | Token | The creator the token belongs to. |
POST /api/spaces | Token | Lists a space. Details |
GET /api/spaces?mine=1 | Token | The creator's listed spaces. Details |
POST /api/spaces/verify | Token | Claims a space Velven listed. Details |
GET, PATCH /api/spaces/{slug} | Token | Reads or changes a space's fields and its page. Details |
POST /api/spaces/{slug}/move | Token | Moves a space to a new URL. Details |
POST /api/spaces/recapture | Token | Asks for a new clip. Details |
POST /api/v1/hosting/versions | Token or none | Starts a version of a hosted space: its files and velven.json. Details |
POST /api/v1/hosting/versions/{id}/finish | Token or claim | Makes the uploaded version a preview, or sends it to the safety check. Details |
GET /api/v1/hosting/versions/{id} | Token or claim | A version's status. Details |
GET /api/v1/hosting/claim | Claim | The unlisted page a claim token opens, and when it is deleted: { pageUrl, expiresAt }. Details |
GET /api/v1/hosting/spaces/{slug}/versions | Token | A hosted space's versions. Details |
POST /api/v1/hosting/spaces/{slug}/rollback | Token | Puts an earlier version live again. Details |
POST /api/v1/hosting/versions/{id}/preview | Token | A new 24-hour preview link. Details |
POST /api/v1/hosting/versions/{id}/review | Token | Asks a person at Velven to review a refused or unjudged version. Details |
POST /api/v1/hosting/spaces/{slug}/reset | Token | Deletes the space's sandbox data. Details |
GET, PUT /api/spaces/{slug}/boards | Token | Reads or writes the space's boards. Details |
GET, POST /api/spaces/{slug}/secret | Token | The board secret's status, or a new secret. Details |
GET, PUT /api/spaces/{slug}/achievements | Token | Reads or writes the space's stats and achievements. Details |
DELETE /api/spaces/{slug}/scores/{id} | Token | Deletes one submission. Details |
GET, POST, DELETE /api/spaces/{slug}/bans | Token | Lists, adds or lifts bans. Details |
POST /api/v1/scores | Secret | A space's own server posts a score. Details |
POST /api/v1/achievements, POST /api/v1/stats | Secret | A space's own server unlocks an achievement or sets a stat. Details |
GET /.well-known/jwks.json | None | The public keys player tokens are signed with. |
- Endpoint
POST /api/agent/login- Auth
- None
- What it does
- Starts a login; answers a
device_codeand a link for the creator. Details
- Endpoint
POST /api/agent/token- Auth
- None
- What it does
- Exchanges the
device_codefor a token once the creator approves. Details
- Endpoint
GET /api/agent/me- Auth
- Token
- What it does
- The creator the token belongs to.
- Endpoint
POST /api/spaces- Auth
- Token
- What it does
- Lists a space. Details
- Endpoint
GET /api/spaces?mine=1- Auth
- Token
- What it does
- The creator's listed spaces. Details
- Endpoint
POST /api/spaces/verify- Auth
- Token
- What it does
- Claims a space Velven listed. Details
- Endpoint
GET,PATCH /api/spaces/{slug}- Auth
- Token
- What it does
- Reads or changes a space's fields and its page. Details
- Endpoint
POST /api/spaces/{slug}/move- Auth
- Token
- What it does
- Moves a space to a new URL. Details
- Endpoint
POST /api/spaces/recapture- Auth
- Token
- What it does
- Asks for a new clip. Details
- Endpoint
POST /api/v1/hosting/versions- Auth
- Token or none
- What it does
- Starts a version of a hosted space: its files and
velven.json. Details
- Endpoint
POST /api/v1/hosting/versions/{id}/finish- Auth
- Token or claim
- What it does
- Makes the uploaded version a preview, or sends it to the safety check. Details
- Endpoint
GET /api/v1/hosting/versions/{id}- Auth
- Token or claim
- What it does
- A version's status. Details
- Endpoint
GET /api/v1/hosting/claim- Auth
- Claim
- What it does
- The unlisted page a claim token opens, and when it is deleted:
{ pageUrl, expiresAt }. Details
- Endpoint
GET /api/v1/hosting/spaces/{slug}/versions- Auth
- Token
- What it does
- A hosted space's versions. Details
- Endpoint
POST /api/v1/hosting/spaces/{slug}/rollback- Auth
- Token
- What it does
- Puts an earlier version live again. Details
- Endpoint
POST /api/v1/hosting/versions/{id}/preview- Auth
- Token
- What it does
- A new 24-hour preview link. Details
- Endpoint
POST /api/v1/hosting/versions/{id}/review- Auth
- Token
- What it does
- Asks a person at Velven to review a refused or unjudged version. Details
- Endpoint
POST /api/v1/hosting/spaces/{slug}/reset- Auth
- Token
- What it does
- Deletes the space's sandbox data. Details
- Endpoint
GET,PUT /api/spaces/{slug}/boards- Auth
- Token
- What it does
- Reads or writes the space's boards. Details
- Endpoint
GET,POST /api/spaces/{slug}/secret- Auth
- Token
- What it does
- The board secret's status, or a new secret. Details
- Endpoint
GET,PUT /api/spaces/{slug}/achievements- Auth
- Token
- What it does
- Reads or writes the space's stats and achievements. Details
- Endpoint
DELETE /api/spaces/{slug}/scores/{id}- Auth
- Token
- What it does
- Deletes one submission. Details
- Endpoint
GET,POST,DELETE /api/spaces/{slug}/bans- Auth
- Token
- What it does
- Lists, adds or lifts bans. Details
- Endpoint
POST /api/v1/scores- Auth
- Secret
- What it does
- A space's own server posts a score. Details
- Endpoint
POST /api/v1/achievements,POST /api/v1/stats- Auth
- Secret
- What it does
- A space's own server unlocks an achievement or sets a stat. Details
- Endpoint
GET /.well-known/jwks.json- Auth
- None
- What it does
- The public keys player tokens are signed with.
Move a space
Points the space at a new URL and keeps everything else. The new page passes the checks a listing does and, on a claimed space, carries the proof tag naming its owner. The owner may move a space, and an admin any space; each space gets 6 tries in 10 minutes. Move to another host says what to change on the new host.
curl -s -X POST https://velven.ai/api/spaces/<slug>/move \ -H "authorization: Bearer $VELVEN_TOKEN" \ -H "content-type: application/json" \ -d '{"url":"https://orbit-dodger.vercel.app"}'| Status | Body | Meaning |
|---|---|---|
| 200 | The space's fields, velven_url and previous_url | Moved. The fields are those GET /api/spaces/{slug} answers, with the new url. |
| 401 | { "error": "unauthorized" } | The API token is missing, expired or revoked. Sign in again. |
| 403 | { "error": "blocked", "message" } | The new URL cannot be listed on Velven. |
| 404 | { "error": "not_found", "message" } | No space with that slug on this account. |
| 409 | { "error": "unverified", "message", "instruction", "snippet" } | The new page does not carry the proof tag naming the space's owner. Add snippet to its head, deploy, and call again. |
| 409 | { "error": "duplicate", "url", "message" } | Another space on Velven already uses that URL, before or after its redirects; url is its page. |
| 409 | { "error": "same_url", "message" } | The new URL is the space's own. Nothing to move. |
| 409 | { "error": "removed", "message" } | The space is off Velven. Put it back first. |
| 409 | { "error": "held", "message" } | The space is under review. It can move once the review is done. |
| 409 | { "error": "conflict", "message" } | The space changed while it was being moved. Read it again, then retry. |
| 422 | { "error": "invalid", "issues" } | The body was not JSON or not { url }, the URL is not a web address, or Velven does not list its host. |
| 422 | { "error": "unreachable", "message", "instruction" } | The new page did not answer with a page, or asks visitors to sign in (a locked preview answers 401): make it public, or use the production URL. instruction comes only with a locked page. |
| 422 | { "error": "unframeable", "message", "instruction", "snippet" } | The new page's headers refuse frames. Make the change that instruction describes (the steps for each host), deploy, and call again. |
| 429 | { "error": "rate_limited", "message" } | The space has had its 6 tries in 10 minutes. Wait a few minutes; do not retry in a loop. |
- Status
- 200
- Body
- The space's fields,
velven_urlandprevious_url - Meaning
- Moved. The fields are those
GET /api/spaces/{slug}answers, with the newurl.
- Status
- 401
- Body
{ "error": "unauthorized" }- Meaning
- The API token is missing, expired or revoked. Sign in again.
- Status
- 403
- Body
{ "error": "blocked", "message" }- Meaning
- The new URL cannot be listed on Velven.
- Status
- 404
- Body
{ "error": "not_found", "message" }- Meaning
- No space with that slug on this account.
- Status
- 409
- Body
{ "error": "unverified", "message", "instruction", "snippet" }- Meaning
- The new page does not carry the proof tag naming the space's owner. Add
snippetto its head, deploy, and call again.
- Status
- 409
- Body
{ "error": "duplicate", "url", "message" }- Meaning
- Another space on Velven already uses that URL, before or after its redirects;
urlis its page.
- Status
- 409
- Body
{ "error": "same_url", "message" }- Meaning
- The new URL is the space's own. Nothing to move.
- Status
- 409
- Body
{ "error": "removed", "message" }- Meaning
- The space is off Velven. Put it back first.
- Status
- 409
- Body
{ "error": "held", "message" }- Meaning
- The space is under review. It can move once the review is done.
- Status
- 409
- Body
{ "error": "conflict", "message" }- Meaning
- The space changed while it was being moved. Read it again, then retry.
- Status
- 422
- Body
{ "error": "invalid", "issues" }- Meaning
- The body was not JSON or not
{ url }, the URL is not a web address, or Velven does not list its host.
- Status
- 422
- Body
{ "error": "unreachable", "message", "instruction" }- Meaning
- The new page did not answer with a page, or asks visitors to sign in (a locked preview answers 401): make it public, or use the production URL.
instructioncomes only with a locked page.
- Status
- 422
- Body
{ "error": "unframeable", "message", "instruction", "snippet" }- Meaning
- The new page's headers refuse frames. Make the change that
instructiondescribes (the steps for each host), deploy, and call again.
- Status
- 429
- Body
{ "error": "rate_limited", "message" }- Meaning
- The space has had its 6 tries in 10 minutes. Wait a few minutes; do not retry in a loop.
Boards
GET answers { "boards": [...] }, every board as it stands. PUT takes the same shape as the page's boards block and writes only the boards named; the page's next sync overwrites a key the page also names.
curl -s https://velven.ai/api/spaces/<slug>/boards \ -H "authorization: Bearer $VELVEN_TOKEN"curl -s -X PUT https://velven.ai/api/spaces/<slug>/boards \ -H "authorization: Bearer $VELVEN_TOKEN" \ -H "content-type: application/json" \ -d '{"boards":[{"key":"main","trust":"client","metric":"points","sort":"desc"}]}'| Status | Body | Meaning |
|---|---|---|
| 200 | { "boards": [...] } | The boards after the write. |
| 401 | { "error": "unauthorized" } | The API token is missing, expired or revoked. Sign in again. |
| 404 | { "error": "not_found", "message" } | No space with that slug on this account. |
| 409 | { "error": "too_many", "message" } | The write would take the space past its board limit. |
| 422 | { "error": "invalid", "issues" } | A board did not validate; issues names the field. |
| 503 | { "error": "failed", "message" } | The boards could not be saved just now. Try again. |
- Status
- 200
- Body
{ "boards": [...] }- Meaning
- The boards after the write.
- Status
- 401
- Body
{ "error": "unauthorized" }- Meaning
- The API token is missing, expired or revoked. Sign in again.
- Status
- 404
- Body
{ "error": "not_found", "message" }- Meaning
- No space with that slug on this account.
- Status
- 409
- Body
{ "error": "too_many", "message" }- Meaning
- The write would take the space past its board limit.
- Status
- 422
- Body
{ "error": "invalid", "issues" }- Meaning
- A board did not validate;
issuesnames the field.
- Status
- 503
- Body
{ "error": "failed", "message" }- Meaning
- The boards could not be saved just now. Try again.
Hosting
The calls the Velven CLI makes, for a tool of your own that publishes. A version is made in 3 calls: create it with the file list, upload the files Velven lacks, then finish it. The files are named by their SHA-256, so a file Velven already has is never sent again. Hosting has the rules.
curl -s -X POST https://velven.ai/api/v1/hosting/versions \ -H "authorization: Bearer $VELVEN_TOKEN" \ -H "content-type: application/json" \ -d '{"space":"star-hop","fields":{"title":"Star Hop","space_type":"game","devices":["desktop"]}, "files":[{"path":"index.html","size":1843,"sha256":"<hex>"}]}'It answers 201 { version: { id, number }, space: { slug, url }, uploads: [{ sha256, url }], limits }. PUT each file's bytes to its upload url, with no authorization header; a link lasts 2 hours. Then finish:
curl -s -X POST https://velven.ai/api/v1/hosting/versions/<id>/finish \ -H "authorization: Bearer $VELVEN_TOKEN" \ -H "content-type: application/json" \ -d '{"preview":false}'- The create body:
space(a slug of yours, left out for a new space),fields(the listing, named asspaceFieldsSchemanames them:title,space_type,devices,description,engine,ai_tools,models,how_made,source_url),files(1 to 2,000 of{ path, size, sha256 }; avelven.json, a dotfile, a dot-folder ornode_modules, in any folder and any case, is refused, naming the path, since none is ever published), andvelven.json'sentry,spa,sdk,start,toasts,boards,achievementsandstatsas they are. - Finish takes
{ preview },trueby default: a preview answers{ status: "preview", previewUrl, watchUrl };falseanswers{ status: "checking", watchUrl }and the version goes live when the check passes. Finishing twice answers as the first did; a file not yet uploaded answers 409missing_uploads; a version made before the space moved to a live URL answers 409not_hostedforfalse, since it can only be previewed. GET /api/v1/hosting/versions/{id}answers{ id, number, status, reason?, shownOnTake?, pageUrl, previewUrl?, createdAt, liveAt?, reviewRequested, stopped? }. Poll it every few seconds for the verdict:live,refusedorunjudged.stopped: trueon acheckingversion means its check ended without a verdict (the space was hidden, held or moved, or its checks are over the hourly limit) and none is queued: stop polling and publish again.GET .../spaces/{slug}/versionsanswers the last 100 as{ versions: [{ id, number, status, createdAt, live, files, size, reason?, liveAt?, rollback, leftBehind }] }(leftBehind: made before the space moved to a live URL, so it can only be previewed).POST .../rollback { version }answers{ version: { number }, status: "live", pageUrl }, ornot_allowedfor a version that was never live, andheldorremovedwhile the space is off Velven.POST .../versions/{id}/reviewanswers{ requestedAt }, once per refused or unjudged version, and never for one older than the live version (superseded).POST .../spaces/{slug}/reset { categories?, player? }answers{ deleted }, the categories beingscores,saves,achievements,contentandrooms.- Without a token, create makes an unlisted page and adds
pageUrl,expiresAtand, the first time,claimToken. Send the token asclaimin later create and finish bodies, and as thex-velven-claimheader on reads. Finish withpreview: falseanswers 403sign_in_required.
| Code | Meaning |
|---|---|
missing_fields, invalid_fields | The listing lacks a required field or has a bad one; fields names them. |
invalid_request, invalid_files, invalid_declaration | The body, a file path, a file's size (it must be the file's real bytes) or a board, stat or achievement did not validate. |
no_entry | No file at entry, index.html by default. |
too_large, too_many_files | Over the version limits; the largest files are named. |
too_many | velven.json and what the achievements or boards API declared would take the space past 50 stats, 50 achievements or 10 boards. Declare fewer in velven.json. |
not_owner, not_found, removed, held | Not your space, no such space or version, or the space is off Velven. |
rate_limited | Too many publishes; Retry-After says when to try again. |
checks_busy | 429 on finish: too many of your versions are already waiting for the safety check. Nothing is lost: finish again after the seconds Retry-After gives. |
sign_in_required, claim_not_found, claim_expired, claim_no_profile, claim_banned | Production needs an account; the claim token is wrong, used or past its 7 days; the account claiming has no handle yet, or cannot claim. |
claim_spent | 409 without an account, on a create, a finish, a version read or GET /api/v1/hosting/claim: the claim token's page was claimed (during the check, say). Its versions go on under that account; sign in as it and publish again. |
unauthorized, no_handle, forbidden | The token does not resolve (run velven login again); the account has no handle yet; the account cannot publish. |
cross_site | A publish without an account that another site's page sent. Publish from Velven itself or the CLI. |
not_allowed, not_hosted, not_previewable, not_refused, superseded, space_unavailable, no_player | A rollback to a version never live, or on a space now at a live URL, or to a version from before the space moved to a live URL; a review, or a finish for production, of a version uploaded before the space moved to a live URL; a preview of a version not uploaded or refused; a review of a version neither refused nor unjudged; a review of a version older than the one live, which a review could not take live (publish a new version instead); a preview while the space is held, removed or hidden; a reset for a handle with no player. |
missing_uploads | Finish before every file was uploaded; the paths are named. |
hosting_unavailable, server_error | Velven's file store did not answer, or Velven had a problem on its side. Try again in a moment. |
- Code
missing_fields,invalid_fields- Meaning
- The listing lacks a required field or has a bad one;
fieldsnames them.
- Code
invalid_request,invalid_files,invalid_declaration- Meaning
- The body, a file path, a file's
size(it must be the file's real bytes) or a board, stat or achievement did not validate.
- Code
no_entry- Meaning
- No file at
entry,index.htmlby default.
- Code
too_large,too_many_files- Meaning
- Over the version limits; the largest files are named.
- Code
too_many- Meaning
velven.jsonand what the achievements or boards API declared would take the space past 50 stats, 50 achievements or 10 boards. Declare fewer invelven.json.
- Code
not_owner,not_found,removed,held- Meaning
- Not your space, no such space or version, or the space is off Velven.
- Code
rate_limited- Meaning
- Too many publishes;
Retry-Aftersays when to try again.
- Code
checks_busy- Meaning
- 429 on finish: too many of your versions are already waiting for the safety check. Nothing is lost: finish again after the seconds
Retry-Aftergives.
- Code
sign_in_required,claim_not_found,claim_expired,claim_no_profile,claim_banned- Meaning
- Production needs an account; the claim token is wrong, used or past its 7 days; the account claiming has no handle yet, or cannot claim.
- Code
claim_spent- Meaning
- 409 without an account, on a create, a finish, a version read or
GET /api/v1/hosting/claim: the claim token's page was claimed (during the check, say). Its versions go on under that account; sign in as it and publish again.
- Code
unauthorized,no_handle,forbidden- Meaning
- The token does not resolve (run velven login again); the account has no handle yet; the account cannot publish.
- Code
cross_site- Meaning
- A publish without an account that another site's page sent. Publish from Velven itself or the CLI.
- Code
not_allowed,not_hosted,not_previewable,not_refused,superseded,space_unavailable,no_player- Meaning
- A rollback to a version never live, or on a space now at a live URL, or to a version from before the space moved to a live URL; a review, or a finish for production, of a version uploaded before the space moved to a live URL; a preview of a version not uploaded or refused; a review of a version neither refused nor unjudged; a review of a version older than the one live, which a review could not take live (publish a new version instead); a preview while the space is held, removed or hidden; a reset for a handle with no player.
- Code
missing_uploads- Meaning
- Finish before every file was uploaded; the paths are named.
- Code
hosting_unavailable,server_error- Meaning
- Velven's file store did not answer, or Velven had a problem on its side. Try again in a moment.
Achievements
GET answers the space's stats and achievements. PUT takes { "stats": [...], "achievements": [...] } in the shape of the page's block, writes only the keys named, stats first since a trigger names one, and answers the declarations after the write. An icon here is an https URL. 422 invalid with issues for a field that does not validate, 422 unknown_stat for a trigger on a stat the space does not declare, 409 too_many when the write would take the space past 50 stats or 50 achievements, 503 failed when they could not be saved just now (try again).
curl -s -X PUT https://velven.ai/api/spaces/<slug>/achievements \ -H "authorization: Bearer $VELVEN_TOKEN" \ -H "content-type: application/json" \ -d '{"stats":[{"key":"kills","label":"Kills"}],"achievements":[{"key":"centurion","title":"Centurion","trigger":{"stat":"kills","atLeast":100}}]}'Board secret
A server board takes posts only with the space's secret. POST issues one and answers 201 with { "secret", "message" }; the secret is shown this once, only its hash is kept, and issuing again replaces it. GET answers { "secret": { "created_at", "rotated_at" } }, or { "secret": null } before the first, never the secret itself.
curl -s -X POST https://velven.ai/api/spaces/<slug>/secret \ -H "authorization: Bearer $VELVEN_TOKEN"curl -s https://velven.ai/api/spaces/<slug>/secret \ -H "authorization: Bearer $VELVEN_TOKEN"Moderation
Delete one submission; the player is re-ranked from what is left. Or ban a player, by handle, from every board of the space, and lift the ban later.
No endpoint lists submissions yet. The latest are on the Leaderboards tab of the space's edit page, each with Remove.
# delete one submissioncurl -s -X DELETE https://velven.ai/api/spaces/<slug>/scores/<id> \ -H "authorization: Bearer $VELVEN_TOKEN"# list, add and lift banscurl -s https://velven.ai/api/spaces/<slug>/bans \ -H "authorization: Bearer $VELVEN_TOKEN"curl -s -X POST https://velven.ai/api/spaces/<slug>/bans \ -H "authorization: Bearer $VELVEN_TOKEN" \ -H "content-type: application/json" -d '{"handle":"@mara"}'curl -s -X DELETE https://velven.ai/api/spaces/<slug>/bans \ -H "authorization: Bearer $VELVEN_TOKEN" \ -H "content-type: application/json" -d '{"handle":"@mara"}'| Status | Body | Meaning |
|---|---|---|
| 200 | { "deleted": true } or { "banned", "handle" } | Done. |
| 400 | { "error": "invalid" } | The submission id is not a positive integer. |
| 404 | not_found or no_such_player | No such submission on this space's boards, or no account has that handle. |
| 422 | { "error": "invalid", "issues" } | The body was not { handle }, or it named you. |
- Status
- 200
- Body
{ "deleted": true }or{ "banned", "handle" }- Meaning
- Done.
- Status
- 400
- Body
{ "error": "invalid" }- Meaning
- The submission id is not a positive integer.
- Status
- 404
- Body
not_foundorno_such_player- Meaning
- No such submission on this space's boards, or no account has that handle.
- Status
- 422
- Body
{ "error": "invalid", "issues" }- Meaning
- The body was not
{ handle }, or it named you.