One endpoint to host your reference images, source videos, or audio at a public R2 URL — then drop the URL into any /v1 generation call (image-to-image, video reference, dubbing, lipsync). No need to run your own storage.
The two modes are dispatched by your request Content-Type. Use mode 1 for video or any large file.
POST /api/v1/files
Authorization: Bearer <API_KEY>
Content-Type: application/json
{
"filename": "ref.mp4",
"contentType": "video/mp4",
"fileSize": 12345678
}| Field | Required | Type | Description |
|---|---|---|---|
| filename | required | string | Original file name (used to pick the stored extension) |
| contentType | required | string | MIME type, e.g. image/png, video/mp4, audio/mpeg |
| fileSize | optional | number | Byte count; if present, size cap is checked up-front |
{
"code": 200,
"msg": "success",
"data": {
"uploadUrl": "https://<account>.r2.cloudflarestorage.com/apimodels/uploads/<userId>/1717...xxxx.png?X-Amz-Signature=...",
"publicUrl": "https://r2.apimodels.app/uploads/<userId>/1717...xxxx.png",
"expiresAt": "2026-05-30T09:30:00.000Z"
}
}uploadUrl is valid for 10 minutes; publicUrl is reachable as soon as the PUT completes and stays for 7 days.
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: video/mp4" \
--data-binary @ref.mp4⚠️ The PUT Content-Type must match the contentType you requested — otherwise R2 will reject the signature.
cURL
# 1) Request a presigned PUT URL
RESP=$(curl -s -X POST https://apimodels.app/api/v1/files \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "ref.png",
"contentType": "image/png",
"fileSize": '"$(wc -c < ref.png)"'
}')
UPLOAD_URL=$(echo "$RESP" | jq -r .data.uploadUrl)
PUBLIC_URL=$(echo "$RESP" | jq -r .data.publicUrl)
# 2) PUT the file body directly to R2 (no Vercel size limit)
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: image/png" \
--data-binary @ref.png
# 3) Use PUBLIC_URL in any /v1 generation endpoint
echo "Public URL: $PUBLIC_URL"Python
import requests, os
API_KEY = os.environ["API_KEY"]
with open("ref.png", "rb") as f:
data = f.read()
# 1) Request presigned URL
r = requests.post(
"https://apimodels.app/api/v1/files",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"filename": "ref.png",
"contentType": "image/png",
"fileSize": len(data),
},
)
upload_url = r.json()["data"]["uploadUrl"]
public_url = r.json()["data"]["publicUrl"]
# 2) PUT directly to R2
requests.put(upload_url, data=data, headers={"Content-Type": "image/png"})
# 3) public_url is your reference for any generation call
print("Public URL:", public_url)Node.js
import fs from 'node:fs/promises'
const API_KEY = process.env.API_KEY
const file = await fs.readFile('ref.png')
// 1) Request presigned URL
const r = await fetch('https://apimodels.app/api/v1/files', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: 'ref.png',
contentType: 'image/png',
fileSize: file.length,
}),
})
const { data: { uploadUrl, publicUrl } } = await r.json()
// 2) PUT to R2 directly
await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': 'image/png' },
body: file,
})
console.log('Public URL:', publicUrl)POST /api/v1/files
Authorization: Bearer <API_KEY>
Content-Type: multipart/form-data; boundary=...
(form field "file" with the raw bytes)| Field | Required | Type | Description |
|---|---|---|---|
| file | required | binary | Form file field; its Content-Type is read from the multipart part |
{
"code": 200,
"msg": "success",
"data": {
"publicUrl": "https://r2.apimodels.app/uploads/<userId>/1717...xxxx.png"
}
}cURL
curl -X POST https://apimodels.app/api/v1/files \
-H "Authorization: Bearer $API_KEY" \
-F "file=@ref.png"Python
import requests, os
r = requests.post(
"https://apimodels.app/api/v1/files",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
files={"file": open("ref.png", "rb")},
)
print(r.json()["data"]["publicUrl"])Node.js
import fs from 'node:fs'
const form = new FormData()
form.append(
'file',
new Blob([fs.readFileSync('ref.png')], { type: 'image/png' }),
'ref.png',
)
const r = await fetch('https://apimodels.app/api/v1/files', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.API_KEY}` },
body: form,
})
console.log((await r.json()).data.publicUrl)The returned publicUrl drops straight into the image_url / video_url / images[] field of any /v1 endpoint:
# 1) Upload your reference photo
PUBLIC_URL=$(curl -s -X POST https://apimodels.app/api/v1/files \
-H "Authorization: Bearer $API_KEY" \
-F "file=@photo.jpg" | jq -r .data.publicUrl)
# 2) Pass it into an image-to-image call
curl -X POST https://apimodels.app/api/v1/images/generations \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"nanobanana2/gemini-3.1-flash-image-preview\",
\"prompt\": \"Make it Studio-Ghibli-style\",
\"image_url\": \"$PUBLIC_URL\"
}"Same pattern works for /v1/video/generations video_url, lipsync video_url, dubbing audio_url, and similar fields.
Errors return a uniform { "code": <status>, "msg": "..." } body.
| HTTP | msg | When |
|---|---|---|
| 400 | filename and contentType are required | Missing required field in JSON mode |
| 400 | Only image/, video/, or audio/ content types are accepted | Content-Type not in the allow-list |
| 400 | File too large (max 100MB) / (max 10MB) | Exceeds the per-type size cap |
| 400 | No file provided (expected form field "file") | multipart body missing the "file" field |
| 400 | Invalid JSON body | JSON body failed to parse |
| 400 | Expected multipart/form-data or application/json | Unrecognized request Content-Type |
| 401 | Invalid or missing API key | Missing key, wrong key, or key is disabled |
| 500 | Storage not configured | R2 env vars not configured (should not happen in prod) |