Skip to main content
The multipart POST /v1/uploads endpoint streams file bytes through our API gateway, which caps inbound HTTP/1 request bodies at roughly 32 MiB. Requests above that size are rejected with 413 Payload Too Large before they reach the upload handler. For larger files (up to MAX_FILE_SIZE_BYTES, currently 250 MB), use the two-step signed-URL flow below. The bytes go straight from your client to our object storage, bypassing the gateway entirely.
If you’re uploading small files (under ~30 MiB) the multipart POST /v1/uploads endpoint is simpler and a single round trip. Reach for the two-step flow only when you actually need it.

When to use this flow

  • Any file larger than ~32 MiB (PPTX decks with embedded images, multi-page PDFs, high-resolution source assets).
  • Files where you’d rather not have the bytes traverse our gateway (e.g. you’re streaming directly from another cloud bucket).

End-to-end recipe

1. Request a signed PUT URL

Response:
The upload_url is a GCS V4 signed URL. It is valid for expires_in_seconds (default 600, max 3600) and is bound to the mime_type you passed — the PUT must send a matching Content-Type header or storage rejects it. Keep the storage_key around — you’ll pass it back at step 3.

2. PUT the file bytes directly to storage

No Authorization header on this request — the signed URL itself is the capability. The PUT goes directly to object storage, so the response status comes from GCS (a successful PUT returns 200 OK).

3. Register the upload to receive a stable file id

Response (the same shape as POST /v1/uploads):
register hashes the staged blob and deduplicates against existing team files by content hash, so re-uploading the same bytes returns the existing file_id with was_duplicate: true. Because hashing requires reading the full blob, register for a 100+ MB file can take several seconds — set your client timeout accordingly. The pending blob at storage_key is deleted once the canonical file row is created (or, on a dedup hit, immediately).

4. Attach the file to a design task

Use the returned id exactly as you would from POST /v1/uploads:
See Start a design task for the full request shape.

Errors and edge cases

Retries and idempotency

Neither endpoint takes an idempotency_key:
  • /uploads/url is safe to retry — each call just mints a fresh, independent signed URL. A URL you minted and didn’t use is harmless (a PUT never happened, so nothing is stored).
  • /uploads/register is naturally idempotent on content hash. Calling register twice with the same storage_key after a successful first call will fail the second time with 422 "No blob found" (the first call deleted the staged blob). Calling register for the same file content via a different signed URL returns the existing file with was_duplicate: true.

Required scope

Both endpoints require the uploads:write scope on your API key — the same scope as POST /v1/uploads.

Cleanup

You don’t need to do anything in the happy path: register deletes the pending blob after the canonical file row is created (or immediately, on a dedup hit). If you mint a signed URL and never PUT to it, nothing is stored. If you PUT but never call register, the staged blob is left in the pending prefix — there’s no scheduled sweep today, so prefer re-using the same storage_key on retry rather than minting fresh URLs you abandon.