Plattform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Modelle Tools & Services
Lösungen
Organisationen Entwickler Internet Service Provider Managed Service Provider AI-in-a-Box
Ressourcen
Support Documentation Blog Downloads
Unternehmen
Über uns Forschung Karriere Investieren Kontakt
Anmelden

Downloads API

POST /api/v1/downloads/{product} and friends let a product team's CI pipeline ship SDK artifacts to the ScaiLabs /downloads page using the same dev-<product> API key that publishes their docs. The downloads scope (downloads:{read,write,delete}:<product>) was added to every existing dev key on 2026-06-14, so no key rotation or extra setup is needed.

Prefer the official SDKs over raw curl. Each SDK has a Downloads.publish() method that handles MIME inference, multipart form-data, retry, and typed errors — see Use an official SDK below. The raw cURL pipeline still works and is documented further down.

How a release is modelled#

A release is one row in the download content type on the ScaiLabs site:

Field Type Notes
product text Required. The product slug, e.g. scaigrid. The scope check binds the row to this.
version text Required. Semver recommended (1.2.3, 2.0.0-rc1).
language text Optional. python, typescript, dotnet, go, rust, java, csharp, curl, etc. Used as a badge, as the per-language colour on the /downloads page, and as the "latest" grouping key (latest is per (product, language) pair).
file asset Required. Uploaded to S3 under /sdks/<product>/.
file_size number Server-set from the uploaded file.
sha256 text Optional. Server stores it; the page surfaces it for verification.
release_notes markdown Rendered in the SDK card on /downloads.
is_latest boolean Defaults to true; on publish, prior (product, language) releases are flipped to false (but stay listed under "older versions").
published_at datetime Server-set on create.

The download content type exists on ScaiLabs only. To add publishing on another site, seed the same content type there (slug download) with the field set above before issuing scopes.

Use an official SDK#

Three first-party SDKs ship with full downloads support. Each one has identical resource shape — see the SDKs section on /downloads for current artifacts.

Python#

bash
1
pip install scaicms
python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from scaicms import ScaiCMS

with ScaiCMS(
    base_url="https://www.scailabs.ai",
    api_key="scai_live_…",
    site_id="30847722-3fae-477a-b7ce-34e6fce390fe",
) as cms, open("dist/scaigrid-1.2.3-py3-none-any.whl", "rb") as f:
    rel = cms.downloads.publish(
        product="scaigrid", file=f, version="1.2.3",
        language="python", release_notes="- Bug fixes",
    )
print(rel.file_url)

CLI shortcut (pip install scaicms[cli]):

bash
1
2
scaicms downloads publish scaigrid dist/scaigrid-1.2.3-py3-none-any.whl \
    --version 1.2.3 --language python

TypeScript / JavaScript#

bash
1
npm install @scaicms/client
ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { ScaiCMS } from "@scaicms/client";
import { readFile } from "node:fs/promises";

const cms = new ScaiCMS({
  baseUrl: "https://www.scailabs.ai",
  apiKey: process.env.SCAICMS_API_KEY,
  siteId: process.env.SCAICMS_SITE_ID,
});

const buf = await readFile("./dist/scaigrid-1.2.3.tgz");
const rel = await cms.downloads.publish({
  product: "scaigrid",
  file: { data: buf, filename: "scaigrid-1.2.3.tgz" },
  version: "1.2.3", language: "typescript",
  releaseNotes: "- Bug fixes",
});

.NET#

bash
1
dotnet add package ScaiCMS
csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
using ScaiCMS;

using var cms = new ScaiCmsClient(new ScaiCmsOptions
{
    BaseUrl = "https://www.scailabs.ai",
    ApiKey = Environment.GetEnvironmentVariable("SCAICMS_API_KEY"),
    SiteId = Environment.GetEnvironmentVariable("SCAICMS_SITE_ID"),
});

await using var fs = File.OpenRead("./dist/ScaiGrid.1.2.3.nupkg");
var rel = await cms.Downloads.PublishAsync(
    product: "scaigrid",
    file: fs,
    filename: "ScaiGrid.1.2.3.nupkg",
    version: "1.2.3",
    language: "dotnet",
    releaseNotes: "- Bug fixes");

