Create a direct-upload descriptor

Reserve an upload slot and return a signed URL the client PUTs the binary to. Full upload→run recipe: (1) POST /api/uploads with the file's name, exact size in bytes, and mime — the response carries a uri (e.g. upload://upl_xxx), a signed url, and headers. (2) PUT the raw file bytes (not multipart) to url, sending exactly the returned headers (Content-Type; in direct-presign S3 mode also If-None-Match: *, plus Content-Length when the storage signs the declared size). The body must then be exactly size bytes. A successful direct PUT is create-only: replaying the URL cannot replace the stored bytes. When sha256 was supplied, the returned signed checksum header is required as well. (3) Call runAgent with uri as the value of the file-typed input field. The actual byte count must equal the declared size, and binary MIMEs are verified by magic-byte sniffing. Consumed uploads are NOT single-use: the bytes stay retained — and the uri re-consumable — for UPLOAD_RETENTION_HOURS (default 24 h) after the first consume, so the same input can be re-run (e.g. via rerun_from after cancelling) without re-uploading. Unconsumed uploads expire with the signed URL. Small files (≤4 MiB decoded) can skip this flow entirely: inline the content directly in the runAgent input as data:<mime>;name=<filename>;base64,<payload>. Rate-limited to 20/min.

POST/api/uploads

Authorization

better-auth.session_token<token>

Cookie session from Better Auth. Requires X-Org-Id header for org-scoped routes.

In: cookie

Header Parameters

X-Org-Id?string

Organization ID. Required for cookie auth. Not needed for API key auth (org resolved from key).

Formatuuid
X-Space-Id?string

Space ID. Required for space-scoped routes (agents, runs, schedules, and space-scoped module routes). Not needed for API key auth (space resolved from key).

Request Body

application/json

curl -X POST "https://your-instance/api/uploads" \  -H "Content-Type: application/json" \  -d '{    "name": "invoice.pdf",    "size": 24576,    "mime": "application/pdf"  }'
{
  "object": "upload",
  "id": "upl_abc123",
  "uri": "upload://upl_abc123",
  "url": "https://app.example.com/api/uploads/_content?token=eyJrIjoi...",
  "method": "PUT",
  "headers": {
    "Content-Type": "application/pdf"
  },
  "expiresAt": "2026-04-14T12:15:00Z"
}

{
  "type": "https://docs.appstrate.dev/errors/validation-failed",
  "title": "Validation Failed",
  "status": 400,
  "detail": "name: Invalid input: expected string, received undefined (+2 more)",
  "code": "validation_failed",
  "request_id": "req_abc123",
  "errors": [
    {
      "field": "name",
      "code": "required",
      "message": "Invalid input: expected string, received undefined"
    },
    {
      "field": "email",
      "code": "invalid_format",
      "message": "Invalid email address"
    },
    {
      "field": "age",
      "code": "invalid_type",
      "message": "Invalid input: expected number, received string"
    }
  ]
}

{
  "type": "https://docs.appstrate.dev/errors/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Invalid or missing session",
  "code": "unauthorized",
  "request_id": "req_abc123"
}
{
  "type": "about:blank",
  "title": "Storage Limit Exceeded",
  "status": 403,
  "detail": "Organization staging limit (5368709120 bytes) would be exceeded",
  "code": "storage_limit_exceeded",
  "request_id": "req_abc123"
}
{
  "type": "about:blank",
  "title": "Upload Staging Limit Exceeded",
  "status": 429,
  "detail": "Too many active staged uploads (max 20); consume or let existing uploads expire before staging more",
  "code": "upload_staging_limit_exceeded",
  "request_id": "req_abc123"
}