---
title: WECP
url: 'https://wiki.teletypegames.org/engines/warp-engine/wecp'
markdown: 'https://wiki.teletypegames.org/engines/warp-engine/wecp.md'
lang: en
date: '2026-09-10'
description: '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-Version header. Authorization: Bearer … is optional everywhere. A…'
taxonomy:
  tag:
    - engine
    - reference
---

# WECP

> **WECP — WarpEngine Catalog Protocol**: the read side of the [WarpEngine](https://wiki.teletypegames.org/engines/warp-engine) API, which the [WarpEngine Client](https://wiki.teletypegames.org/stores/warp-engine-client), the [warpstore](https://wiki.teletypegames.org/engines/warpstore) engines and the site talk to. JSON, camelCase keys, snake\_case query parameters. Every response carries a `WarpEngine-Version` header. `Authorization: Bearer …` is optional everywhere. A 404 on any endpoint but `/api/software` means an older engine without that feature.

```ts
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
```

# GET /api/service

Since 0.5. What this server is, and how to sign in. `auth` is null where there is no sign-in.

```ts
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
    }
  }
  admin: null | {                 // since 1.0; null while the host has it off
    version: number               // Admin API version (1)
    url: string                   // its base address
    openapiUrl: string            // its OpenAPI 3.1 document
    mcpUrl: string | null         // its MCP endpoint, null while MCP is off
    schemes: ["bearer"]
  }
  shelves: Shelf[]                // since 1.7; empty where the host keeps no shelf
}

interface Shelf {
  platform: string                // whose titles are on it
  kinds: string[]                 // the asset kinds it holds
  layout: "flat" | "folders"      // one file a title, or one folder a title (INFO.INI + FILES.LST)
  index: string                   // the index file in its folder: "INDEX.TXT"
  urls: string[]                  // where the host serves the folder (ftp, http, https)
}
```

`shelves` (since 1.7) says where the [shelves](https://wiki.teletypegames.org/engines/warp-engine) are: folders for clients that cannot read this API, which the host serves over FTP or plain HTTP. The engine only writes them; the addresses are the host's. A client for a machine that cannot speak HTTPS cannot ask this endpoint, so it keeps its own default address.

`admin` points at the [Admin API](https://wiki.teletypegames.org/engines/warp-engine/admin-api), which is not part of WECP: a catalog reader needs none of it, and everything there wants an admin token. WarpEngine 1.0 changed nothing else a reader sees — the envelope is still `api: 2`.

# GET /api/software

The catalog. Without `page` the whole catalog comes in one envelope; with `page` one page of it.

```ts
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
}
```

# GET /api/software/facets

Since 0.13. Same filters as `/api/software`. Each count is what the value would show given the other filters.

```ts
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
}
```

# GET /api/software/vocabulary

Since 0.13. The statuses and categories the catalog uses, in display order.

```ts
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
  }[]
}
```

# GET /api/software/highlighted

The highlighted title, or `null`.

```ts
type Highlighted = Entry | null
```

# GET /api/builds

Platform labels and the asset kinds each platform expects.

```ts
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"][]
}
```

# GET /api/softwares/:name/builds

Asset coverage of one title, per release.

```ts
interface SoftwareBuilds {
  platform: string
  expected: Asset["kind"][]
  releases: Record<string, {      // version →
    actual: Asset["kind"][]
    missing: Asset["kind"][]
  }>
}
```

# GET /api/download?path=

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.

# GET /file/\*path

The same files inline, without a download log: web-playable builds, docs, box art. 301 when storage is not local, 403 when refused.

# POST /api/auth/device

Since 0.5, at `auth.device.authorizeUrl`. Starts a sign-in. 404 where `/api/service` says `auth: null`.

```ts
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
}
```

# POST /api/auth/device/token

At `auth.device.tokenUrl`. Poll until the state settles. 404 or 410: the grant is gone, start again.

```ts
interface TokenRequest {
  device_code: string
}

interface TokenPoll {
  state: "pending" | "approved" | "denied" | "expired"
  token?: string                  // present exactly once, on the poll that finds it approved
}
```

# DELETE /api/auth/token

At `auth.device.revokeUrl`. Revokes the bearer token on the request. 204 revoked, 401 no token.

'; var prevLevel = 2; for (var i = 0; i < headings.length; i++) { var h = headings\[i\]; var text = h.textContent.trim(); var slug = text.toLowerCase() .replace(/\[áàâä\]/g,'a').replace(/\[éèêë\]/g,'e').replace(/\[íìîï\]/g,'i') .replace(/\[óòôöő\]/g,'o').replace(/\[úùûüű\]/g,'u') .replace(/\[^a-z0-9\]+/g, '-').replace(/^-|-$/g, ''); if (!slug) slug = 'heading-' + i; if (used\[slug\]) { slug += '-' + i; } used\[slug\] = true; h.id = slug; var level = parseInt(h.tagName.charAt(1)); var cls = level > 2 ? ' class="toc-child"' : ''; html += '- [' + text + '](#'%20+%20slug%20+%20')
'; } html += '
'; toc.innerHTML = html; // Scroll-spy: highlight current heading var links = toc.querySelectorAll('a'); var ids = \[\]; for (var j = 0; j < links.length; j++) ids.push(links\[j\].getAttribute('href').slice(1)); function updateActive() { var current = ''; for (var k = 0; k < ids.length; k++) { var el = document.getElementById(ids\[k\]); if (el && el.getBoundingClientRect().top <= 80) current = ids\[k\]; } for (var m = 0; m < links.length; m++) { links\[m\].parentNode.classList.toggle('active', links\[m\].getAttribute('href') === '#' + current); } } window.addEventListener('scroll', updateActive, { passive: true }); updateActive(); })();

---

## Navigation

- Parent: [WarpEngine](https://wiki.teletypegames.org/engines/warp-engine.md)
- Previous: [WarpEngine Admin API](https://wiki.teletypegames.org/engines/warp-engine/admin-api.md)