All three SDKs map 403 SCOPE_DENIED to a typed PermissionError (Python), PermissionError (TS), or PermissionException (.NET) so your pipeline can react cleanly. The Python and .NET SDKs are sync-by-default with async variants; the TypeScript SDK is promise-only.

Endpoints#

POST /api/v1/downloads/{product}#

Publish a new release. Multipart body.

Form field Required Notes
file yes The artifact. Accepted MIME (from the global asset allowlist): application/zip, application/x-tar, application/gzip, application/x-gzip, application/java-archive. The endpoint additionally infers MIME from the filename when the client sends application/octet-stream (covers .zip, .whl, .nupkg, .jar, .war, .tar, .gz, .tgz, .tar.gz). Up to 100 MB by default.
version yes Free text.
language no Free text.
title no Display name. Default "<Product> <language> <version>".
release_notes no Markdown.
sha256 no Optional checksum.
is_latest no Defaults to true. Set false if back-publishing an old release.

Required headers: Authorization: Bearer <key>, X-Site-ID: <scailabs-site>.

Required scope: downloads:write:<product> (or a wildcard such as downloads:* for a global key).

Returns 201 with the created release shape:

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "id": "8bc7…",
  "product": "scaigrid",
  "version": "2.0.0",
  "language": "python",
  "title": "Scaigrid python 2.0.0",
  "file_url": "/media/sites/<site>/assets/2026/06/<uuid>/scaigrid-2.0.0-py3-none-any.whl",
  "file_size": 1247831,
  "sha256": null,
  "release_notes": "- Bug fixes",
  "is_latest": true,
  "published_at": "2026-06-14T15:06:28.125559",
  "slug": "scaigrid-python-2-0-0",
  "status": "published"
}

Side effects:

  • A new asset row is created with folder=/sdks/<product>/.
  • If is_latest=true, all prior (product, language) rows are flipped to is_latest=false — they stay listed under "older versions" on the public page but lose the "latest" tag.

GET /api/v1/downloads/{product}#

List releases. Query parameters:

Param Notes
language=<x> Filter to one language/platform.
latest_only=true Only rows with is_latest=true.

Required scope: downloads:read:<product>.

Returns {"items": [...], "total": N} with each item shaped like the POST response. Order: newest first.

PATCH /api/v1/downloads/{product}/{release_id}#

In-place edit of release metadata. JSON body — any field omitted is preserved as-is. Only these fields are mutable:

Field Notes
title Display name on the public page.
release_notes Markdown.
sha256 Optional checksum.
is_latest When set to true, demotes any other release for the same (product, language) pair — same behaviour as the publish path.

The artifact (file) and immutable identifiers (product, version, language, slug, file_size) are deliberately not editable here: republish if you need to change those.

Required scope: downloads:write:<product>. Returns the updated release shape on 200.

bash
1
2
3
4
5
curl -X PATCH "https://www.scailabs.ai/api/v1/downloads/scaigrid/<release_id>" \
  -H "Authorization: Bearer $DOCS_TOKEN" \
  -H "X-Site-ID: 30847722-3fae-477a-b7ce-34e6fce390fe" \
  -H "Content-Type: application/json" \
  -d '{"title": "Cleaner name", "release_notes": "- Fix typo"}'

DELETE /api/v1/downloads/{product}/{release_id}#

By default a soft delete: the row is flagged is_deleted=1 and disappears from listings and the public /downloads page. The asset stays in S3 — it's referenced by storage_key and can still be served from /media/... for anyone who has the link.

The slug is freed immediately on soft-delete (as of the schema fix on 2026-06-15). You can republish the same (product, language, version) without further cleanup — the soft-deleted audit row stays in the DB but its slug is excluded from the unique constraint via a generated column.

SLUG_TAKEN (409) only fires when an actively-published release already holds the slug — i.e. you're trying to publish v1.0.0/python twice without deleting the first one. The response is:

