---
title: "Krowk upload API — declare, upload, finalize"
canonical_url: "https://krowk.com/docs/api"
last_updated: "2026-09-02T08:08:07.681Z"
meta:
  description: "The wire reference: three calls to upload a file, the keyless read, the error envelope, idempotency keys, and the size, expiry and rate limits the registry actually enforces."
  "og:description": "The wire reference: three calls to upload a file, the keyless read, the error envelope, idempotency keys, and the size, expiry and rate limits the registry actually enforces."
  "og:title": "Krowk upload API — declare, upload, finalize"
---

**Docs**

# **The upload API**

Three calls: declare, upload, finalize. Bytes go straight to object storage, so curl is a supported path and the CLI is a convenience.

[The CLI reference](https://krowk.com/docs/cli) [See the pricing](https://krowk.com/pricing)

**api.krowk.com/v1 · every write scoped by key, reads by slug.**

The CLI speaks a documented REST API at `api.krowk.com/v1`, so nothing has to be installed. Bytes never pass through the application: the registry hands out a presigned URL, the client uploads straight to object storage, and a third call confirms what landed.

## 1. Declare

terminal

```
curl -s https://api.krowk.com/v1/artifacts \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "filename": "screenshot.png",
    "content_type": "image/png",
    "byte_size": 421888,
    "checksum": "<hex sha-256 of the file>"
  }'
```

The response carries the artifact's slug and a presigned `upload.url` with the exact headers to send. Size and digest are declared up front because they are signed into that URL — which is how storage refuses an oversized or corrupted body at the edge rather than accepting it and leaving the application to notice later. It is also why a client reads the whole file to digest it before the first call.

`Idempotency-Key` is how a retry says "this is the same attempt" and gets back the record the first try made. Without it, eleven retries of one push leave ten artifacts behind to expire. For a keyless caller the key is a credential — it is the only thing a retry can present to prove it made the original call — so a guessable one from a shared address hands the declare to whoever guesses it.

## 2. Upload

terminal

```
curl -X PUT --upload-file screenshot.png \
  -H "Content-Type: image/png" \
  "<upload.url from the declare response>"
```

## 3. Finalize

terminal

```
curl -s -X PUT \
  https://api.krowk.com/v1/artifacts/art_00000000000000000000demo/finalization
```

Idempotent, which is why it is a PUT: a retry gets the same success and the record keeps the moment it first reached that state.

## The rest of the surface

- `GET /` — the service descriptor: which service this is and which API versions it serves. What a reachability probe reads.
- `GET /v1/artifacts` — list, newest first ( `--limit`, `--before` on the CLI side). Needs a key.
- `GET /v1/artifacts/:slug` — read one back, with the run metadata embedded. Works with no key: the slug is the capability.
- `DELETE /v1/artifacts/:slug` — take an upload down. Immediate, unrecoverable, and it purges the edge. Destroying the record is what the verb already means, so this is not a nested resource.
- `POST /v1/artifacts/:slug/claim` — spend a one-shot claim token. Needs a key.
- `PUT /v1/artifacts/:slug/run` — put an artifact under a run after it was uploaded.
- `POST /v1/runs` and `PUT /v1/runs/:slug/completion` — open and close a run. Both need a key; closing is idempotent.
- `GET /v1/runs`, `GET /v1/runs/:slug` and `GET /v1/runs/:slug/artifacts` — browse runs and what is under one. A run's artifacts are a collection of the run rather than a filter on the listing, so it is its own path.
- `GET /v1/key` — which key this is and the workspace it acts in.
- `POST /v1/cli/authorizations` and `GET /v1/cli/authorizations/:slug` — open a browser login and collect the key it mints. Keyless; the slug is the capability and the read is one-shot. The one create that takes no idempotency key, because a lost response means the code was never seen.

Authenticate with `Authorization: Bearer krowk_sk_…` to land the artifact in your workspace. Without it the upload is anonymous, the declare response includes a claim token, and the link expires 24 hours later.

A read of a single artifact ignores workspace scope entirely, key or no key; writes do not. Practically: treat the URL as the credential, and send no key on a read you would otherwise have to keep secret.

Why the read is unscoped is on [the security page](https://krowk.com/security).

## Errors

Every failure from either side has one shape, with a machine-readable code and the actual offending value — so a client, or a model, can act on it without parsing prose:

response

```
{
  "error": {
    "code": "invalid",
    "message": "Byte size is too large",
    "details": { "byte_size": ["must be at most 104857600"] }
  }
}
```

## Limits and behaviour to expect

- **Artifact ceiling.** 100 MB on the free tier, 2 GB on Pro. The cap lives in the registry, so a client digests the file and only then hears it is too large.
- **Expiry.** Keyless uploads expire 24 hours after upload and then answer 410 Gone carrying the original filename and upload time. The bytes wait in cold storage 30 more days and a claim brings the link back.
- **Rate ceiling.** 10 creates a minute per address without a key, 120 per key. Past it, 429 with Retry-After. On by default and not switchable off.
- **Free daily quota.** 100 uploads a day, counted per workspace. Its 429 carries a Retry-After naming the UTC-midnight reset.
- **Identical bytes do not dedupe.** Pushing the same file twice as two attempts creates two artifacts with two links. Identity is a random slug, not a digest — a digest-derived id would make an anonymous link guessable.
- **No X-RateLimit-Remaining.** A client cannot see a ceiling approaching, only hit it. That is a gap rather than a design choice.

The enforcement numbers above are the registry's, not the marketing copy's. Metered overage past the included Pro amounts is published at $1 per 1,000 uploads and $0.05 per GB-month, and billing for it is not wired up — going over costs nothing today. There is no spend cap; the per-minute ceiling slows a bill rather than stopping one.

## **Nothing here needs the CLI.**

Three HTTP calls and an error envelope with one shape. The client is MIT if you would rather read it than trust this page.

[github.com/krowkcom/cli](https://github.com/krowkcom/cli) [The CLI reference](https://krowk.com/docs/cli)