The multipartDocumentation Index
Fetch the complete documentation index at: https://docs.moda.app/llms.txt
Use this file to discover all available pages before exploring further.
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
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
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
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 returnedid exactly as you would from POST /v1/uploads:
Errors and edge cases
| Status | Returned by | Meaning |
|---|---|---|
415 on /uploads/url | Moda | The mime_type isn’t on the allow-list (no executables, scripts, etc.). |
422 on /uploads/register | Moda | The storage_key is missing, malformed, or isn’t prefixed for your team — i.e. it wasn’t issued by a prior /uploads/url call on the same API key. |
422 "No blob found at storage_key" | Moda | The PUT never completed, the signed URL expired before the bytes arrived, or you registered the wrong key. Re-mint a fresh URL and re-PUT. |
403 on /uploads/register | Moda | The API key’s user is no longer a member of the team the key is bound to. Distinct from the prefix-mismatch 422 above. |
413 on /uploads/register | Moda | The file is above MAX_FILE_SIZE_BYTES (250 MB). The oversized blob is deleted automatically. |
403 on the PUT | GCS | Signed URL expired, content type doesn’t match the one you minted, or the URL was tampered with. Mint a new one. |
Retries and idempotency
Neither endpoint takes anidempotency_key:
/uploads/urlis 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/registeris naturally idempotent on content hash. Callingregistertwice with the samestorage_keyafter a successful first call will fail the second time with422 "No blob found"(the first call deleted the staged blob). Callingregisterfor the same file content via a different signed URL returns the existing file withwas_duplicate: true.
Required scope
Both endpoints require theuploads: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.