Files (/v1/files) and Skills (/v1/skills) are Project resources with their own lifecycle, independent of Sessions. A File holds uploaded bytes that Environments copy by ID. A Skill holds immutable, versioned bundles that Templates and Sessions reference. Every API key of a Project shares them.
These routes follow the SDK pinned in upstream.json: the Files resource, create parameters, FileObject and the Skills resource. They need a Project API key and no OpenAI-Beta header. A missing ID and another Project's ID return the same 404.
Files
| Operation | Behavior |
|---|---|
POST /files | Multipart upload with one file part and purpose=user_data, in either order. Returns 200 with the File |
GET /files | Lists the Project's Files without reading their bytes |
GET /files/{file_id} | Returns the File |
GET /files/{file_id}/content | 400 Not allowed to download files of purpose: user_data, with a null code and param. The ID is checked first, so a missing File returns 404 |
DELETE /files/{file_id} | Deletes the File and its bytes; returns {"id": …, "object": "file", "deleted": true} |
Use a File by passing its ID to Environment files or to a Template's or Session's initial files (Environments). Those copies read the bytes internally; the public download stays refused.
Upload
- Only
purpose=user_datais accepted. Other purposes,expires_afterand the Uploads API are not supported. - The file may be empty and holds up to 512 MiB; the whole multipart body may exceed that by 64 KiB. A larger upload returns 413
request_too_large. The transfer must finish within five minutes. - A missing, repeated or unknown part, a
Content-EncodingorContent-Transfer-Encodingheader, or a filename that is empty, longer than 1,024 bytes, not UTF-8 or contains NUL returns 400. Core stores nothing until the whole request validates. - Core does not deduplicate uploads. After a lost response, list Files before uploading again.
File object
| Field | Value |
|---|---|
id, object | File ID; file |
bytes | Size in bytes |
created_at | Unix seconds |
filename | The uploaded name. It is metadata only and never becomes a filesystem path |
purpose | user_data |
status | processed, meaning the bytes are stored. Core does not parse, index or scan them |
expires_at, status_details | null |
List Files
| Parameter | Rule |
|---|---|
order | desc (default) or asc, by creation time, then ID |
after | ID of a File this Project can see |
purpose | One of user_data, assistants, batch, fine-tune, vision, evals, assistants_output, batch_output, fine-tune-results. Any other value, including a different case, returns 400 with param: "purpose" before the cursor is resolved. Values other than user_data return an empty page. An empty value means no filter |
The response is {"object": "list", "data": [...], "first_id", "last_id", "has_more"}; an empty page has null IDs. limit, query parsing and their errors follow the shared list rules.
Errors
A missing or foreign File returns 404 with type invalid_request_error, a null code and param: "id" for retrieve, content and delete.
Storage and deletion
Core stores File bytes as PostgreSQL large objects in its own database. An upload and a deletion each commit in one transaction, so a failure leaves neither partial bytes nor metadata. Back up the database with its large objects; deleting a File does not remove it from write-ahead logs or earlier backups.
The source Files schema refuses a downgrade while File rows remain. Delete Files through the API first so their large objects are removed.
A copy into a workspace reads a consistent snapshot of the File and can finish after the File is deleted; later lookups fail. Deleting a File never changes a workspace copy.
Skills
| Operation | Behavior |
|---|---|
POST /skills | Uploads a new Skill. Its first version is both default and latest |
POST /skills/{skill_id}/versions | Uploads a new version. A default form field of true makes it the default; false or omitted leaves the default unchanged |
GET /skills, GET /skills/{skill_id} | Skill metadata, without decrypting any bundle |
POST /skills/{skill_id} | {"default_version": "<n>"} changes the default version |
DELETE /skills/{skill_id} | Deletes the Skill and every version |
GET /skills/{skill_id}/content | ZIP of the default version |
GET /skills/{skill_id}/versions, GET /skills/{skill_id}/versions/{version} | Version metadata. The list orders by version number, and after is a version ID (skillver_…), not a number |
GET /skills/{skill_id}/versions/{version}/content | ZIP of that version |
DELETE /skills/{skill_id}/versions/{version} | See Delete a version |
limit, cursors and query errors follow the shared list rules.
Upload a bundle
Send one ZIP as a files part, or a directory as repeated files[] parts whose filenames are relative paths such as report/SKILL.md. SDK 3.13.0 sends no part when files is a single file rather than a list, so upload a single ZIP with plain HTTP:
curl "$OPENAI_BASE_URL/skills" -H "Authorization: Bearer $OPENAI_API_KEY" -F files=@report.zipA bundle has one top-level folder containing SKILL.md and any supporting files:
SKILL.mdis UTF-8, at most 256 KiB, and starts with YAML front matter.nameis required: lowercase letters and digits, optionally separated by single-or_, at most 64 characters.descriptionis required and non-empty.license,compatibilityand a string-valuedmetadatamap are optional; any other key is rejected.- Entries are regular files or directories with clean relative paths. Links, special files, absolute paths,
..components and duplicates are rejected. - Limits: 5 MiB compressed, 20 MiB expanded, 500 files, and 1,000 ZIP entries including directories.
Core encrypts each version's bundle bound to its Project, Skill and version. ZIP uploads keep executable bits; directory uploads store files with mode 0644.
Versions and metadata
- Version numbers start at 1, increase by one per upload and are never reused, even after the latest version is deleted. Template and Session selectors name versions by number, so a reused number could point a stored selector at different bytes.
- The Skill's
nameanddescriptionare those of its default version. Changing the default, byPOST /skills/{skill_id}or by uploading withdefault=true, updates the pointer and both fields together;idandcreated_atstay the same. latest_versionis the highest remaining version.- Uploads and deletions of one Skill run one at a time, so a deletion never removes a version whose upload was acknowledged.
How Templates and Sessions select a version (default, latest or a number) and freeze its bytes is in Environments.
Delete a version
| Version | Result |
|---|---|
| The default, and the only version | 200 {"id": "skillver_…", "object": "skill.version.deleted", "deleted": true, "version": "1"}. The Skill is deleted in the same transaction |
| The default, while other versions exist | 400, type invalid_request_error, code invalid_value, param: "version", Cannot delete the default skill version. |
| Any other version | 200 with the same body. If it was the latest, latest_version falls back to the highest remaining |
| A missing or foreign Skill or version | 404 |
Deleting a Skill or a version does not change Sessions that already installed it; Templates keep the reference they stored.