OpenTools docs

Give your agent 203 tools through one connection: documents, media, browsers, AI models, inboxes, people, schedules and proof. Use MCP if your agent supports it, or the REST API from your own code. Both use the same API key and the same tools.

Quickstart

  1. Create a free account. You get a workspace with 1,000 runs a month.
  2. Create an API key under API keys. It starts with ot_live_ and is shown once.
  3. Add OpenTools to your agent over MCP, or call the REST API.
run a tool
curl https://opentools.opentools.workers.dev/v1/jobs \
  -H "Authorization: Bearer $OPENTOOLS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tool": "pdf.merge", "input": {"files": [{"url": "https://example.com/a.pdf"}, {"url": "https://example.com/b.pdf"}]}}'

Connect an agent (MCP)

The MCP endpoint is https://opentools.opentools.workers.dev/mcp (streamable HTTP). Authenticate with your API key as a bearer token. The catalog is too big to list every tool to your agent up front, so it finds them with search_tools, reads one with describe_tool and runs it with run_tool, alongside run_batch, get_batch and get_job. To list tools directly, add ?tools= to the URL with ids, prefixes, categories or a use-case bundle (?tools=pdf.*,images, ?tools=bundle:web-research), or ?tools=all. Direct tools are named with dots replaced by underscores (pdf.merge → pdf_merge).

Claude Code
claude mcp add --transport http opentools https://opentools.opentools.workers.dev/mcp \
  --header "Authorization: Bearer $OPENTOOLS_KEY"
Cursor · .cursor/mcp.json
{
  "mcpServers": {
    "opentools": {
      "url": "https://opentools.opentools.workers.dev/mcp",
      "headers": {
        "Authorization": "Bearer ot_live_…"
      }
    }
  }
}
Claude Desktop · claude_desktop_config.json
{
  "mcpServers": {
    "opentools": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://opentools.opentools.workers.dev/mcp",
        "--header",
        "Authorization: Bearer ot_live_…"
      ]
    }
  }
}

REST API

Base URL https://opentools.opentools.workers.dev/v1. Send Authorization: Bearer <key> on every request. Bodies are JSON.

GET/v1/toolsList tools. Add ?include=schema for each tool’s JSON input schema. No key needed.
GET/v1/tools/:idOne tool, with its input schema.
POST/v1/jobsRun a tool. Body: { "tool": "…", "input": { … }, "wait": true }. With wait: true (the default) the response is the finished job. With wait: false you get 202 and a queued job to poll.
GET/v1/jobs/:idA job’s status, output and fresh download links.
GET/v1/jobsRecent jobs. Filters: status, tool, limit (max 100), before (an ISO timestamp, for paging).
a finished job
{
  "id": "5d0c…",
  "object": "job",
  "tool": "image.remove_bg",
  "status": "succeeded",
  "duration_ms": 1840,
  "attempts": 1,
  "credits": 2,
  "output": {
    "data": {
      "width": 2400,
      "height": 3200,
      "transparent": true
    },
    "files": [
      {
        "id": "a81f…",
        "name": "bottle-cutout.png",
        "content_type": "image/png",
        "size_bytes": 1203344,
        "url": "https://…signed…",
        "expires_at": "…"
      }
    ]
  },
  "error": null
}

Files

Anywhere a tool takes a file, pass one of:

  • { "url": "https://…" } — a public link. Best for agents.
  • { "file_id": "…" } — a file you uploaded to POST /v1/files.
  • { "base64": "…", "name": "scan.pdf" } — inline bytes, up to 10 MB.
upload
curl https://opentools.opentools.workers.dev/v1/files \
  -H "Authorization: Bearer $OPENTOOLS_KEY" \
  -F file=@contract.pdf

Output files come back with signed download links that last one hour; call GET /v1/jobs/:id for fresh ones. Inputs and outputs are deleted after your workspace’s retention period (24 hours by default, up to 30 days).

Drive

Tool files expire. The Drive is where agents keep the ones worth keeping: reports, results and artefacts, in folders, with no expiry. People browse and open them in the app (images, video, PDFs, spreadsheets, Markdown and text all preview in the browser), and a Drive file’s id works as { "file_id": "…" } input to any tool.

