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
Simple Upload (Multipart)
POST /predictions/upload lets you handle both the file upload and the prediction request in a single API call. There is no need to separately issue a presigned URL or upload directly to S3.
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 (max 50MB per file, multiple files allowed under the same key) |
Note
- The maximum size per file is 50MB.
- 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.
Presigned URL Flow (Advanced)
For large files or when you need to show upload progress, you can use the 3-step flow below. It is also useful when you want to upload a file in advance and reuse it across multiple predictions.
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"}'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 per-plan storage quota (table above), 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: 5.0 GB, Used: 5.0 GB, Requested: 0 B",
"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), 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) |
| 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) |
File 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 plan has limits on total capacity and file count.
| 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.
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 within the retention period |
| 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 | 30 days |
| Pro | 365 days |
| Team / Enterprise | Unlimited |
Important
- Uploaded files are kept for the per-plan retention period (table above), and until then they will not disappear unless you delete them yourself
- 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 - You can check storage usage in the dashboard settings
Re-sign a File (Refresh URL)
Download URLs expire after the default 7 days, but the file itself remains for the plan's retention period. 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}")