Blog

Claude API Agent Skills: use a ready-made skill without Claude Code

Markdown

Connect a pptx skill with container.skills and code execution, inspect the response, and download its presentation through the Claude Files API.

Read full article

Key takeaway

Ready-made Agent Skills for documents can be used through the direct Claude API: specify the skill in container.skills and enable code_execution. For your first request, use the compatible model and tool pairing from the official example. Download the generated file separately using its file_id; the example uses the same API key for the request and the download.

You can find additional instructions in the Ailoot documentation.

What do you need to use Claude API Agent Skills?

This guide is for calling the API directly from a program, rather than installing a skill in Claude Code. You need a Claude API key, access to the selected model, and Python 3. The code below uses the Python standard library and HTTP, so it does not depend on which SDK version you have installed. The key is passed through the ANTHROPIC_API_KEY environment variable; do not put it in the program text or a conversation.

The API offers ready-made Anthropic skills: pptx for presentations, xlsx for spreadsheets, docx for Word documents, and pdf for PDFs. They are connected using type: "anthropic". This guide uses a ready-made skill, without uploading a custom one.

The official quickstart, opened when this material was prepared, uses the pairing claude-opus-5-5 and code_execution_20260521. That is the pairing used below. This example has been checked against the documentation; it is not a report of a paid API run.

Which beta headers are needed

The example follows the quickstart's current HTTP contract: the headers are x-api-key, anthropic-version: 2023-06-01, and content-type: application/json; it does not send anthropic-beta. Do not combine a model, a calling method, and a response format from different generations of the documentation.

The Files API documentation also states that the API is out of beta. The old files-api-2025-04-14 header retains the old response formats; removing the header switches them to the current formats. That is why an old example using client.beta.files cannot simply be combined with a new one.

We are not migrating an existing service from the old beta contract here. If you already have such a service, check its headers and response formats first. The word beta in an SDK method name is not enough on its own: the Files API documentation separately explains how different SDK versions behave.

Create a presentation and download the file

The example sends a task to create five slides about renewable energy, saves the entire response to skill-response.json, and then extracts the IDs of the generated files. It does not take a save path from the model's response: the files are saved in a new local folder called skill-output. If that folder already exists, the program stops so that it does not overwrite the previous result. The folder is created before the API call, so it also remains after a failed call. Inspect and retain that attempt before doing anything again; do not delete it merely to make a rerun pass.

import json
import os
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen

API = "https://api.anthropic.com/v1"
KEY = os.environ["ANTHROPIC_API_KEY"]
HEADERS = {
    "x-api-key": KEY,
    "anthropic-version": "2023-06-01",
    "content-type": "application/json",
}

def request(path, payload=None, binary=False):
    data = None if payload is None else json.dumps(payload).encode("utf-8")
    req = Request(API + path, data=data, headers=HEADERS)
    try:
        with urlopen(req, timeout=600) as result:
            body = result.read()
    except HTTPError as error:
        try:
            details = json.loads(error.read().decode("utf-8"))
        except (ValueError, UnicodeError):
            details = {}
        info = details.get("error", {})
        request_id = error.headers.get("request-id") or details.get("request_id")
        diagnostic = {
            "http_status": error.code,
            "request_id": request_id,
            "error_type": info.get("type"),
            "message": str(info.get("message", "No JSON error message")).replace(KEY, "[REDACTED]")[:1000],
        }
        (out / "skill-error.json").write_text(
            json.dumps(diagnostic, ensure_ascii=False, indent=2), encoding="utf-8"
        )
        raise RuntimeError(
            f"Claude API HTTP {error.code}; request_id={request_id}; inspect skill-error.json"
        ) from None
    return body if binary else json.loads(body)

out = Path("skill-output")
out.mkdir(exist_ok=False)
response = request("/messages", {
    "model": "claude-opus-5-5",
    "max_tokens": 16000,
    "container": {
        "skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}]
    },
    "messages": [{
        "role": "user",
        "content": "Create a presentation about renewable energy with 5 slides in my preferred language.",
    }],
    "tools": [{"type": "code_execution_20260521", "name": "code_execution"}],
})
(out / "skill-response.json").write_text(
    json.dumps(response, ensure_ascii=False, indent=2), encoding="utf-8"
)
print("stop_reason:", response.get("stop_reason"))