Over MCP, use drive_save, drive_list, drive_read, drive_move and drive_delete. Paths look like reports/2026/q3.csv, and missing folders are created. When a name is taken, if_exists picks rename (the default), replace or fail.

save a tool's output
curl https://opentools.opentools.workers.dev/v1/drive \
  -H "Authorization: Bearer $OPENTOOLS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path": "reports/2026/q3.pdf", "file": {"file_id": "…"},
       "description": "Q3 board report"}'
upload, list and read
curl "https://opentools.opentools.workers.dev/v1/drive?path=reports/2026/q3.csv" \
  -H "Authorization: Bearer $OPENTOOLS_KEY" --data-binary @q3.csv

curl "https://opentools.opentools.workers.dev/v1/drive?path=reports&recursive=true" -H "Authorization: Bearer $OPENTOOLS_KEY"

curl "https://opentools.opentools.workers.dev/v1/drive/FILE_ID?text=true" -H "Authorization: Bearer $OPENTOOLS_KEY"

Saved files come back with a one-hour download url and an app_url to hand to a person. POST /v1/drive with {"action": "mkdir" | "move", …} organises it, and DELETE /v1/drive?path=… removes a file or a whole folder.

Batches

Run one tool over many inputs: up to 1,000 on Free, 25,000 on Pro and 250,000 on Scale. Every input becomes its own job, queued durably and retried independently, so one bad file never fails the rest.

start a batch
curl https://opentools.opentools.workers.dev/v1/batches \
  -H "Authorization: Bearer $OPENTOOLS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tool": "image.remove_bg", "inputs": [
        {"file": {"url": "https://example.com/1.jpg"}},
        {"file": {"url": "https://example.com/2.jpg"}}
      ]}'

Poll GET /v1/batches/:id for progress (succeeded, failed, pending), or pass webhook_url to be told when it’s done. Every input’s result is at GET /v1/batches/:id/results (JSON, or ?format=csv), in input order; page with ?after=. POST /v1/batches/:id/cancel stops one.

Bulk runs over storage

For thousands of files, don’t send them at all. Point a batch at a folder in a storage connection with from: OpenTools lists it page by page, starts each file as soon as it’s found and, with to, writes every output back. Shared options go in input.

OCR a whole folder
curl https://opentools.opentools.workers.dev/v1/batches \
  -H "Authorization: Bearer $OPENTOOLS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tool": "doc.ocr",
    "name": "Q3 scans",
    "from": { "connection": "archive", "path": "scans/2026", "match": ["*.pdf", "*.tiff"] },
    "input": { "language": "eng" },
    "to": { "connection": "archive", "path": "searchable/{dir}/{stem}.pdf", "overwrite": "skip" },
    "webhook_url": "https://example.com/hooks/opentools"
  }'
  • match takes patterns like *.pdf; one with a slash matches the whole path, and ** crosses folders. recursive (default true) includes subfolders.
  • In to.path, {dir} is the file’s folder under from.path, so the output mirrors the source tree. {name}, {stem}, {ext}, {date} and {job} work too.
  • overwrite is rename (default), skip or overwrite. A results CSV is written next to the outputs unless to.results is false.
  • While the folder is being read, the batch has listing: true and total keeps growing. Jobs run in parallel: 20 at once on Free, 50 on Pro, 200 on Scale.

Agents do the same with run_batch over MCP: pass from and to instead of inputs.

Webhooks

When a batch finishes (succeeded, failed or canceled), OpenTools POSTs { "type": "batch.finished", "data": <batch> } to its webhook_url, retrying with backoff until it gets a 2xx. The batch’s response includes a webhook.secret once; each delivery is signed with it in the OpenTools-Signature header.

verify a delivery (Node)
import { createHmac, timingSafeEqual } from "node:crypto";

// header: "t=1767225600,v1=5f2b…"
function verify(secret, rawBody, header) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Errors & retries

Errors are JSON: { "error": { "code": "invalid_input", "message": "…" } }. Bad input (invalid_input, not_found) fails straight away. Transient problems — a slow source URL, a busy service — are retried three times with backoff before a job is marked failed. You’re only charged for runs that succeed.

Limits

  • 100 MB per file (10 MB inline base64)
  • 1,000 inputs per batch on Free, 25,000 on Pro, 250,000 on Scale
  • Free workspaces: 1,000 runs a month
  • Results kept 24 hours by default