Files API
The Files API allows you to upload and manage files for use with vision, audio, and other AI endpoints. It mirrors OpenAI’s Files API with PickLayer extensions.
Key Concepts
- PickLayer File UUIDs: Uploads return a PickLayer file UUID (e.g.,
add71816-b93b-440d-9ff3-a5556b5dc39a). Use this UUID in AI calls, never provider IDs. - CDN & Signed URLs: Files are stored privately and served via CloudFront with per-request signed URLs.
- Purpose-based uploads: Specify a
purposeto categorize files.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/files | Upload a file |
GET | /v1/files | List files |
GET | /v1/files/{id} | Get file metadata |
GET | /v1/files/{id}/content | Download file content |
DELETE | /v1/files/{id} | Delete a file |
Upload a File
Multipart upload
curl -X POST "https://api.picklayer.dev/api/org/{org}/app/{app}/v1/files" \
-H "Authorization: Bearer $TOKEN" \
-F "purpose=vision" \
-F "file=@/tmp/cat.jpg;type=image/jpeg"JSON base64 upload
curl -X POST "https://api.picklayer.dev/api/org/{org}/app/{app}/v1/files" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"purpose": "vision",
"filename": "cat.jpg",
"mime_type": "image/jpeg",
"file_base64": "data:image/jpeg;base64,..."
}'Response
{
"id": "add71816-b93b-440d-9ff3-a5556b5dc39a",
"purpose": "vision",
"bytes": 245678,
"filename": "cat.jpg",
"mime_type": "image/jpeg",
"status": "uploaded",
"expires_at": "2025-02-15T12:00:00Z",
"hash_sha256": "abc123..."
}Purposes
user_data is accepted as an alias for documents to stay OpenAI-SDK compatible. Responses for document uploads return purpose: "user_data".
| Purpose | Description |
|---|---|
vision | Images for chat/vision endpoints |
audio | Audio files for transcription/speech |
documents | PDFs, code, text files (alias: user_data) |
assistants | Tool assets |
image_generation | Generated images from AI |
video_generation | Generated videos from AI |
Encrypted files
Files uploaded with is_encrypted: true (using the per-file file_key flow described in Encryption) work seamlessly with chat references — but the chat request must additionally carry a top-level file_keys map so the gateway can decrypt before forwarding bytes to the upstream provider.
{
"model": "openai/gpt-4o",
"input": [
{ "type": "input_text", "text": "Describe this image." },
{ "type": "input_image", "image_url": { "url": "<encrypted-file-uuid>" } }
],
"file_keys": {
"<encrypted-file-uuid>": "<base64url-derived-key>"
}
}The same file_keys mechanism applies to image_url, input_audio, and input_file references on Responses, Messages, and Chat Completions endpoints. The selection between the inline data: URL path and the signed CDN URL path (with opaque efk query param) is automatic. See Encryption — Attaching encrypted files to chat requests for the full architectural reference.
Using Files in AI Calls
In Chat Completions (vision)
{
"model": "openai/gpt-4o",
"messages": [{
"role": "user",
"content": [
{ "type": "text", "text": "What do you see?" },
{ "type": "image_url", "image_url": { "url": "add71816-b93b-440d-9ff3-a5556b5dc39a" } }
]
}]
}In Responses API
{
"model": "openai/gpt-4o",
"input": [
{ "type": "input_text", "text": "What do you see?" },
{ "type": "input_image", "image_url": { "url": "add71816-b93b-440d-9ff3-a5556b5dc39a" } }
]
}Audio input
{
"input_audio": { "url": "add71816-b93b-440d-9ff3-a5556b5dc39a", "format": "wav" }
}File Expiration
Files can be set to expire using the OpenAI-compatible expires_after parameter:
{
"purpose": "vision",
"file_base64": "...",
"expires_after": {
"anchor": "created_at",
"seconds": 86400
}
}- Default expiration: 30 days
- Use
"seconds": "forever"for permanent storage
Limits
- Max file size: 25 MB
- Image parts per request: 10
- Audio input duration: 15 minutes max
Sharing & access modes
Files have an access_mode that determines who can see them, plus an access_surface_mode that controls which surfaces (list, direct fetch, AI-only) expose them.
access_mode
| Value | Description |
|---|---|
single | Default. Only the uploader can read, patch, or delete the file. |
shared | The uploader grants access to specific users via shared_with. Each entry is { user_id, can_read, can_write }. |
app-wide | File is visible to every user in the app. Non-owners are read-only; only owners/builders can patch or delete. App-wide writes require an active billing assignment. |
shared_with shape
{
"access_mode": "shared",
"shared_with": [
{ "user_id": "...", "can_read": true, "can_write": false }
]
}access_surface_mode
| Value | Description |
|---|---|
all | Default. File appears in list endpoints, is directly retrievable, and usable in AI calls. |
direct | Hidden from list for non-owners but retrievable by ID and usable in AI calls. |
ai_only | List/retrieve/signed-url/content routes all return 404 for non-owners; file is only reachable when referenced from AI endpoints (image_url, input_audio, etc.). |
ACL endpoints
POST /v1/files/{file_id}/share— body{ shared_with: [{ user_id, can_read, can_write }], access_surface_mode? }. Replaces or extends the ACL.DELETE /v1/files/{file_id}/share/{user_id}— revokes a single user’s access.
SDK examples
# Python
client.share_file(
file_id,
[{"user_id": other_user_id, "can_read": True, "can_write": False}],
)
client.revoke_file_share(file_id, other_user_id)// TypeScript / React
await client.shareFile(fileId, [
{ user_id: otherUserId, can_read: true, can_write: false },
]);
await client.revokeFileShare(fileId, otherUserId);// Flutter
await client.shareFile(
fileId: fileId,
sharedWith: [
{'user_id': otherUserId, 'can_read': true, 'can_write': false},
],
);
await client.revokeFileShare(fileId: fileId, userId: otherUserId);App-wide files
App-wide files are visible to every user in the app. They have their own dedicated route family:
| Method | Path | Description |
|---|---|---|
GET | /v1/files/app-wide | List app-wide files. |
POST | /v1/files/app-wide | Upload an app-wide file. |
PATCH | /v1/files/app-wide/{file_id} | Update metadata (owner only). |
DELETE | /v1/files/app-wide/{file_id} | Delete (owner only). |
# Python — upload an app-wide file
client.upload_file(
purpose="documents",
file_base64=b64,
filename="policy.txt",
mime_type="text/plain",
access_mode="app-wide",
)
client.list_app_wide_files()Using POST /v1/files with access_mode: "app-wide" produces the same result as POST /v1/files/app-wide; both routes land on the same handler.
Builder File Upload (Programmatic)
Builders using the Management API with an API key (ExchangeSecretKey) can upload files programmatically using a three-step pre-signed URL flow. This avoids the REST multipart endpoint (which requires an interactive session token).
Step 1: Reserve a file ID
curl -s https://api.picklayer.ai/v1/graphql \
-H "Authorization: Bearer $BUILDER_TOKEN" \
-H "Content-Type: application/json" \
-H "x-hasura-role: builder" \
-d '{
"query": "mutation($input: BuilderFilesReserveInput!) { BuilderFilesReserve(input: $input) { code data { fileId uploadUrl uploadHeaders expiresAt } } }",
"variables": {
"input": {
"filename": "training_data.csv",
"purpose": "embeddings",
"contentType": "text/csv"
}
}
}'Response includes a pre-signed S3 upload URL valid for 15 minutes.
Step 2: Upload to S3
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: text/csv" \
--data-binary @training_data.csvUpload directly to S3 — no size proxy limits, no server buffering.
Step 3: Confirm the upload
curl -s https://api.picklayer.ai/v1/graphql \
-H "Authorization: Bearer $BUILDER_TOKEN" \
-H "Content-Type: application/json" \
-H "x-hasura-role: builder" \
-d '{
"query": "mutation($input: BuilderFilesConfirmInput!) { BuilderFilesConfirm(input: $input) { code data { fileId filename bytes mimeType createdAt purpose } } }",
"variables": { "input": { "fileId": "YOUR_FILE_ID" } }
}'After confirmation, the file is available for use with BuilderEmbeddingsImportFiles and all other file operations.
Billing
- Uploads meter
file_upload_bytes - Downloads meter
file_download_bytes - Vision billing uses image dimensions for accurate token estimation