import fetch from "node-fetch";
const fileBuffer = fs.readFileSync("./my-video.mp4");
// Step 1: Get upload URL
const response = await fetch("https://www.genviral.io/api/partner/v1/files", {
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
"Idempotency-Key": "prepare-my-video-1",
},
body: JSON.stringify({
contentType: "video/mp4",
filename: "my-video.mp4",
duration_sec: 42,
bytes: fileBuffer.length,
}),
});
const { data } = await response.json();
// data.uploadUrl = presigned S3 URL
// data.id = upload identity used for finalization
// Step 2: Upload file to presigned URL
await fetch(data.uploadUrl, {
method: "PUT",
headers: {
"Content-Type": "video/mp4",
},
body: fileBuffer,
});
// Step 3: Verify the stored bytes and create the Media Library record
const finalizeResponse = await fetch(
`https://www.genviral.io/api/partner/v1/files/${data.id}/finalize`,
{
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
"Idempotency-Key": `finalize-${data.id}`,
},
body: JSON.stringify({
contentType: "video/mp4",
filename: "my-video.mp4",
duration_sec: 42,
bytes: fileBuffer.length,
}),
},
);
const { data: finalized } = await finalizeResponse.json();
// Step 4: Use the CDN URL in your post
await fetch("https://www.genviral.io/api/partner/v1/posts", {
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
},
body: JSON.stringify({
caption: "Check this out!",
media: {
type: "video",
url: finalized.file.url,
},
accounts: [{ id: "account-id" }],
}),
});
{
"ok": true,
"code": 201,
"message": "Upload URL generated",
"data": {
"id": "11111111-1111-4111-8111-111111111111",
"uploadUrl": "https://storage.example.com/presigned-url...",
"contentType": "video/mp4",
"expiresIn": 600
}
}
Files
Upload File
Upload images and videos to Genviral CDN for use in TikTok slideshows, Instagram carousels, YouTube Shorts, Pinterest pins, LinkedIn posts, and Facebook content. Supports programmatic uploads from OpenClaw agents and automation pipelines.
POST
/
api
/
partner
/
v1
/
files
import fetch from "node-fetch";
const fileBuffer = fs.readFileSync("./my-video.mp4");
// Step 1: Get upload URL
const response = await fetch("https://www.genviral.io/api/partner/v1/files", {
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
"Idempotency-Key": "prepare-my-video-1",
},
body: JSON.stringify({
contentType: "video/mp4",
filename: "my-video.mp4",
duration_sec: 42,
bytes: fileBuffer.length,
}),
});
const { data } = await response.json();
// data.uploadUrl = presigned S3 URL
// data.id = upload identity used for finalization
// Step 2: Upload file to presigned URL
await fetch(data.uploadUrl, {
method: "PUT",
headers: {
"Content-Type": "video/mp4",
},
body: fileBuffer,
});
// Step 3: Verify the stored bytes and create the Media Library record
const finalizeResponse = await fetch(
`https://www.genviral.io/api/partner/v1/files/${data.id}/finalize`,
{
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
"Idempotency-Key": `finalize-${data.id}`,
},
body: JSON.stringify({
contentType: "video/mp4",
filename: "my-video.mp4",
duration_sec: 42,
bytes: fileBuffer.length,
}),
},
);
const { data: finalized } = await finalizeResponse.json();
// Step 4: Use the CDN URL in your post
await fetch("https://www.genviral.io/api/partner/v1/posts", {
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
},
body: JSON.stringify({
caption: "Check this out!",
media: {
type: "video",
url: finalized.file.url,
},
accounts: [{ id: "account-id" }],
}),
});
{
"ok": true,
"code": 201,
"message": "Upload URL generated",
"data": {
"id": "11111111-1111-4111-8111-111111111111",
"uploadUrl": "https://storage.example.com/presigned-url...",
"contentType": "video/mp4",
"expiresIn": 600
}
}
Upload media files directly to Genviral’s storage, then finalize the stored bytes into a
CDN-backed Media Library record. A preparation never creates a visible file by itself.
One upload URL carries up to 100 MB (images up to 50 MB). For larger videos, up to 500 MB, use
Start Multipart Upload.
How It Works
- Call this endpoint with the file’s content type and a stable
Idempotency-Key - Receive a file
idand presigneduploadUrl - Upload your file directly to the
uploadUrlusing a PUT request - Call
POST /api/partner/v1/files/{fileId}/finalizewith the same media metadata and a stableIdempotency-Key - Use the finalized
file.urlin post creation or pack-image attachment requests
Body Parameters
string
required
MIME type of the file. Supported types:
- Images:
image/jpeg,image/png,image/gif,image/webp,image/heic,image/heif - Videos:
video/mp4,video/quicktime,video/x-msvideo,video/webm,video/x-m4v
string
Original filename for reference (optional). Used for display purposes only.
number
Optional video duration in seconds. Also accepts
duration_seconds, duration, durationSec,
or video_duration_sec. Stored with the CDN file record so /posts can hydrate validation
metadata when you use the returned url.number
Optional file size in bytes. Also accepts
size. Images: 1 through 52,428,800 (50 MB). Videos:
1 through 104,857,600 (100 MB) on this endpoint; larger videos use
Start Multipart Upload.Response
Successful requests return201 with:
uploadUrl- Presigned URL to upload your file (expires in 10 minutes)id- Stable upload identity used by the finalize endpointcontentType- The content type you specifiedexpiresIn- Seconds until the upload URL expires (600)
file, including its verified byte size and public
https://cdn.vireel.io/... URL. Invalid, absent, empty, oversized, or MIME-mismatched bytes are
rejected without creating a Media Library row.
Using With Packs
If your goal is to add a local file to a pack:- Call this endpoint and capture
data.id+data.uploadUrl. - Upload your bytes to
data.uploadUrlwithPUT. - Finalize the upload and capture
data.file.url. - Call Add Pack Image with
image_url = data.file.url.
Examples
Upload a video
import fetch from "node-fetch";
const fileBuffer = fs.readFileSync("./my-video.mp4");
// Step 1: Get upload URL
const response = await fetch("https://www.genviral.io/api/partner/v1/files", {
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
"Idempotency-Key": "prepare-my-video-1",
},
body: JSON.stringify({
contentType: "video/mp4",
filename: "my-video.mp4",
duration_sec: 42,
bytes: fileBuffer.length,
}),
});
const { data } = await response.json();
// data.uploadUrl = presigned S3 URL
// data.id = upload identity used for finalization
// Step 2: Upload file to presigned URL
await fetch(data.uploadUrl, {
method: "PUT",
headers: {
"Content-Type": "video/mp4",
},
body: fileBuffer,
});
// Step 3: Verify the stored bytes and create the Media Library record
const finalizeResponse = await fetch(
`https://www.genviral.io/api/partner/v1/files/${data.id}/finalize`,
{
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
"Idempotency-Key": `finalize-${data.id}`,
},
body: JSON.stringify({
contentType: "video/mp4",
filename: "my-video.mp4",
duration_sec: 42,
bytes: fileBuffer.length,
}),
},
);
const { data: finalized } = await finalizeResponse.json();
// Step 4: Use the CDN URL in your post
await fetch("https://www.genviral.io/api/partner/v1/posts", {
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
},
body: JSON.stringify({
caption: "Check this out!",
media: {
type: "video",
url: finalized.file.url,
},
accounts: [{ id: "account-id" }],
}),
});
# Step 1: Get upload URL
curl --request POST \
--url https://www.genviral.io/api/partner/v1/files \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: prepare-my-video-1' \
--data '{
"contentType": "video/mp4",
"filename": "my-video.mp4",
"duration_sec": 42,
"bytes": 8000000
}'
# Step 2: Upload to presigned URL (use uploadUrl from response)
curl --request PUT \
--url "<uploadUrl from step 1>" \
--header 'Content-Type: video/mp4' \
--data-binary @my-video.mp4
# Step 3: Finalize with POST /files/{fileId}/finalize and the exact step-1 metadata
{
"ok": true,
"code": 201,
"message": "Upload URL generated",
"data": {
"id": "11111111-1111-4111-8111-111111111111",
"uploadUrl": "https://storage.example.com/presigned-url...",
"contentType": "video/mp4",
"expiresIn": 600
}
}
Error Responses
400 invalid_request-Idempotency-Keyis missing or invalid400 invalid_json- Request body is not valid JSON422 invalid_payload- Invalid content type, missing required fields, or a declared size above the single-upload limit401- authentication failed (missing/invalid/revoked token)402 subscription_required- active Creator/Professional/Business plan required403 tier_not_allowed- Scheduler tier cannot use Partner API500 create_failed- Failed to initialize upload (retry)
The presigned upload URL expires after 10 minutes. If it expires before you upload,
simply request a new one.
POST /files only prepares an expiring upload destination. The file is ready for use only after
the PUT succeeds and POST /files/{fileId}/finalize returns the canonical CDN-backed record.