WECP — WarpEngine Catalog Protocol: the read side of the WarpEngine API, which the WarpEngine Client, the warpstore engines and the site talk to. JSON, camelCase keys, snake_case query parameters. Every response carries a
WarpEngine-Versionheader.Authorization: Bearer …is optional everywhere. A 404 on any endpoint but/api/softwaremeans an older engine without that feature.
type Timestamp = string // ISO 8601 UTC, "2026-07-31T16:57:28.186Z"; missing = "0001-01-01T00:00:00Z"
type Path = string // server-relative, "/file/…" or "/api/image/7"; prefix with the catalog's base URL
Since 0.5. What this server is, and how to sign in. auth is null where there is no sign-in.
interface Service {
engine: "warp_engine"
version: string // engine version, same as the WarpEngine-Version header
api: {
version: number // shape version, the `api` field of every envelope (2)
catalog: {
pageSize: number // per_page when the reader does not say
maxPageSize: number // the most a reader may ask for
}
}
catalog: {
gated: boolean // can any title here require an entitlement
}
auth: null | {
schemes: ["bearer"]
device: { // device authorization grant, RFC 8628
authorizeUrl: string // POST here to start a sign-in
tokenUrl: string // POST here to poll for the token
revokeUrl: string | null // DELETE here to sign out
verificationUrl: string // where a person types the user code
interval: number // seconds between polls
}
}
}
The catalog. Without page the whole catalog comes in one envelope; with page one page of it.
interface SoftwareQuery {
page?: number // from 1; absent = everything
per_page?: number // capped by maxPageSize
status?: string // comma-separated status keys
platform?: string // one platform name
category?: string // one category key
q?: string // matched against title and name
owner_id?: number // one publisher's softwares
sort?: "title" | "updated" | "newest"
}
interface Catalog {
api: number // 2; absent on engines before 0.13 (plain list)
page: number
perPage: number
total: number // softwares matching the filters, all pages
totalPages: number
nextPage: number | null // follow until null
softwares: Entry[]
}
interface Entry {
software: Software
releases: Release[] // newest first
latestRelease: Release | null // newest whose version is not "dev-…"
webPlayableRelease: Release | null // latestRelease when it has an "html" asset
totalDownloads: number
access: Access // since 0.5, present even under the open policy
}
interface Software {
id: number
ownerId: number
name: string // the key of the title
title: string
author: string
desc: string // short description
story: string // long description
license: string
platform: string // slug: "c64", "tic80", "godot", …; labels in /api/builds
status: string // key from /api/software/vocabulary
category: string | null // since 0.12; key from /api/software/vocabulary
highlighted: boolean
imageUrl: Path | null // default box art
images: { url: Path, isDefault: boolean, position: number }[]
externalLinks: { id: number, softwareId: number, label: string, url: string }[]
platformLinks: { name: string, url: string }[]
createdAt: Timestamp
updatedAt: Timestamp
deletedAt: Timestamp | null
}
interface Release {
id: number
softwareId: number
version: string // "1.0.0", or "dev-1.0.0-branch" off the main branch
assets: Asset[] // what a store installs from
cartridgePath: Path | "" // legacy flat fields, the same paths as the matching assets
sourcePath: Path | ""
htmlFolderPath: Path | ""
docsFolderPath: Path | ""
downloadCount: number
createdAt: Timestamp
updatedAt: Timestamp
deletedAt: Timestamp | null
}
interface Asset {
kind: "html" | "win_x86" | "win_x64" | "linux_x64" | "linux_arm64"
| "mac_universal" | "mac_x64" | "mac_arm64" | "cartridge" | "source" | "docs"
path: Path // hand it to GET /api/download?path=
}
interface Access {
gated: boolean // this title can require an entitlement
entitled: boolean | null // true: may download; false: may not; null: anonymous caller
price: { amountCents: number, currency: string } | null
purchaseUrl: string | null
webUrl: string | null
}
Since 0.13. Same filters as /api/software. Each count is what the value would show given the other filters.
interface Facets {
api: number
total: number // matching every filter
statuses: Record<string, number> // status key → count
platforms: Record<string, number> // platform → count
categories: Record<string, number> // category key → count; "" for no category
}
Since 0.13. The statuses and categories the catalog uses, in display order.
interface Vocabulary {
api: number
statuses: {
key: string // the value a software's status holds
label: string
position: number
defaultVisible: boolean // listed by a store with no preference of its own
badge: string | null // style hint
}[]
categories: {
key: string // the value a software's category holds
label: string
position: number
count: number // visible softwares carrying it
}[]
}
The highlighted title, or null.
type Highlighted = Entry | null
Platform labels and the asset kinds each platform expects.
interface Builds {
platforms: Record<string, { // platform slug →
label: string // "TIC-80", "Plus/4", "LÖVE"
kinds: Asset["kind"][] // what a release on this platform may carry
}>
allKinds: Asset["kind"][]
}
Asset coverage of one title, per release.
interface SoftwareBuilds {
platform: string
expected: Asset["kind"][]
releases: Record<string, { // version →
actual: Asset["kind"][]
missing: Asset["kind"][]
}>
}
The file at path (an Asset.path), as an attachment, and a logged download. 302 to the storage URL when storage is not local — do not send the bearer token to that host. 400 blank path, 403 refused by the access policy, 404 not found.
The same files inline, without a download log: web-playable builds, docs, box art. 301 when storage is not local, 403 when refused.
Since 0.5, at auth.device.authorizeUrl. Starts a sign-in. 404 where /api/service says auth: null.
interface DeviceRequest {
client_name?: string // what to call this device in the person's account
}
interface DeviceCode {
deviceCode: string // poll with this
userCode: string // show this, "WXYZ-1234"
verificationUrl: string // send the person here
interval: number // seconds between polls; wins over the descriptor's
expiresIn: number // seconds
}
At auth.device.tokenUrl. Poll until the state settles. 404 or 410: the grant is gone, start again.
interface TokenRequest {
device_code: string
}
interface TokenPoll {
state: "pending" | "approved" | "denied" | "expired"
token?: string // present exactly once, on the poll that finds it approved
}
At auth.device.revokeUrl. Revokes the bearer token on the request. 204 revoked, 401 no token.