# API reference

> POST /api/upload, DELETE /api/delete, and GET /f/:slug (via rewrite): methods, headers, status codes, JSON response shape, and streaming behavior.

- Repository: MaxLeiter/vcup
- GitHub: https://github.com/MaxLeiter/vcup
- Human docs: https://grok-wiki.com/public/docs/maxleiter-vcup-05c16ae77ebb
- Complete Markdown: https://grok-wiki.com/public/docs/maxleiter-vcup-05c16ae77ebb/llms-full.txt

## Source Files

- `api/upload.ts`
- `api/delete.ts`
- `api/f.ts`
- `vercel.json`
- `cli.ts`

---

---
title: "API reference"
description: "POST /api/upload, DELETE /api/delete, and GET /f/:slug (via rewrite): methods, headers, status codes, JSON response shape, and streaming behavior."
---

vcup exposes three HTTP surfaces on a Vercel deployment: authenticated `POST /api/upload` and `DELETE /api/delete` handlers that stream through `@vercel/blob`, and a public `GET /f/:slug` path rewritten to `api/f` that proxies blob storage with inline `Content-Disposition`. All handlers disable the default body parser and return plain-text error bodies except upload success, which returns JSON.

## Endpoint inventory

| Method | Path | Auth | Handler | Purpose |
|--------|------|------|---------|---------|
| `POST` | `/api/upload` | Bearer `VCUP_TOKEN` | `api/upload.ts` | Stream body to Vercel Blob; return proxy and raw URLs |
| `DELETE` | `/api/delete` | Bearer `VCUP_TOKEN` | `api/delete.ts` | Delete blob by store URL |
| `GET` | `/f/:slug*` | None | `api/f.ts` (via rewrite) | Proxy blob bytes with inline rendering |

<Note>
`vercel.json` rewrites `/f/:slug*` to `/api/f`. Clients and the upload response use the public `/f/...` path; the rewrite is transparent to callers.
</Note>

## Request flow

```mermaid
sequenceDiagram
  participant Client
  participant Upload as api/upload
  participant Blob as Vercel Blob
  participant Proxy as api/f
  participant Delete as api/delete

  Client->>Upload: POST body stream + Bearer + X-Filename
  Upload->>Blob: put(filename, stream, addRandomSuffix)
  Blob-->>Upload: blob.url
  Upload-->>Client: JSON url + raw

  Client->>Proxy: GET /f/slug
  Proxy->>Blob: fetch(BLOB_STORE_URL/slug)
  Blob-->>Proxy: body stream
  Proxy-->>Client: streamed bytes + Content-Disposition inline

  Client->>Delete: DELETE + Bearer + X-Blob-Url
  Delete->>Blob: del(url)
  Delete-->>Client: 200 Deleted
```

## Shared behavior

### Body parsing

Upload and delete export `config = { api: { bodyParser: false } }`, so the Node request stream is passed through without buffering by the platform parser. Upload passes `req` directly to `put()`; delete has no body.

### Authentication

Upload and delete require `Authorization: Bearer <token>` where the token equals `process.env.VCUP_TOKEN`. If `VCUP_TOKEN` is unset or the bearer value does not match, the handler responds with **401** and body `Unauthorized`. The proxy route does not check a token.

### Error response format

Except for upload success (**200** `application/json`), failures use `res.end()` with a short plain-text message and no JSON envelope. Status codes are set on `res.statusCode` before the body is written.

---

:::endpoint POST /api/upload
Stream the request body into Vercel Blob storage and return proxy and raw URLs.
:::

### Method

Only `POST` is accepted. Any other method returns **405** with body `Method not allowed`.

### Request headers

<ParamField header="Authorization" type="string" required>
Bearer token. The value after `Bearer ` must equal `VCUP_TOKEN`.
</ParamField>

<ParamField header="X-Filename" type="string" required>
Logical filename passed to `put()`. Blob storage may append a random suffix to the stored path.
</ParamField>

### Request body

The raw request stream is the upload payload. There is no `Content-Type` requirement in the handler; the CLI sends a `ReadableStream` with Bun `duplex: "half"` for streaming uploads.

### Success response

**200** `Content-Type: application/json`

<ResponseField name="url" type="string">
Proxy URL: `{baseUrl}/f/{slug}` where `slug` is the pathname of the blob URL (leading `/` stripped). `baseUrl` is `VCUP_BASE_URL` if set, otherwise `{x-forwarded-proto || "http"}://{host}` from the incoming request.
</ResponseField>

<ResponseField name="raw" type="string">
Direct Vercel Blob public URL returned by `put()`.
</ResponseField>

<RequestExample>

```bash
curl -X POST "https://your-deployment.vercel.app/api/upload" \
  -H "Authorization: Bearer your-vcup-token" \
  -H "X-Filename: screenshot.png" \
  --data-binary @screenshot.png
```

</RequestExample>

<ResponseExample>

```json
{
  "url": "https://your-deployment.vercel.app/f/screenshot-abc123.png",
  "raw": "https://abc123.public.blob.vercel-storage.com/screenshot-abc123.png"
}
```

</ResponseExample>

### Blob write options

`put(filename, req, { access: "public", addRandomSuffix: true })` stores the object publicly and lets the SDK randomize the final path segment.

### Error responses

| Status | Body |
|--------|------|
| 401 | `Unauthorized` |
| 400 | `Missing x-filename header` |
| 405 | `Method not allowed` |

<Warning>
The handler does not catch `put()` failures in code. SDK or network errors surface as platform-level **500** responses rather than a documented JSON error shape.
</Warning>

---

:::endpoint DELETE /api/delete
Delete a blob object by its store URL.
:::

