Documentation Index Fetch the complete documentation index at: https://docs.ofauth.com/llms.txt
Use this file to discover all available pages before exploring further.
Learn how to upload media to the OnlyFans vault so you can use it in posts and messages.
Prerequisites
Upload Flow Overview
Uploading media is a multi-step process:
Step 1: Initialize Upload
Tell OFAuth about the file you want to upload:
const response = await fetch ( "https://api.ofauth.com/v2/access/uploads/init" , {
method: "POST" ,
headers: {
apikey: "YOUR_API_KEY" ,
"x-connection-id" : "conn_abc123" ,
"Content-Type" : "application/json"
},
body: JSON . stringify ({
filename: "photo.jpg" ,
size: 1024000 ,
contentType: "image/jpeg"
})
})
const upload = await response . json ()
console . log ( "Upload ID:" , upload . mediaUploadId )
console . log ( "Parts:" , upload . totalParts )
Request body:
Field Type Required Description filenamestring Yes Original file name, including extension sizenumber Yes File size in bytes contentTypestring Yes MIME type, such as image/jpeg or video/mp4 vaultUploadobject No Options for automatic vault upload workflows
Example response:
{
"mediaUploadId" : "upload:550e8400-e29b-41d4-a716-446655440000" ,
"uploadType" : "multipart" ,
"totalParts" : 2 ,
"partSize" : 5242880 ,
"expiresAt" : "2026-04-29T10:18:48.831Z" ,
"parts" : [
{
"partNumber" : 1 ,
"uploadUrl" : "https://api.ofauth.com/v2/access/uploads/upload%3A550e8400-e29b-41d4-a716-446655440000/parts/1" ,
"byteRange" : {
"start" : 0 ,
"end" : 5242879
}
},
{
"partNumber" : 2 ,
"uploadUrl" : "https://api.ofauth.com/v2/access/uploads/upload%3A550e8400-e29b-41d4-a716-446655440000/parts/2" ,
"byteRange" : {
"start" : 5242880 ,
"end" : 6825498
}
}
],
"completeUrl" : "https://api.ofauth.com/v2/access/uploads/complete"
}
Response fields:
Field Type Description mediaUploadIdstring Upload session ID. You can later pass this value directly in mediaItems. uploadType"single" or "multipart"Whether the upload uses one request or multiple part requests. totalPartsnumber Number of file parts to upload. partSizenumber Maximum bytes per part. expiresAtstring ISO timestamp when the upload session expires. partsarray Ordered upload steps. Upload each byteRange to its matching uploadUrl. completeUrlstring URL to call after multipart uploads. Omitted for single-part uploads.
The response also includes x-ofauth-upload-total-parts and x-ofauth-upload-part-size headers for backward compatibility. Prefer the response body for new integrations.
Step 2: Upload the File
Single-Part Upload (Small Files)
For files that fit in one part (upload.uploadType is "single"), upload the whole file to the first uploadUrl:
const fileBuffer = fs . readFileSync ( "photo.jpg" )
const [ part ] = upload . parts
const response = await fetch ( part . uploadUrl , {
method: "PUT" ,
headers: {
apikey: "YOUR_API_KEY" ,
"x-connection-id" : "conn_abc123" ,
"Content-Type" : "image/jpeg"
},
body: fileBuffer
})
// Single-part upload auto-completes, returns media info directly
const result = await response . json ()
console . log ( "Media ID:" , result . media ?. id )
Single-part PUT response:
{
"mediaUploadId" : "upload:550e8400-e29b-41d4-a716-446655440000" ,
"media" : {
"id" : 12345 ,
"type" : "photo" ,
"files" : {}
}
}
Multi-Part Upload (Large Files)
For larger files, upload each parts[] entry. Slice the file using the provided byteRange, then send that chunk to the entry’s uploadUrl:
const fileBuffer = fs . readFileSync ( "video.mp4" )
for ( const part of upload . parts ) {
const chunk = fileBuffer . slice ( part . byteRange . start , part . byteRange . end + 1 )
const response = await fetch ( part . uploadUrl , {
method: "PUT" ,
headers: {
apikey: "YOUR_API_KEY" ,
"x-connection-id" : "conn_abc123" ,
"Content-Type" : "video/mp4"
},
body: chunk
})
if ( ! response . ok ) {
throw new Error ( `Part ${ part . partNumber } failed: ${ await response . text () } ` )
}
console . log ( `Uploaded part ${ part . partNumber } / ${ upload . totalParts } ` )
}
Multipart part PUT response:
{
"mediaUploadId" : "upload:550e8400-e29b-41d4-a716-446655440000" ,
"partNumber" : 1 ,
"etag" : " \" abc123 \" "
}
Step 3: Complete Upload (Multi-Part Only)
For multi-part uploads, call completeUrl after all chunks are uploaded:
const response = await fetch ( upload . completeUrl , {
method: "POST" ,
headers: {
apikey: "YOUR_API_KEY" ,
"x-connection-id" : "conn_abc123" ,
"Content-Type" : "application/json"
},
body: JSON . stringify ({
mediaUploadId
})
})
const result = await response . json ()
console . log ( "Use this in mediaItems:" , result . mediaUploadId )
Request body:
{
"mediaUploadId" : "upload:550e8400-e29b-41d4-a716-446655440000"
}
Response body:
{
"mediaUploadId" : "upload:550e8400-e29b-41d4-a716-446655440000"
}
Use the returned mediaUploadId in a post or message:
{
"text" : "New video" ,
"mediaItems" : [ "upload:550e8400-e29b-41d4-a716-446655440000" ]
}
Single-part uploads auto-complete. When uploadType is "single", the PUT response returns the media directly. Do not call /complete.
Minimal JavaScript Multipart Example
import { readFileSync } from "node:fs"
const API_KEY = "YOUR_API_KEY"
const CONNECTION_ID = "conn_abc123"
const CONTENT_TYPE = "video/mp4"
const FILENAME = "video.mp4"
const file = readFileSync ( FILENAME )
const jsonHeaders = {
apikey: API_KEY ,
"x-connection-id" : CONNECTION_ID ,
"Content-Type" : "application/json"
}
const initResponse = await fetch ( "https://api.ofauth.com/v2/access/uploads/init" , {
method: "POST" ,
headers: jsonHeaders ,
body: JSON . stringify ({
filename: FILENAME ,
size: file . length ,
contentType: CONTENT_TYPE
})
})
if ( ! initResponse . ok ) throw new Error ( await initResponse . text ())
const upload = await initResponse . json ()
if ( upload . uploadType !== "multipart" ) {
throw new Error ( "This example expects a multipart upload. Use a larger file." )
}
for ( const part of upload . parts ) {
const chunk = file . subarray ( part . byteRange . start , part . byteRange . end + 1 )
const partResponse = await fetch ( part . uploadUrl , {
method: "PUT" ,
headers: {
apikey: API_KEY ,
"x-connection-id" : CONNECTION_ID ,
"Content-Type" : CONTENT_TYPE
},
body: chunk
})
if ( ! partResponse . ok ) {
throw new Error ( `Part ${ part . partNumber } failed: ${ await partResponse . text () } ` )
}
}
const completeResponse = await fetch ( upload . completeUrl , {
method: "POST" ,
headers: jsonHeaders ,
body: JSON . stringify ({ mediaUploadId: upload . mediaUploadId })
})
if ( ! completeResponse . ok ) throw new Error ( await completeResponse . text ())
const result = await completeResponse . json ()
console . log ({
mediaItems: [ result . mediaUploadId ]
})
Type Formats Max Size Images JPEG, PNG, GIF, WebP ~50MB Videos MP4, MOV ~5GB Audio MP3, M4A ~50MB
Check OnlyFans’ current guidelines for exact size limits, as they may change.
Once uploaded, use the media in posts or messages.
Use Upload Reference Directly
Pass the mediaUploadId string directly to mediaItems - the system resolves it automatically:
const { mediaUploadId } = result
await fetch ( "https://api.ofauth.com/v2/access/posts" , {
method: "POST" ,
headers ,
body: JSON . stringify ({
text: "New content! 📸" ,
mediaItems: [ mediaUploadId ] // e.g. "upload:550e8400-e29b-41d4-a716-446655440000"
})
})
Upload references are automatically resolved to vault media IDs by the API. This is the simplest approach.
If your upload workflow returns media, you can use the numeric vault media ID instead:
const vaultMediaId = result . media . id
await fetch ( "https://api.ofauth.com/v2/access/posts" , {
method: "POST" ,
headers ,
body: JSON . stringify ({
text: "New content! 📸" ,
mediaItems: [ vaultMediaId ] // 12345
})
})
See the mediaItems reference .
Single-use : Upload references are consumed when used. Once you include a mediaUploadId in a post or message, it cannot be reused. If you need to use the same media multiple times, store the vault media ID instead.
Use /uploads/check to see if media with a known upload ETag and size already exists in the vault:
const response = await fetch ( "https://api.ofauth.com/v2/access/uploads/check" , {
method: "POST" ,
headers: {
apikey: "YOUR_API_KEY" ,
"x-connection-id" : "conn_abc123" ,
"Content-Type" : "application/json"
},
body: JSON . stringify ({
etag: " \" abc123 \" " ,
size: 1024000
})
})
const result = await response . json ()
console . log ( "Exists:" , result . exists )
console . log ( "Media:" , result . media )
Request body:
Field Type Required Description etagstring Yes ETag returned by a previous upload response sizenumber Yes Uploaded file size in bytes
Response body:
{
"exists" : true ,
"media" : {
"id" : 12345 ,
"type" : "photo" ,
"files" : {}
}
}
Error Handling
try {
const response = await fetch ( "https://api.ofauth.com/v2/access/uploads/init" , {
method: "POST" ,
headers ,
body: JSON . stringify ({ filename , size: fileBuffer . length , contentType: mimeType })
})
if ( ! response . ok ) {
const error = await response . json ()
switch ( response . status ) {
case 400 :
console . error ( "Invalid upload request:" , error . error || error . message )
break
case 413 :
console . error ( "File too large" )
break
case 415 :
console . error ( "Unsupported file type" )
break
default :
console . error ( "Upload failed:" , error )
}
return
}
// Continue with upload...
const upload = await response . json ()
for ( const part of upload . parts ) {
// Upload each byte range to part.uploadUrl
}
} catch ( err ) {
console . error ( "Network error:" , err )
}
Tips & Best Practices
Retry failed chunks : If a chunk upload fails, you can retry just that chunk without restarting the entire upload.
Upload to vault first for reuse : Vault uploads give you a media ID or upload reference that you can attach to posts and messages.
Session timeout : Upload sessions expire after a period of inactivity. Start the complete flow and finish promptly.
Next Steps
Create a Post Use uploaded media in posts
Send a Message Send media in direct messages