File Upload & Storage
The Storage API for file upload, search, and metadata management.
When you need file upload
- Image-to-Image: Reference-image-based image editing/transformation
- Image-to-Video: Generate video from a first-frame image
- Voice Cloning: Sample audio for voice cloning
- ControlNet: Control images such as pose/depth maps
Presigned URL flow
The file goes straight to S3 without passing through the API server. The request-body cap does not apply, so you get up to 50MB per file, plus upload progress, retries, and file reuse across predictions. The console and both official SDKs use this flow.
Issue a Presigned URL
First, issue a presigned URL for the file upload. At this point the file metadata is registered in storage.
curl -X POST https://api.core.today/v1/files/upload-url \
-H "X-API-Key: cdt_your_api_key" \
-H "Content-Type: application/json" \
-d '{"filename": "reference_image.png", "folder": "inputs"}'Reseller workspaces — X-Customer-Id is required
A reseller team must say whose file this is: send X-Customer-Id to attribute the upload to that end customer, or X-Customer-Scope: all for the reseller's own (unattributed) file. Each customer's uploads live in their own key namespace, so two customers using the same folder and filename get two different objects.
curl -X POST https://api.core.today/v1/files/upload-url \
-H "X-API-Key: cdt_your_api_key" \
-H "X-Customer-Id: user_abc123" \
-H "Content-Type: application/json" \
-d '{"filename": "reference_image.png", "folder": "inputs"}'⚠️ Since 2026-10-13 a reseller-team upload with neither header returns 400 customer_scope_required (it used to silently store an unattributed file). A file name that already exists under a different owner returns 409 file_name_conflict instead of being overwritten. Always store the object_key from the response — do not rebuild it from the folder and filename.
Storing Metadata
You can store custom metadata alongside the file (up to 10KB):
curl -X POST https://api.core.today/v1/files/upload-url \
-H "X-API-Key: cdt_your_api_key" \
-H "Content-Type: application/json" \
-d {
"filename": "data",
"folder": "projects/my-project",
"metadata": {
"description": "Project data",
"tags": ["dataset", "training"],
"version": "1.0"
}
}Example Response
{
"upload_url": "https://s3.ap-northeast-2.amazonaws.com/files.core.today",
"upload_fields": {
"Content-Type": "image/png",
"key": "aiapi/f04423dfab11/inputs/1765720407215_reference_image.png",
"x-amz-algorithm": "AWS4-HMAC-SHA256",
"x-amz-credential": "ASIA.../20251214/ap-northeast-2/s3/aws4_request",
"x-amz-date": "20251214T120000Z",
"x-amz-security-token": "IQoJb3JpZ2luX2VjEG0a...",
"policy": "eyJleHBpcmF0aW9uIjoiMjAyNS0xMi0xNFQxMzowMDowMFoiLCJjb25kaXRpb25zIjpbey...",
"x-amz-signature": "e6444e667072a27fd5589241803c8abf..."
},
"file_url": "https://files.core.today/aiapi/.../reference_image.png?Expires=...&Signature=...&Key-Pair-Id=...",
"object_key": "aiapi/f04423dfab11/inputs/1765720407215_reference_image.png",
"folder": "inputs"
}Storage Quota Exceeded (403)
When a team has exceeded its storage quota (the 'Storage Limits by Plan' table below, plus any storage packs it has bought), calling POST /files/upload-url returns 403 along with a storage_quota_exceeded code. This is separate from the 50MB per-file limit, and no matter how small a single file is, a new upload is rejected once the team's total usage exceeds the limit. AI prediction outputs are not blocked by this quota.
{
"detail": {
"message": "Storage limit exceeded. Limit: 1.0 GB, Used: 1.0 GB, Requested: 0 B. Your workspace is on the 'free' plan. To continue: delete unused files, buy a storage add-on (console → Billing → Storage add-on, 10 GB per pack, billed monthly in credits), or upgrade your plan (console → Subscription).",
"code": "storage_quota_exceeded"
}
}message is a human-readable description string whose wording varies depending on whether the byte or file-count limit was exceeded (subject to change) — always branch on the code value (storage_quota_exceeded). To resolve: free up space by deleting old files (check current usage with GET /files/storage), buy a storage pack (extra capacity in 10GB steps, console Billing page), or upgrade to a higher plan.
Upload the File to S3
Using the issued upload_url and upload_fields, upload the file directly to S3.
cURL
# Add all upload_fields in order, and add file last
curl -X POST "https://s3.ap-northeast-2.amazonaws.com/files.core.today" \
-F "Content-Type=image/png" \
-F "key=aiapi/f04423dfab11/inputs/1765720407215_reference_image.png" \
-F "x-amz-algorithm=AWS4-HMAC-SHA256" \
-F "x-amz-credential=ASIA.../20251214/ap-northeast-2/s3/aws4_request" \
-F "x-amz-date=20251214T120000Z" \
-F "x-amz-security-token=IQoJb3JpZ2luX2VjEG0a..." \
-F "policy=eyJleHBpcmF0aW9uIjoiMjAyNS0xMi0xNFQxMzowMDowMFoiLCJjb25kaXRpb25zIjpbey..." \
-F "x-amz-signature=e6444e667072a27fd5589241803c8abf..." \
-F "file=@/path/to/reference_image.png"JavaScript
async function uploadFile(file, apiKey) {
// 1. Get presigned URL
const urlResponse = await fetch(
"https://api.core.today/v1/files/upload-url",
{
method: "POST",
headers: {
"X-API-Key": apiKey,
"Content-Type": "application/json"
},
body: JSON.stringify({ filename: file.name, folder: "inputs" })
}
);
const { upload_url, upload_fields, file_url } = await urlResponse.json();
// 2. Upload to S3
const formData = new FormData();
Object.entries(upload_fields).forEach(([key, value]) => {
formData.append(key, value);
});
formData.append("file", file); // file must be last!
await fetch(upload_url, {
method: "POST",
body: formData
});
// 3. Return the CDN URL (use this in prediction requests)
return file_url;
}Python
import requests
def upload_file(filepath, api_key, folder="inputs"):
filename = filepath.split("/")[-1]
# 1. Get presigned URL
url_response = requests.post(
"https://api.core.today/v1/files/upload-url",
headers={
"X-API-Key": api_key,
"Content-Type": "application/json"
},
json={"filename": filename, "folder": folder}
)
data = url_response.json()
# 2. Upload to S3
with open(filepath, "rb") as f:
files = {"file": (filename, f)}
requests.post(
data["upload_url"],
data=data["upload_fields"],
files=files
)
# 3. Return the CDN URL (use this in prediction requests)
return data["file_url"]Important
filefield must always be added last.- Pass all
upload_fieldsvalues through exactly as provided.
Use in Model Input
After the upload completes, use file_url as the model's input parameter.
Image-to-Image (FLUX Kontext)
curl -X POST https://api.core.today/v1/predictions \
-H "X-API-Key: cdt_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"model": "black-forest-labs/flux-kontext-pro",
"input": {
"prompt": "change the background to a beach sunset",
"image_url": "https://cdn.core.today/inputs/abc123/reference_image.png"
}
}'Image-to-Video (Kling)
curl -X POST https://api.core.today/v1/predictions \
-H "X-API-Key: cdt_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"model": "kwaivgi/kling-v2.1",
"input": {
"prompt": "camera slowly zooms in, gentle wind blowing",
"image": "https://cdn.core.today/inputs/abc123/first_frame.jpg",
"duration": 5
}
}'Voice Cloning (Fish Speech)
curl -X POST https://api.core.today/v1/predictions \
-H "X-API-Key: cdt_your_api_key" \
-H "Content-Type: application/json" \
-d {
"model": "minimax/speech-02-turbo",
"input": {
"text": "This sentence is read in the exact same voice as the audio you uploaded.",
"reference_audio": "https://cdn.core.today/inputs/abc123/voice_sample.wav"
}
}Storage API Endpoints
Beyond file upload, a variety of APIs are provided for storage management, search, and metadata management.
| Method | Endpoint | Description |
|---|---|---|
| Upload | ||
| POST | /files/upload-url | Issue a presigned URL |
| Storage Management | ||
| GET | /files/storage | Usage query (size, file count, breakdown by type) |
| GET | /files/storage/files | List files (pagination, folder filter) |
| GET | /files/storage/file/{object_key} | Get a single file (metadata + download URL) |
| GET | /files/storage/folders | List folders (file count, size) |
| POST | /files/storage/folder | Create a folder (pre-create an empty folder) |
| DELETE | /files/storage/folder | Delete a folder (empty only) |
| File Management | ||
| PATCH | /files/storage/file/metadata | Update metadata |
| POST | /files/storage/file/public | Set public/private |
| DELETE | /files/storage/file | Delete a file |
| Search | ||
| POST | /files/storage/search | Metadata-based file search |
| GET | /files/storage/distinct/{field} | Get distinct values by field |
| GET | /files/storage/schema/fields | List of searchable fields |
Storage Usage Query
Query the current team's storage usage.
curl -X GET "https://api.core.today/v1/files/storage" \
-H "X-API-Key: cdt_your_api_key"
# Example response
{
"total_bytes": 52428800,
"total_mb": 50.0,
"file_count": 127,
"formatted_size": "50.0 MB",
"by_content_type": [
{"content_type": "image/png", "size": 31457280, "count": 85},
{"content_type": "video/mp4", "size": 15728640, "count": 12},
{"content_type": "audio/wav", "size": 5242880, "count": 30}
]
}Response Fields
| Field | Type | Description |
|---|---|---|
| total_bytes | integer | Total usage (bytes) |
| total_mb | float | Total usage (MB) |
| file_count | integer | Total file count |
| formatted_size | string | Formatted usage (e.g., "50.0 MB") |
| by_content_type | array | Usage breakdown by content type |
List Files
List uploaded files with pagination.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| folder | string | Filter by folder path (optional) |
| limit | integer | Number of files to return (default: 50) |
| offset | integer | Number of files to skip (default: 0) |
| url_expiration | integer | Download URL expiration (seconds). 300–15,552,000 (5 min–180 days). Default: 604,800 (7 days) |
# Default query (7-day URL)
curl -X GET "https://api.core.today/v1/files/storage/files?folder=inputs&limit=10" \
-H "X-API-Key: cdt_your_api_key"
# Query with a 1-hour expiration URL
curl -X GET "https://api.core.today/v1/files/storage/files?folder=inputs&limit=10&url_expiration=3600" \
-H "X-API-Key: cdt_your_api_key"
# Example response
{
"files": [
{
"filename": "photo.png",
"file_size": 1048576,
"content_type": "image/png",
"uploaded_at": "2025-12-14T10:00:00Z",
"object_key": "aiapi/team123/inputs/1734175200000_photo.png",
"folder": "inputs",
"expires_at": "2025-12-21T10:00:00Z",
"file_url": "https://files.core.today/..."
}
],
"total_count": 42
}Get a Single File
Query a single file's metadata and download URL using its object key.
object_key is included directly in the URL path, the same way as the S3 REST API.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| url_expiration | integer | Download URL expiration (seconds). 300–15,552,000 (5 min–180 days). Default: 604,800 (7 days) |
# Default query (7-day URL)
curl -X GET "https://api.core.today/v1/files/storage/file/aiapi/team123/inputs/1734175200000_photo.png" \
-H "X-API-Key: cdt_your_api_key"
# Query with a 5-minute expiration URL
curl -X GET "https://api.core.today/v1/files/storage/file/aiapi/team123/inputs/1734175200000_photo.png?url_expiration=300" \
-H "X-API-Key: cdt_your_api_key"
# Example response
{
"filename": "photo.png",
"file_size": 1048576,
"content_type": "image/png",
"uploaded_at": "2025-12-14T10:00:00Z",
"object_key": "aiapi/team123/inputs/1734175200000_photo.png",
"folder": "inputs",
"status": "uploaded",
"expires_at": "2025-12-21T10:00:00Z",
"file_url": "https://files.core.today/...",
"metadata": {"project": "avatar-gen", "tags": ["portrait"]},
"is_public": false,
"public_url": null
}List Folders
Query the storage folder structure. Includes each folder's file count and total size.
curl -X GET "https://api.core.today/v1/files/storage/folders" \
-H "X-API-Key: cdt_your_api_key"
# Example response
{
"folders": [
{"name": "inputs", "path": "inputs", "file_count": 85, "total_size": 31457280},
{"name": "outputs", "path": "outputs", "file_count": 30, "total_size": 15728640},
{"name": "projects", "path": "projects", "file_count": 12, "total_size": 5242880}
]
}Create a Folder
You can pre-create an empty folder before uploading files. Creating a folder that already exists returns the existing folder.
docs.fileUpload.createFolder.code| Parameter | Type | Description |
|---|---|---|
| folder_name | string (required) | Folder name (1–255 chars, special characters restricted) |
| parent_folder | string (optional) | Parent folder path (root if empty) |
Delete a Folder
Deletes a folder. By default a folder whose tree still holds any file is refused with 409 (folder_not_empty), and so is a folder with subfolders. Send recursive: true to delete empty subfolders along with it, or delete_files: true to delete the files as well (see 'Deleting a folder with its files' below). Deleting an empty folder only removes index documents, so no S3 object or storage quota is affected.
docs.fileUpload.deleteFolder.code| Parameter | Type | Description |
|---|---|---|
| folder_path | string (required) | Full path of the folder to delete (e.g. my-project/images) |
| recursive | boolean (optional, default false) | When true, empty subfolders at any depth are deleted too (up to 5,000). If any file exists anywhere in the tree, nothing is deleted and 409 is returned. |
| delete_files | boolean (optional, default false) | When true, the files in the folder and its subfolders are deleted too (implies recursive). Each call deletes up to 100 files and returns remaining_files — repeat until done is true. Reseller teams must send X-Customer-Scope: all (X-Customer-Id is not accepted). |
| expected_file_count | integer (required with delete_files) | The file count you confirmed — first the file_count from GET /files/storage/folder/summary, then the previous response's remaining_files. If the live count differs, nothing is deleted and 409 folder_changed is returned. |
Deleting a folder with its files
Check what will be deleted with GET /files/storage/folder/summary (files, bytes, public files, AI outputs, uploads in progress), then send its file_count as expected_file_count. Each file is deleted through the same path as DELETE /files/storage/file (S3 original and public copy, CDN cache, storage usage). Uploads still in progress return 409 upload_in_progress. Deleted files cannot be restored, and signed or public URLs you already shared stop working.
docs.fileUpload.deleteFolder.withFilesCodeFile Search API
You can search uploaded files by various conditions.
Request Parameters
| Parameter | Type | Description |
|---|---|---|
| filters | object | Search conditions (based on metadata fields) |
| sort_by | string | Sort field (default: created_at) |
| sort_order | string | "asc" or "desc" (default: desc) |
| page | integer | Page number (default: 1) |
| page_size | integer | Results per page (default: 20, max: 100) |
| url_expiration | integer | Download URL expiration (seconds). 300–15,552,000 (5 min–180 days). Default: 604,800 (7 days) |
Search Example
# Search recently uploaded PNG files
curl -X POST https://api.core.today/v1/files/storage/search \
-H "X-API-Key: cdt_your_api_key" \
-H "Content-Type: application/json" \
-d {
"filters": {
"content_type": "image/png",
"folder": "inputs"
},
"sort_by": "created_at",
"sort_order": "desc",
"page_size": 10
}Example Response
{
"files": [
{
"object_key": "aiapi/team123/inputs/1734175200000_photo.png",
"filename": "photo.png",
"content_type": "image/png",
"size": 1048576,
"folder": "inputs",
"created_at": "2025-12-14T10:00:00Z",
"url": "https://files.core.today/...",
"metadata": {
"project": "avatar-gen",
"tags": ["portrait", "input"]
}
}
],
"total": 42,
"page": 1,
"page_size": 10,
"total_pages": 5
}Searchable Metadata Fields
You can search files using 40 indexed fields.
Identity Fields
file_id - Unique file IDobject_key - S3 object keyfilename - Original filenameteam_id - Team IDuser_id - User IDTime Fields
created_at - Creation timeupdated_at - Update timeexpires_at - Expiration timeSource Fields
source - Source (upload/prediction)source_job_id - Source job IDsource_model - Generating model nameupload_ip - Upload IPObject Fields
content_type - MIME typesize - File size (bytes)extension - Extensionfolder - Folder pathchecksum - MD5 hashClassification Fields
category - Categorysubcategory - Subcategorytags - Tag arraylabels - Label arrayproject - Project nameenvironment - Environment (dev/prod)Status Fields
status - Status (pending/active/archived)visibility - Visibility scopeis_processed - Whether processing is completeis_archived - Whether archivedMetrics Fields
width - Image/video widthheight - Image/video heightduration - Audio/video duration (seconds)frame_count - Video frame countsample_rate - Audio sample ratebit_depth - Audio bit depthAI/ML Context Fields
prompt - Generation promptnegative_prompt - Negative promptseed - Random seedsteps - Number of generation stepscfg_scale - CFG scalemodel_version - Model versionCompliance Fields
retention_days - Retention periodlegal_hold - Whether under legal holdclassification_level - Security classification levelDistinct Values API
Query the list of distinct values for a specific field. Useful for building a filtering UI.
# List all folders in use
curl -X GET "https://api.core.today/v1/files/storage/distinct/folder" \
-H "X-API-Key: cdt_your_api_key"
# Example response
{
"field": "folder",
"values": ["inputs", "outputs", "projects/avatars", "projects/backgrounds"],
"count": 4
}Supported Fields
folder, content_type, extension, category, project, tags, source_model, status and more
Schema Fields API
Query all searchable fields and their type information.
curl -X GET "https://api.core.today/v1/files/storage/schema/fields" \
-H "X-API-Key: cdt_your_api_key"
# Example response
{
"fields": [
{"name": "file_id", "type": "keyword", "searchable": true},
{"name": "filename", "type": "text", "searchable": true},
{"name": "created_at", "type": "date", "searchable": true},
{"name": "size", "type": "long", "searchable": true},
{"name": "tags", "type": "keyword", "searchable": true, "array": true}
],
"total_fields": 40
}Metadata Update API
Update a file's metadata. It is merged with the existing metadata.
docs.fileUpload.metaMod.codeCaution
- System fields (file_id, team_id, created_at, etc.) cannot be modified
- Total metadata size cannot exceed 10KB
File Deletion API
Permanently deletes the file and its metadata.
curl -X DELETE "https://api.core.today/v1/files/storage/file?object_key=aiapi/team123/inputs/1734175200000_photo.png" \
-H "X-API-Key: cdt_your_api_key"
# Example response
{
"success": true,
"deleted_object_key": "aiapi/team123/inputs/1734175200000_photo.png",
"deleted_at": "2025-12-14T12:00:00Z"
}Warning
- Deleted files cannot be recovered
- You can only delete files belonging to your own team
SDK Examples
Python — File Search
import requests
API_KEY = "cdt_your_api_key"
BASE_URL = "https://api.core.today/v1"
def search_files(filters, page=1, page_size=20):
"""Search files."""
response = requests.post(
f"{BASE_URL}/files/storage/search",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json"
},
json={
"filters": filters,
"page": page,
"page_size": page_size,
"sort_by": "created_at",
"sort_order": "desc"
}
)
return response.json()
# Example: search PNG images
results = search_files({
"content_type": "image/png",
"folder": "inputs"
})
for file in results["files"]:
print(f"{file['filename']} - {file['size']} bytes")
# Example: search files in a specific project
project_files = search_files({
"project": "avatar-gen",
"tags": ["approved"]
})JavaScript — File Search
const API_KEY = "cdt_your_api_key";
const BASE_URL = "https://api.core.today/v1";
async function searchFiles(filters, options = {}) {
const { page = 1, pageSize = 20, sortBy = "created_at", sortOrder = "desc" } = options;
const response = await fetch(`${BASE_URL}/files/storage/search`, {
method: "POST",
headers: {
"X-API-Key": API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
filters,
page,
page_size: pageSize,
sort_by: sortBy,
sort_order: sortOrder
})
});
return response.json();
}
// Example: search recently uploaded images
const images = await searchFiles({
content_type: "image/png",
folder: "inputs"
});
console.log(`Found ${images.total} files`);
images.files.forEach(file => {
console.log(`${file.filename} - ${file.size} bytes`);
});
// Example: search files generated by a specific model
const generatedFiles = await searchFiles({
source_model: "black-forest-labs/flux-schnell",
created_at: { gte: "2025-12-01T00:00:00Z" }
});Metadata Design Guide
Per-project Management
{
"project": "avatar-generator",
"category": "portraits",
"environment": "production"
}Workflow Tracking
{
"status": "pending_review",
"tags": ["needs-approval", "high-priority"],
"assigned_to": "reviewer@company.com"
}AI Generation Context
{
"source_model": "black-forest-labs/flux-schnell",
"prompt": "A professional headshot",
"seed": 12345,
"steps": 28,
"cfg_scale": 7.5
}Best Practices
- Consistent naming: Use the same field names across all projects
- Use tags: Use the tags array when you need multiple classifications
- Separate environments: Separate files by dev/staging/production environment
- Versioning: Track file versions with the version field
File Upload Limits
File Formats
All file formats supported
Images, video, audio, documents, binaries, and more
Maximum Size
50MB
Storage Limits by Plan
File upload and storage usage do not consume credits. However, each subscription plan (console → Subscription) has limits on total capacity and file count. Credit packages on the Billing page (10K to 1M credits) only add credits — they do not change your plan or storage limit, so teams without a subscription get the Free limits.
| Plan | File Storage | Max Files | DB Storage |
|---|---|---|---|
| Free | 1 GB | 1,000 | 100 MB |
| Pro | 10 GB | 50,000 | 5 GB |
| Team | 100 GB | 500,000 | 50 GB |
| Enterprise | 1 TB | 10,000,000 | 500 GB |
You can check current usage on the dashboard's Storage page or via the GET /files/storage API.
Storage packs (extra capacity)
To grow file storage without changing plans, buy storage packs. Pack capacity is added on top of your plan's base limit, and the price is deducted monthly from team credits.
- 1 pack = 10GB + 5,000 files, 900 credits per month. Up to 50 packs (500GB) per team.
- Team owners/admins buy and change packs in the console → Billing → 'Storage Add-on' card. The limit increases immediately, and packs renew automatically every 30 days.
- A first purchase deducts packs × 900 credits right away. Adding packs mid-cycle charges only the added packs, pro-rated by the days left until the next renewal.
- Reducing or cancelling takes effect at the next renewal, with no refund. Until then you keep the current capacity.
- If credits are short on the renewal date, the add-on goes past due; recharge within 7 days and it recovers automatically. After 7 days the packs are cancelled, but files already stored are not deleted — new uploads are refused while you are over the limit. While past due, buying or reducing packs returns 409 addon_past_due.
File Validity Period
| Item | Validity Period | Description |
|---|---|---|
| Presigned Upload URL | 7 days | Upload must happen within 7 days of URL issuance |
| Uploaded file | By plan (see table below) | Retention is independent of the URL validity period — even if the URL expires the file is not deleted, and it can be re-signed at any time |
| Download URL (default) | 7 days | Default expiration for file/result download URLs |
| Download URL (custom, storage query) | 5 min – 180 days | Set via the GET /files/storage/* storage query endpoint's url_expiration parameter (in seconds, 300–15,552,000) |
| Re-signed URL (/files/sign) | Up to 7 days | POST /files/sign's expiration is capped at 604,800 seconds (7 days) |
Why the two caps differ: /files/sign re-signing is currently capped at up to 7 days, while generating a download URL directly from the storage query endpoints allows up to 180 days. This is not a bug but a per-endpoint policy difference, and either way the file remains even after the URL expires, so if you store the object_key you can reissue a URL at any time.
| Plan | File Retention Period |
|---|---|
| Free | Kept until you delete it |
| Pro | Kept until you delete it |
| Team / Enterprise | Kept until you delete it |
Important
- Uploaded files and generated outputs are kept until you delete them, on every plan — only files you asked to be temporary (ephemeral outputs: 24h, purpose: "input" uploads: 7 days) are removed automatically
- Even if the download URL expires the file remains — if you store the object_key you can reissue a new URL via
POST /files/sign - Check storage usage and limits on the console Storage page or with GET /files/storage
Temporary outputs (ephemeral) example
If you move outputs to your own server or storage right away, request with ephemeral: true. The outputs don't count toward your storage quota, are deleted automatically after 24 hours, and their URLs expire at the same time. Below is a real request and response.
Request
curl -X POST https://api.core.today/v1/predictions \
-H "X-API-Key: cdt_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"model": "black-forest-labs/flux-schnell",
"input": {
"prompt": "a small paper boat floating on a calm lake at sunrise, soft watercolor style",
"aspect_ratio": "16:9",
"output_format": "webp"
},
"ephemeral": true
}'Completed response (GET /predictions/:job_id, some fields and the signature trimmed) — the URL's Expires is the request time + 24 hours, when the file is deleted
{
"job_id": "KAvB4xYP8Zg8oLnxzFjTvx0RdwsRTJvu",
"status": "completed",
"provider": "replicate",
"model": "black-forest-labs/flux-schnell",
"created_at": "2026-10-29T11:24:31.098905",
"output_files": [
{
"object_key": "aiapi/94bbce570e24c705/KAvB4xYP8Zg8oLnxzFjTvx0RdwsRTJvu/o/0.webp",
"url": "https://files.core.today/aiapi/94bbce570e24c705/KAvB4xYP8Zg8oLnxzFjTvx0RdwsRTJvu/o/0.webp?Expires=1793359479&Signature=...&Key-Pair-Id=..."
}
],
"credits_used": 7
}
// Expires=1793359479 → 2026-10-30T11:24:39Z (created_at + 24h)
Image generated by the request above (black-forest-labs/flux-schnell, 7 credits). The original is deleted after 24 hours, so this page shows a copy.
To keep an output, download and move it within 24 hours, or request without ephemeral (or with an output_folder) — outputs are then kept until you delete them.
Re-sign a File (Refresh URL)
Download URLs expire after the default 7 days, but the file itself remains until you delete it. POST /files/sign lets you reissue a fresh URL for any object this team owns at any time. Store the object_key (or job_id) instead of the URL — we recommend this, since the URL expires but the object_key stays valid as long as the file exists.
| Field | Type | Description |
|---|---|---|
| object_key | string | The S3 object key to re-sign. url/job_id — specify exactly one of the three. |
| url | string | A previously issued CloudFront URL (for re-signing) |
| job_id | string | Prediction job ID — re-signs all result files owned by that job at once |
| expiration | integer (optional) | Request expiration (seconds). Capped at up to 7 days (604,800 seconds) |
docs.fileUpload.resign.codeA URL is issued only after ownership is verified — an object owned by another team or a nonexistent key both return 404 file_not_found the same way (so file existence is not leaked). In a reseller workspace, calling with the X-Customer-Id header limits re-signing to files attributed to that customer.
Re-referencing Previous Output (Model Chaining)
You can pass a previous prediction's output file without re-downloading or re-uploading it directly as the next prediction's input value. If the input contains a https://files.core.today/... URL that this team owns, the gateway automatically re-signs it with a fresh URL before forwarding to the provider. Even if the URL has already expired, it still works as long as you own the file. For example, you can feed an image generation result straight into a video model's input image.
- Files not owned by this team, or URLs that are not on
files.core.today, pass through unchanged (not an error) — a still-valid presigned URL or an external URL can be used as-is. - A single
inputcan auto-re-sign up to 20 owned references.
Pattern 1 — Pass a stored URL as-is (auto re-sign)
docs.fileUpload.chaining.pattern1CodePattern 2 — Store the object_key and pass a new URL from /files/sign
docs.fileUpload.chaining.pattern2CodePattern 1 has the gateway re-sign automatically, so no separate /files/sign call is needed. Pattern 2 is useful when you need to validate/log the URL yourself or hand it to a client in advance. In both cases the file is never re-uploaded.
Complete Example (Python)
import requests
import time
API_KEY = "cdt_your_api_key"
BASE_URL = "https://api.core.today/v1"
def upload_and_transform(image_path, prompt, project="default"):
"""Upload an image and transform it."""
# 1. Get presigned URL with metadata
filename = image_path.split("/")[-1]
url_response = requests.post(
f"{BASE_URL}/files/upload-url",
headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
json={
"filename": filename,
"folder": f"projects/{project}/inputs",
"metadata": {
"project": project,
"source": "upload",
"original_filename": filename
}
}
)
url_data = url_response.json()
# 2. Upload to S3
with open(image_path, "rb") as f:
requests.post(
url_data["upload_url"],
data=url_data["upload_fields"],
files={"file": (filename, f)}
)
# 3. Create prediction with uploaded image
pred_response = requests.post(
f"{BASE_URL}/predictions",
headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
json={
"model": "black-forest-labs/flux-kontext-pro",
"input": {
"prompt": prompt,
"image_url": url_data["file_url"]
}
}
)
job_id = pred_response.json()["job_id"]
# 4. Poll for result
while True:
result = requests.get(
f"{BASE_URL}/predictions/{job_id}",
headers={"X-API-Key": API_KEY}
).json()
if result["status"] == "completed":
return result["result"]
elif result["status"] == "failed":
raise Exception(result.get("error"))
time.sleep(2)
# Usage
result = upload_and_transform(
"/path/to/photo.jpg",
"make it look like a watercolor painting",
project="art-styles"
)
print(f"Result: {result}")Quick upload (Multipart)
POST /predictions/upload accepts the file directly, so upload and prediction happen in a single API call. The bytes pass through the API server's memory, so the 10MB request-body cap applies — for anything larger, or for a production integration, use the Presigned URL flow above.
File Field Rules
file:<input_key> as the file field name, and the uploaded file's URL is automatically injected into input[input_key].
| File Field Name | Result |
|---|---|
| file:image | input["image"] = "https://..." |
| file:image (x2) | input["image"] = ["url1", "url2"] |
cURL
# Single file - Image-to-Image
curl -X POST https://api.core.today/v1/predictions/upload \
-H "X-API-Key: cdt_your_api_key" \
-F "model=flux-kontext-pro" \
-F 'input={"prompt":"change background to a beach sunset"}' \
-F "file:image_url=@photo.png"
# Multiple files - send several files under the same key
curl -X POST https://api.core.today/v1/predictions/upload \
-H "X-API-Key: cdt_your_api_key" \
-F "model=flux-kontext-pro" \
-F 'input={"prompt":"combine these images"}' \
-F "file:image_url=@photo1.png" \
-F "file:image_url=@photo2.png"Python
import requests, json
with open("photo.png", "rb") as f:
response = requests.post(
"https://api.core.today/v1/predictions/upload",
headers={"X-API-Key": "cdt_your_api_key"},
data={
"model": "black-forest-labs/flux-kontext-pro",
"input": json.dumps({"prompt": "change background to a beach sunset"}),
},
files={"file:image_url": ("photo.png", f, "image/png")},
)
job = response.json()
print(job["job_id"]) # then poll GET /predictions/{job_id} for the resultJavaScript
const formData = new FormData();
formData.append("model", "black-forest-labs/flux-kontext-pro");
formData.append("input", JSON.stringify({ prompt: "change background to a beach sunset" }));
formData.append("file:image_url", fileInput.files[0]);
const response = await fetch("https://api.core.today/v1/predictions/upload", {
method: "POST",
headers: { "X-API-Key": "cdt_your_api_key" },
body: formData,
});
const job = await response.json();
console.log(job.job_id);Form Fields
| Field | Type | Required | Description |
|---|---|---|---|
| model | string | Yes | Model ID |
| input | string (JSON) | No | Model input JSON string (default: "{}") |
| is_public | string | No | "true" or "false" (default: "false") |
| output_folder | string | No | Output storage folder path |
| webhook_url | string | No | Webhook URL to call on completion |
| file:<input_key> | file | No | File to upload (multiple files allowed under the same key) |
Note
- This method is capped at 10MB for the whole request — the bytes pass through the API server, so the global request-body limit applies (413 above it). For larger files use the Presigned URL method below (50MB per file, uploaded straight to S3).
- The response format is identical to
POST /predictions. After that, poll for the result viaGET /predictions/{job_id}. - Uploaded files are stored in S3 storage, and a CDN URL accessible for 7 days is generated.