Um único endpoint para hospedar suas imagens de referência, vídeos de origem ou áudio em uma URL pública do R2 — depois é só inserir a URL em qualquer chamada de geração /v1 (image-to-image, referência de vídeo, dubbing, lipsync). Sem precisar manter seu próprio armazenamento.
Os dois modos são selecionados pelo Content-Type da sua requisição. Use o modo 1 para vídeo ou qualquer arquivo grande.
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 | obrigatório | string | Nome original do arquivo (usado para definir a extensão armazenada) |
| contentType | obrigatório | string | Tipo MIME, ex.: image/png, video/mp4, audio/mpeg |
| fileSize | opcional | number | Número de bytes; se informado, o limite de tamanho é verificado antecipadamente |
{
"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 é válida por 10 minutos; publicUrl fica acessível assim que o PUT é concluído e permanece por 7 dias.
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: video/mp4" \
--data-binary @ref.mp4⚠️ O Content-Type do PUT deve coincidir com o contentType que você solicitou — caso contrário, o R2 rejeitará a assinatura.
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 | obrigatório | binary | Campo de arquivo do formulário; seu Content-Type é lido da parte multipart |
{
"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)A publicUrl retornada vai diretamente para o campo image_url / video_url / images[] de qualquer endpoint /v1:
# 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\"
}"O mesmo padrão funciona para video_url de /v1/video/generations, video_url de lipsync, audio_url de dubbing e campos semelhantes.
Os erros retornam um corpo uniforme { "code": <status>, "msg": "..." }.
| HTTP | msg | Quando |
|---|---|---|
| 400 | filename and contentType are required | Campo obrigatório ausente no modo JSON |
| 400 | Only image/, video/, or audio/ content types are accepted | O Content-Type não está na lista de permitidos |
| 400 | File too large (max 100MB) / (max 10MB) | Excede o limite de tamanho por tipo |
| 400 | No file provided (expected form field "file") | Falta o campo "file" no corpo multipart |
| 400 | Invalid JSON body | Não foi possível analisar o corpo JSON |
| 400 | Expected multipart/form-data or application/json | Content-Type da requisição não reconhecido |
| 401 | Invalid or missing API key | Chave ausente, incorreta ou desativada |
| 500 | Storage not configured | Variáveis de ambiente do R2 não configuradas (não deveria acontecer em produção) |