file_ids = []
for block in response["content"]:
    if block["type"] == "bash_code_execution_tool_result":
        result = block["content"]
        if result["type"] == "bash_code_execution_result":
            for item in result["content"]:
                file_id = item.get("file_id")
                if file_id and file_id not in file_ids:
                    file_ids.append(file_id)
if not file_ids:
    raise RuntimeError("No generated file_id; inspect skill-response.json")

for index, file_id in enumerate(file_ids, start=1):
    from urllib.parse import quote
    file_path = "/files/" + quote(file_id, safe="")
    metadata = request(file_path)
    if metadata.get("downloadable") is not True:
        raise RuntimeError("This file cannot be downloaded through Files API")
    target = out / f"result-{index}.pptx"
    target.write_bytes(request(file_path + "/content", binary=True))
    print("Saved:", target)

The names result-1.pptx and so on were chosen for this presentation task. A file extension does not prove what the file contains: open the downloaded document and check its format, slide count, and text. Do not treat HTTP 200 alone, or the presence of a file_id, as a successful result.

The program prints stop_reason, the reason the response stopped, and saves the full response for inspection. It does not continue the conversation or automatically repeat the paid request. Having a downloaded file does not mean the whole task is complete: inspect the saved response and the document itself before deciding whether another request is needed.

For a failed attempt, inspect skill-error.json if present. It records the HTTP status, request ID when supplied, and a bounded provider error message with the configured API key removed. Review the message for other private content before sharing it. Compare authentication errors, permission/model errors, billing limits, and request-format errors with the official error reference; do not infer that all failures need another paid generation.

If skill-response.json already contains generated file_id values and only a download failed, retain that response and diagnose the file metadata/download step using the same authorized workspace access. Do not run the generation block again just to download an existing output. If the request outcome is unknown, preserve its request ID and resolve its status before sending another generation. Only after deciding that a new generation is needed, change the local output line to an unused path, for example out = Path("skill-output-attempt-2"), and retain exist_ok=False. A fresh folder prevents overwriting; it does not prevent a duplicate API charge.

What file permissions are needed

The Files API associates files with a workspace, rather than with an individual user of your application or a conversation. According to the documentation, any API key with access to that workspace can access files uploaded there. The example uses the same key for downloading as for generation. Never accept a file_id from end users or other untrusted sources. Store these IDs on the server and maintain the mapping between users and their files in your own application.

Check the file metadata separately: it must contain downloadable: true. The documentation allows downloading files generated by skills or the code execution tool; files you upload have downloadable: false, and an attempted download returns 400. Access to the workspace and the ability to download a particular file are separate requirements.

For a multi-user application, the Files API documentation recommends a separate workspace for each client as the file isolation boundary. This recommendation is specifically about files; do not extend it to all entities and permissions without checking the relevant API.

Ready-made skills and custom skills

Ready-made skills have the IDs pptx, xlsx, docx, and pdf. In the skills list reference, the custom source is described as private to the workspace, while anthropic is shared and read-only.

version: "latest" in the first example means the latest published version of the skill. For a reproducible integration, bear in mind that this selection can change; switching to a fixed version requires choosing an existing version of the skill you use, rather than an invented ID from an example.

Limitations and the next step

The example covers only creating a presentation with a ready-made skill and downloading it. Uploading custom skills, deleting them, and accessing third-party services have not been tested here. Those tasks require a separate check of the relevant API.

If your task involves files in the Claude interface, there is a separate guide on how to upload a file to Claude. For direct calls, see how to calculate API token usage. For work in a terminal agent, read how to choose Claude Code skills: they are connected in a different way.

For the first check, one ready-made pptx skill is enough: inspect the response, download a file that is permitted to be downloaded, and open it. Add custom skills and access separation between clients after that check.

When you are ready, get to know Ailoot.

More details

Questions about Claude API Agent Skills

Why is a file_id not enough to declare the presentation finished?

The ID shows that a file was returned, not that it is a usable presentation. Check its metadata for downloadable: true, download it with workspace access, and open it to verify the file format, slide count, and text.

Can I download any file uploaded to the Claude Files API?

No. The guide distinguishes generated files marked downloadable: true from uploaded files marked downloadable: false. Workspace access and permission to download a particular file are separate checks; do not accept arbitrary file IDs from end users.

All articles