Create a skill
POST /v1/skills
Creates a skill from uploaded files. The skill belongs to the owner of the API key.
Send multipart/form-data with the skill’s files in files[]. To upload a zip archive instead, send it as the
only files[] part. For the rules an upload must follow, see
Skills. Only the key’s owner can see
the skill until they share it in the Context app.
If the request fails partway, the server deletes the skill, so no partial skill is left. If your organization
uses the newer permissions model, a skill uploaded with files besides SKILL.md is saved as two versions:
SKILL.md alone, then the whole upload. latest_version_id always points to the version that holds the whole
upload.
The key must be a personal key with the skills:write capability. For what each capability allows, see Keys.
Body (multipart/form-data)
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
files | array<binary> | Yes | The skill’s files. On the wire each file is one files[] part, with the file’s path in the skill as the filename. The SDKs send the files parameter this way. You can instead send one zip archive that contains all the files. | |
display_name | string | No | The skill’s display name. Must equal the name in SKILL.md. |
Response
Returns 200 with the skill object. For every field, see Skills.
{ "id": "8b3f6c1e-2a4d-4f7b-9c1e-5d6a7b8c9d0e", "type": "skill", "created_at": "2026-10-27T15:02:11.204Z", "display_name": "pr-summary", "latest_version_id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e", "source": { "type": "custom" }, "updated_at": "2026-10-27T15:02:11.204Z"}Errors
This endpoint can return these errors as well as the common errors.
| Status | error.type | Cause and fix |
|---|---|---|
| 400 | invalid_request_error | The upload breaks a rule: SKILL.md is missing or not at the root, name or description is missing or invalid, a path is unsafe, or the files total over 30 MiB (31,457,280 bytes) uncompressed. The message names the rule. Fix the files and upload again. |
| 400 | invalid_request_error | display_name is not the name in SKILL.md. Leave it out, or send that name. |
| 400 | invalid_request_error | The form has a field other than files[] and display_name, or the request has a query parameter. Remove it. |
| 403 | permission_error | The key lacks skills:write, is not a personal key, or its owner doesn’t have permission to create skills in this organization. Use a personal key with skills:write. |
| 409 | invalid_request_error | The key’s owner already has a skill with this name. The message gives its ID. Upload a new version of that skill, or change name in SKILL.md. |
| 413 | request_too_large | The request is over 32 MiB (33,554,432 bytes). Remove files until the request is under 32 MiB. |
| 429 | rate_limit_error | This key already has 2 skill uploads in progress, or the server is processing too many uploads. Wait for the time in Retry-After, then retry. The SDKs retry it for you. |
Example
curl -sS -X POST "$CONTEXT_BASE_URL/v1/skills" -H "x-api-key: $CONTEXT_API_KEY" \ -F "files[]=@pr-summary/SKILL.md;filename=pr-summary/SKILL.md" \ -F "files[]=@pr-summary/summarize.py;filename=pr-summary/summarize.py"skill = client.beta.skills.create( files=[ ("pr-summary/SKILL.md", open("pr-summary/SKILL.md", "rb")), ("pr-summary/summarize.py", open("pr-summary/summarize.py", "rb")), ],)print(skill.id, skill.latest_version_id)import { readFile } from "node:fs/promises"import { toFile } from "@anthropic-ai/sdk"
const skill = await client.beta.skills.create({ files: [ await toFile(await readFile("pr-summary/SKILL.md"), "pr-summary/SKILL.md"), await toFile(await readFile("pr-summary/summarize.py"), "pr-summary/summarize.py"), ],})