Step 2: PUT the file bytes to Storage
Direct PUT from the client to the signed Supabase Storage URL returned by Step 1. Not a Graniite endpoint — the binary never traverses our application servers.
This step lives between Step 1 (request a signed URL) and Step 3 (finalize media or finalize document).
It isn't a Graniite endpoint, which is why it doesn't appear in the auto-generated REST reference. You PUT the file bytes directly to the signed_url returned by Step 1 — the upload goes straight to Supabase Storage, bypassing Vercel's 4.5 MB request-body cap and our application servers entirely.
Request
| Field | Value |
|---|---|
| Method | PUT |
| URL | the signed_url from Step 1 |
| Auth | none — the signed URL carries its own short-lived token |
| Headers | Content-Type: <mime from Step 1> |
| Body | raw file bytes |
The Content-Type you send here must match exactly what you declared to /uploads/signed-url. A mismatch will not error here — Storage accepts the PUT — but the magic-byte verification at finalize time will reject the upload.
curl
curl -X PUT "<signed_url from Step 1>" \
-H "Content-Type: audio/mpeg" \
--data-binary @meeting.mp3A successful PUT returns 200 OK with an empty body. Hold on to the path, mime, and filename you used in Step 1 — Step 3 needs all three.
Common failures
- 403 / signature mismatch — the signed URL has expired (single-use, ~minutes). Call Step 1 again to get a fresh one.
Content-Typerejected — the header you sent doesn't match what Step 1 declared, or it isn't in the bucket's allowlist. Send the same MIME you registered.- Body too large for your client — Vercel's cap doesn't apply here (you're talking to Supabase, not us), but your HTTP client may have its own buffer limits.
--data-binaryin curl streams without buffering; in code, use a streaming request body.
Next
Call Step 3 to finalize. The finalize_endpoint field in Step 1's response tells you which one to use:
finalize→ audio/video uploads (queues transcription).finalize-document→ PDF/image uploads (extracts text synchronously).
For the full end-to-end flow with code samples, see the Binary Upload quickstart.
Step 1: get a signed Storage URL for a binary upload. POST
Three-step binary upload, step 1 of 3. The server validates the declared MIME + size against the appropriate bucket's allowlist (media-uploads for audio/video, document-uploads for PDF/image), generates a single-use signed URL the client PUTs bytes to directly, and returns the path + which finalize endpoint to call next. Vercel caps request bodies at 4.5 MB; this flow exists because direct-to-Storage upload bypasses that limit. Rate limit: 30/hr per token.
Step 3 (audio/video): finalize a media upload and submit for transcription. POST
Call after PUT-ing the file to the signed URL. Server downloads the bytes, runs magic-byte verification (so a renamed evil.mp3 with non-MP3 bytes is rejected), creates the contents + user_items rows, reserves transcription minutes against the user's monthly quota, and submits the file to AssemblyAI. Returns immediately with status='transcribing' — poll GET /items/{id} until transcription_status === 'done'. Single-folder-scoped tokens auto-file via the scope-size-1 rule. Rate limit: 30/hr per token.