### Method

Only `DELETE` is accepted. Other methods return **405** `Method not allowed`.

### Request headers

<ParamField header="Authorization" type="string" required>
Same Bearer check as upload against `VCUP_TOKEN`.
</ParamField>

<ParamField header="X-Blob-Url" type="string" required>
Full blob URL to pass to `del(url)` from `@vercel/blob`. Must be the raw store URL, not the `/f/...` proxy path.
</ParamField>

### Request body

None.

### Success response

**200** with plain-text body `Deleted`. No `Content-Type` header is set explicitly.

<RequestExample>

```bash
curl -X DELETE "https://your-deployment.vercel.app/api/delete" \
  -H "Authorization: Bearer your-vcup-token" \
  -H "X-Blob-Url: https://abc123.public.blob.vercel-storage.com/screenshot-abc123.png"
```

</RequestExample>

### Error responses

| Status | Body |
|--------|------|
| 401 | `Unauthorized` |
| 400 | `Missing x-blob-url header` |
| 405 | `Method not allowed` |

---

:::endpoint GET /f/:slug
Public inline file proxy. Rewritten internally to `/api/f`.
:::

### Routing

`vercel.json` maps:

```json
{ "source": "/f/:slug*", "destination": "/api/f" }
```

The handler parses `slug` from `pathname` by stripping the `/f/` prefix. Slugs may contain path segments (e.g. nested store paths after random suffix).

### Method

Only `GET`. Other methods return **405** `Method not allowed`.

### Authentication

None. Anyone with the slug can fetch the object if the deployment has a valid `BLOB_STORE_URL`.

### Upstream resolution

The handler builds `blobUrl = `${BLOB_STORE_URL}/${slug}`` and `fetch`es it. `BLOB_STORE_URL` must be the public store base (for example `https://abc123.public.blob.vercel-storage.com`) with no trailing slash issues handled in code—use the base URL as documented in deploy steps.

### Success response

**200** with streamed body from the upstream response.

| Response header | Value |
|-----------------|-------|
| `Content-Type` | `mime-types.lookup(filename)` on the last path segment, or `application/octet-stream` |
| `Content-Disposition` | `inline` |
| `Cache-Control` | `public, max-age=31536000, immutable` |

### Streaming behavior

After a successful upstream response, the handler reads `upstream.body` with `getReader()`, writes each chunk with `res.write(value)`, then `res.end()`. If `upstream.body` is missing, it ends the response without writing bytes (**200** with empty body). This is a pull-based stream proxy, not a buffered download-then-send.

### Error responses

| Status | Body | Condition |
|--------|------|-----------|
| 400 | `Missing file slug` | Empty slug after `/f/` strip |
| 500 | `BLOB_STORE_URL not configured` | Env var unset |
| 4xx/5xx | `Not found` | Upstream `fetch` not `ok`; status code copied from upstream |
| 502 | `Failed to fetch file` | Network or fetch exception |
| 405 | `Method not allowed` | Non-GET method |

<Info>
Upstream non-OK responses propagate the upstream HTTP status to the client while always using the body text `Not found`.
</Info>

---

## CLI mapping

The published `vcup` binary in `cli.ts` is the reference client for these endpoints.

| CLI action | API call | Headers |
|------------|----------|---------|
| `vcup <file>` / stdin | `POST {VCUP_API_URL or ~/.vcuprc url}/api/upload` | `Authorization: Bearer {token}`, `X-Filename: {name}` |
| `vcup rm <url>` | `DELETE .../api/delete` | `Authorization: Bearer {token}`, `X-Blob-Url: {raw blob URL}` |

Upload reads files or stdin into memory, then emits a **64 KiB** chunked `ReadableStream` for progress display before posting. Delete accepts either a raw blob URL or a proxy URL containing `/f/`; proxy URLs require client-side `BLOB_STORE_URL` to reconstruct the blob URL before calling delete—see the upload-and-delete and troubleshooting pages.

## Server environment variables

| Variable | Used by | Role |
|----------|---------|------|
| `VCUP_TOKEN` | upload, delete | Bearer secret validation |
| `VCUP_BASE_URL` | upload | Override proxy URL host in JSON `url` field |
| `BLOB_STORE_URL` | `api/f` | Reconstruct blob URL for proxy fetches |
| `BLOB_READ_WRITE_TOKEN` | `@vercel/blob` SDK | Implicit credentials for `put` / `del` (set in Vercel project, not referenced in handler source) |

## Status code quick reference

| Code | Endpoint(s) | Meaning |
|------|-------------|---------|
| 200 | all | Success (JSON on upload; plain text on delete; bytes on GET) |
| 400 | all | Missing required header or slug |
| 401 | upload, delete | Invalid or missing Bearer token |
| 405 | all | Wrong HTTP method |
| 500 | GET `/f/...` | `BLOB_STORE_URL` missing |
| 502 | GET `/f/...` | Upstream fetch threw |
| upstream | GET `/f/...` | Blob object missing or store error |

## Related pages

<CardGroup>
<Card title="Authentication" href="/authentication">
Bearer token checks on upload and delete; public proxy route.
</Card>
<Card title="Proxy and raw URLs" href="/proxy-and-raw-urls">
`url` vs `raw` fields, `/f` rewrite, and inline disposition.
</Card>
<Card title="CLI reference" href="/cli-reference">
Headers and streaming the CLI sends to these endpoints.
</Card>
<Card title="Environment variables" href="/environment-variables">
Server and client env vars that affect API behavior.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
401, 400, 405, 500, 502, and proxy delete resolution failures.
</Card>
</CardGroup>