json
1
2
3
4
5
6
7
{
  "detail": {
    "code": "SLUG_TAKEN",
    "message": "A published release with slug 'scaigrid-python-1-0-0' already exists. Delete it first (DELETE /api/v1/downloads/scaigrid/<release_id>) or publish with a different version.",
    "slug": "scaigrid-python-1-0-0"
  }
}

?hard=true — purge the audit row too#

The default soft-delete preserves the row for audit. Pass ?hard=true to fully remove it (the row and its content_fields):

bash
1
2
3
curl -X DELETE "https://www.scailabs.ai/api/v1/downloads/scaigrid/<release_id>?hard=true" \
  -H "Authorization: Bearer $DOCS_TOKEN" \
  -H "X-Site-ID: 30847722-3fae-477a-b7ce-34e6fce390fe"

?hard=true works on already-soft-deleted rows too, so soft → hard two-step cleanup is supported.

Required scope: downloads:delete:<product>. Returns 204 on success.

Scope shape and enforcement#

Scopes follow the same {resource}:{action}:{product} shape as docs:

gdscript
1
2
3
downloads:read:scaigrid
downloads:write:scaigrid
downloads:delete:scaigrid

Wildcards work the same way too — downloads:* is the equivalent of "all download actions, any product." Sub-products inherit when the group's docs scopes cover them: dev-scaigrid holds the matching downloads:read:scaibot, downloads:write:scaibot, downloads:delete:scaibot on top of the scaigrid set, because that's how its docs scopes were arranged. (The three actions are stored as three separate entries rather than a single downloads:*:scaibot — they're equivalent at check time but the literal form is the per-action one.)

If a key tries to publish for a product it doesn't cover, the endpoint returns 403:

json
1
2
3
4
5
6
{
  "detail": {
    "code": "SCOPE_DENIED",
    "message": "API key scope does not permit 'write' on downloads for product 'scaicore'"
  }
}

How releases appear on the public page#

/downloads (on ScaiLabs) renders an sdk_downloads block that groups releases by product, then by language. The newest release per (product, language) is the featured card, with the language's canonical colour, a tag like PY/TS/.NET/GO, the version, download CTA, and the release notes you supplied. Prior releases collapse into a "N older versions" disclosure below the card.

Nothing on the publishing side controls this layout — you write release metadata, the template pack handles the visual.

Cheatsheet — the raw cURL pipeline#

If you can't use the SDK (CI environment without Python/Node/.NET, or you're calling from a shell script):

bash
1
2
3
4
5
6
7
8
curl -X POST "https://www.scailabs.ai/api/v1/downloads/scaicms" \
  -H "Authorization: Bearer $DOCS_TOKEN" \
  -H "X-Site-ID: 30847722-3fae-477a-b7ce-34e6fce390fe" \
  -F "file=@dist/scaicms-1.2.3-py3-none-any.whl" \
  -F "version=1.2.3" \
  -F "language=python" \
  --form-string "release_notes=- Bug fixes" \
  -F "sha256=$(sha256sum dist/scaicms-1.2.3-py3-none-any.whl | awk '{print $1}')"

Three things teams trip on:

  • cURL's @ is special in form fields. A title or release_notes that starts with @ makes cURL read it as a filename. Use --form-string title="@latest beta" (or -F "title=<dash>" with the value piped in) instead of -F for any value that might begin with @.
  • Use the www host. The bare apex scailabs.ai 301-redirects to www, and most HTTP clients drop the multipart body when following a 301 on POST. Set DOCS_HOST=https://www.scailabs.ai and you're fine. (The SDKs default to the right host.)
  • X-Site-ID is required — the management API is global, the download content type lives on ScaiLabs. Without the header you get a 404 on the content-type lookup.

See also#

  • API keys — how scopes get evaluated.
  • Assets — where the uploaded files physically live (S3 + the assets table + the /media/ proxy).
  • The handover bundle's api-key.md — your team's specific key + the cURL smoke test. The bundle's product-team-handover.md §12 carries the team-facing how-to for publishing.
Updated 2026-06-20 20:46:27 View source (.md) rev 4