# 1. Ask for an upload URL
curl -X POST "https://api.hooked.so/v1/media/upload" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "fileName": "demo.mp4", "fileType": "video/mp4", "fileSize": 8388608 }'
# 2. PUT the file to data.uploadUrl
curl -X PUT "UPLOAD_URL_FROM_STEP_1" \
-H "Content-Type: video/mp4" \
--data-binary @demo.mp4
# 3. Complete
curl -X POST "https://api.hooked.so/v1/media/MEDIA_ID/complete" \
-H "x-api-key: your_api_key_here"
import { readFile, stat } from "node:fs/promises";
const headers = { "x-api-key": process.env.HOOKED_API_KEY, "Content-Type": "application/json" };
const { size } = await stat("demo.mp4");
const start = await fetch("https://api.hooked.so/v1/media/upload", {
method: "POST",
headers,
body: JSON.stringify({ fileName: "demo.mp4", fileType: "video/mp4", fileSize: size }),
});
const { data: upload } = await start.json();
await fetch(upload.uploadUrl, {
method: upload.method,
headers: upload.headers,
body: await readFile("demo.mp4"),
});
const done = await fetch(`https://api.hooked.so/v1/media/${upload.mediaId}/complete`, {
method: "POST",
headers: { "x-api-key": process.env.HOOKED_API_KEY },
});
const { data: media } = await done.json();
console.log(media.id, media.status); // COMPLETED
import os
import requests
api_key = os.environ["HOOKED_API_KEY"]
path = "demo.mp4"
upload = requests.post(
"https://api.hooked.so/v1/media/upload",
headers={"x-api-key": api_key},
json={"fileName": "demo.mp4", "fileType": "video/mp4", "fileSize": os.path.getsize(path)},
).json()["data"]
with open(path, "rb") as file:
requests.put(upload["uploadUrl"], headers=upload["headers"], data=file).raise_for_status()
media = requests.post(
f"https://api.hooked.so/v1/media/{upload['mediaId']}/complete",
headers={"x-api-key": api_key},
).json()["data"]
print(media["id"], media["status"])
{
"success": true,
"message": "Upload URL created",
"data": {
"mediaId": "cm4x9s8u60009ab12kl56mn90",
"uploadUrl": "https://files.hooked.so/team/public/demo--fid--9c1d.mp4?X-Amz-Signature=...",
"method": "PUT",
"headers": { "Content-Type": "video/mp4" },
"expiresAt": "2026-10-01T09:40:00.000Z"
}
}
{
"success": false,
"message": "fileName: The extension must be one of mp4, mov, webm"
}
Upload Media
Get a presigned URL to upload a file to your media library
# 1. Ask for an upload URL
curl -X POST "https://api.hooked.so/v1/media/upload" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "fileName": "demo.mp4", "fileType": "video/mp4", "fileSize": 8388608 }'
# 2. PUT the file to data.uploadUrl
curl -X PUT "UPLOAD_URL_FROM_STEP_1" \
-H "Content-Type: video/mp4" \
--data-binary @demo.mp4
# 3. Complete
curl -X POST "https://api.hooked.so/v1/media/MEDIA_ID/complete" \
-H "x-api-key: your_api_key_here"
import { readFile, stat } from "node:fs/promises";
const headers = { "x-api-key": process.env.HOOKED_API_KEY, "Content-Type": "application/json" };
const { size } = await stat("demo.mp4");
const start = await fetch("https://api.hooked.so/v1/media/upload", {
method: "POST",
headers,
body: JSON.stringify({ fileName: "demo.mp4", fileType: "video/mp4", fileSize: size }),
});
const { data: upload } = await start.json();
await fetch(upload.uploadUrl, {
method: upload.method,
headers: upload.headers,
body: await readFile("demo.mp4"),
});
const done = await fetch(`https://api.hooked.so/v1/media/${upload.mediaId}/complete`, {
method: "POST",
headers: { "x-api-key": process.env.HOOKED_API_KEY },
});
const { data: media } = await done.json();
console.log(media.id, media.status); // COMPLETED
import os
import requests
api_key = os.environ["HOOKED_API_KEY"]
path = "demo.mp4"
upload = requests.post(
"https://api.hooked.so/v1/media/upload",
headers={"x-api-key": api_key},
json={"fileName": "demo.mp4", "fileType": "video/mp4", "fileSize": os.path.getsize(path)},
).json()["data"]
with open(path, "rb") as file:
requests.put(upload["uploadUrl"], headers=upload["headers"], data=file).raise_for_status()
media = requests.post(
f"https://api.hooked.so/v1/media/{upload['mediaId']}/complete",
headers={"x-api-key": api_key},
).json()["data"]
print(media["id"], media["status"])
{
"success": true,
"message": "Upload URL created",
"data": {
"mediaId": "cm4x9s8u60009ab12kl56mn90",
"uploadUrl": "https://files.hooked.so/team/public/demo--fid--9c1d.mp4?X-Amz-Signature=...",
"method": "PUT",
"headers": { "Content-Type": "video/mp4" },
"expiresAt": "2026-10-01T09:40:00.000Z"
}
}
{
"success": false,
"message": "fileName: The extension must be one of mp4, mov, webm"
}
Overview
Uploading a file from your machine takes three calls:- This call: send the file’s name, MIME type and size. You get a
mediaId(statusPENDING) and a presigneduploadUrl. - PUT the file to
uploadUrlwith theheadersreturned, within 10 minutes (expiresAt). This request goes straight to storage, without your API key. - Complete Upload: Hooked checks that the file arrived and the media becomes
COMPLETED.
fileName must match fileType. Your plan’s storage is checked now with fileSize and again on complete with the size actually uploaded; it is counted only on complete. Uploading is free.# 1. Ask for an upload URL
curl -X POST "https://api.hooked.so/v1/media/upload" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "fileName": "demo.mp4", "fileType": "video/mp4", "fileSize": 8388608 }'
# 2. PUT the file to data.uploadUrl
curl -X PUT "UPLOAD_URL_FROM_STEP_1" \
-H "Content-Type: video/mp4" \
--data-binary @demo.mp4
# 3. Complete
curl -X POST "https://api.hooked.so/v1/media/MEDIA_ID/complete" \
-H "x-api-key: your_api_key_here"
import { readFile, stat } from "node:fs/promises";
const headers = { "x-api-key": process.env.HOOKED_API_KEY, "Content-Type": "application/json" };
const { size } = await stat("demo.mp4");
const start = await fetch("https://api.hooked.so/v1/media/upload", {
method: "POST",
headers,
body: JSON.stringify({ fileName: "demo.mp4", fileType: "video/mp4", fileSize: size }),
});
const { data: upload } = await start.json();
await fetch(upload.uploadUrl, {
method: upload.method,
headers: upload.headers,
body: await readFile("demo.mp4"),
});
const done = await fetch(`https://api.hooked.so/v1/media/${upload.mediaId}/complete`, {
method: "POST",
headers: { "x-api-key": process.env.HOOKED_API_KEY },
});
const { data: media } = await done.json();
console.log(media.id, media.status); // COMPLETED
import os
import requests
api_key = os.environ["HOOKED_API_KEY"]
path = "demo.mp4"
upload = requests.post(
"https://api.hooked.so/v1/media/upload",
headers={"x-api-key": api_key},
json={"fileName": "demo.mp4", "fileType": "video/mp4", "fileSize": os.path.getsize(path)},
).json()["data"]
with open(path, "rb") as file:
requests.put(upload["uploadUrl"], headers=upload["headers"], data=file).raise_for_status()
media = requests.post(
f"https://api.hooked.so/v1/media/{upload['mediaId']}/complete",
headers={"x-api-key": api_key},
).json()["data"]
print(media["id"], media["status"])
{
"success": true,
"message": "Upload URL created",
"data": {
"mediaId": "cm4x9s8u60009ab12kl56mn90",
"uploadUrl": "https://files.hooked.so/team/public/demo--fid--9c1d.mp4?X-Amz-Signature=...",
"method": "PUT",
"headers": { "Content-Type": "video/mp4" },
"expiresAt": "2026-10-01T09:40:00.000Z"
}
}
{
"success": false,
"message": "fileName: The extension must be one of mp4, mov, webm"
}
Errors
| Status | Message | Cause |
|---|---|---|
| 400 | fileName: Required, fileType: Required | A field is missing. |
| 400 | fileSize: Must be the file size in bytes, a positive integer | fileSize is missing or not a whole number of bytes. |
| 400 | fileType: Must be one of ... | Not a supported image or video type. |
| 400 | fileName: The extension must be one of ... | The extension does not match fileType. |
| 400 | fileSize: Max file size is 100 MB | The file is too large. |
| 401 | code: "not_authenticated" | Missing or invalid x-api-key. |
| 403 | Insufficient storage. ... | The file does not fit your plan’s storage. |
Authorizations
Headers
Makes the request safe to retry. 1-255 printable ASCII characters, one per operation (your job id, or a UUID you store), reused on every retry. Within 24 hours the same key with the same body answers with the first response and the header Idempotent-Replayed: true, without running or charging again. Keys are scoped to your team and the endpoint. 5xx answers and refusals before anything ran (401, 402, 403, 409, 429) are not kept. See Idempotency.
1 - 255^[\x20-\x7E]+$Body
The file's name with its extension: jpg, jpeg, png, webp, gif, mp4, mov or webm.
The file's MIME type. Its extension must match (a video/mp4 named .png is refused).
image/jpeg, image/png, image/webp, image/gif, video/mp4, video/quicktime, video/webm Size in bytes, at most 100 MB. Checked against your storage now and again, with the real size, on complete.
1 <= x <= 104857600Name in your library. Defaults to fileName.