Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Hermoso Ai Tools API
version: '2026-08-26'
description: 'Operations tagged tools across 2 of this provider''s published API definitions: app_hermoso_ai.json, app_hermoso_ai_rest.json. Each path carries the servers of the definition it was published in.'
servers:
- url: https://app.hermoso.ai
security:
- bearerAuth: []
tags:
- name: Tools
paths:
/v1/tools:
get:
operationId: listTools
summary: List the callable tools
description: 'Everything this workspace can do over HTTP, grouped by area, each with its human-readable description and its JSON Schema. This is the browsable index: filter by `area`, search by substring, page with a cursor.
READ IT, DO NOT HARD-CODE IT. The set grows continuously and a tool absent here is not a missing capability — a tool whose connector this workspace has not linked is withheld, because it could only ever answer "not connected". `requires_connector` on every row names what to link.
CONTRACT TIER: this endpoint''s SHAPE is stable under the same promise as everything else in `/v1`. The TOOLS inside it are not frozen — their arguments improve as the tools do. Treat the roster as live data.'
tags:
- Tools
parameters:
- name: area
in: query
required: false
schema:
type: string
description: Filter to one area. `areas[]` in the response lists them with a description and a count.
- name: search
in: query
required: false
schema:
type: string
description: Case-insensitive substring over name, title and description.
- name: limit
in: query
required: false
schema:
type: integer
description: 1-200, default 25. Each row carries a JSON Schema, so a large page is a large response.
- name: starting_after
in: query
required: false
schema:
type: string
description: A tool name from a previous page; returns the page after it. An unknown cursor is an error, never a silent restart.
- name: X-Hermoso-User
in: header
required: false
schema:
type: string
description: 'Which profile to act on, when the account holds more than one: an `id` from `GET /v1/brands`. Defaults to the workspace the key is pinned to. Membership is re-authorised on every request, and an id this key cannot act on is a 404 `brand_not_found`, never an empty result.'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/ToolList'
'400':
description: Invalid request.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Missing, invalid or revoked key — or a channel that is not connected, in which case `error.connector` names it and `error.type` is `connector_error`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: '`error.code: brand_not_found`: `X-Hermoso-User` (or the profile this key is pinned to) names a profile this key cannot act on. `GET /v1/brands` lists the valid ids.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Rate limited. `Retry-After` says how long to wait.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
security:
- bearerAuth: []
servers:
- url: https://app.hermoso.ai
/v1/tools/{name}:
get:
operationId: getTool
summary: Retrieve one tool
description: 'One tool: what it does, which area it belongs to, whether it is read-only, which connector it needs, and the JSON Schema for its arguments. A tool withheld from the index because its connector is not linked is still described HERE, with `connector_connected: false` — so "what would I have to link to use this?" is answerable without linking anything first.'
tags:
- Tools
parameters:
- name: name
in: path
required: true
schema:
type: string
- name: X-Hermoso-User
in: header
required: false
schema:
type: string
description: 'Which profile to act on, when the account holds more than one: an `id` from `GET /v1/brands`. Defaults to the workspace the key is pinned to. Membership is re-authorised on every request, and an id this key cannot act on is a 404 `brand_not_found`, never an empty result.'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/Tool'
'400':
description: Invalid request.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Missing, invalid or revoked key — or a channel that is not connected, in which case `error.connector` names it and `error.type` is `connector_error`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: '`error.code: brand_not_found`: `X-Hermoso-User` (or the profile this key is pinned to) names a profile this key cannot act on. `GET /v1/brands` lists the valid ids.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Rate limited. `Retry-After` says how long to wait.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
security:
- bearerAuth: []
post:
operationId: callTool
summary: Call a tool
description: 'Invoke a tool with a JSON body of its arguments and get its result. The body IS the argument object — there is no envelope to wrap it in.
UNKNOWN ARGUMENTS are never silently dropped. A key the tool does not declare that looks like a misspelling or synonym of one it does (`pagId`, `platform` for `target`) is refused with `400 invalid_arguments`, and `error.unknown_arguments` lists each one as `{name, did_you_mean}`, the suggestion always being an argument of THIS tool. Any other undeclared key runs the call without it: the result text starts with a line naming it, and the response carries `ignored_arguments: [{name, did_you_mean}]` (the same list is `data.ignoredArguments`). Keys starting with `_` are host metadata and are never named.
The same handler the MCP server and the CLI run, on the same credential, through the same spend gate: a tool that costs credits costs the same credits here, and one that publishes publishes to the same connected account. Send an `Idempotency-Key` on anything that spends or publishes and a retry is safe.
The reply carries BOTH halves of the tool''s answer: `text` is the sentence (what an agent reads) and `data` is the structured result (what an integration reads). A tool that refuses answers with an HTTP error in the standard envelope, never a 200 with a failure inside it.
CONTRACT TIER: the envelope here — the route, the auth, the errors, the rate-limit headers, the idempotency semantics — is stable. A tool''s own arguments are not frozen: they improve with the tool. Read `GET /v1/tools/{name}` rather than pinning a schema.'
tags:
- Tools
parameters:
- name: name
in: path
required: true
schema:
type: string
- name: callback_url
in: query
required: false
schema:
type: string
description: 'The same as the `X-Hermoso-Callback-Url` header, for a client that cannot set headers. Prefer the header: a callback URL is a secret, and query strings are logged. A callback implies `wait=0` unless you send `wait` yourself.'
- name: wait
in: query
required: false
schema:
type: string
description: 'How long to hold this request while a render finishes. Only tools that render through the job queue are affected: video, avatars, template ads, post-production, and image generation (`generate_image` is queued as an image job whenever `wait` is sent, including `wait=0`). Every other tool answers as it always does. OMIT IT and nothing changes: a render is held for up to 45 seconds and then answered 200 with `data.stillRendering: true` and a `data.jobId`. `wait=0` (or `false`) answers 202 with a job handle the moment the job is queued, which is what a platform with a 30-second step limit wants. `wait=<seconds>` holds at most that long, capped at 45, and answers 202 if the render is not finished by then. Not waiting never changes what a render costs or whether it runs: the job is queued, checked against your balance and billed exactly as it is when you wait. Poll `GET /v1/jobs/{id}`.'
- name: X-Hermoso-Callback-Url
in: header
required: false
schema:
type: string
format: uri
description: 'An https:// URL to POST the outcome to, for a platform that cannot poll (for example Zapier `z.generateCallbackUrl()`). When the call queued a job, Hermoso POSTs the job resource — exactly what `GET /v1/jobs/{id}` answers — once the job is `done` or `error`, with headers `Hermoso-Event: job.done | job.error` and `Hermoso-Delivery` (also sent as `Idempotency-Key`; drop a repeat). A 2xx ends delivery; a 4xx other than 408, 425 and 429 is final; anything else is retried for about 16 hours. When the call finished inside the request, its response body is POSTed too (`Hermoso-Event: tool.result`). The response header `Hermoso-Callback` says what happened: `registered`, `sent`, `limit-reached` or `not-registered`. Nothing about what runs or what it costs changes: the URL is attached after the job is queued. A refusal is the HTTP error and nothing is POSTed. Must be https on a public host name; a replay with the same `Idempotency-Key` and a new URL adds that URL to the same job.'
- name: Idempotency-Key
in: header
required: false
schema:
type: string
description: 'Send a unique value to make this call safe to retry. A replay returns the original response with `Idempotency-Replayed: true`, for 24 hours — except a call that queued a job, whose replay is that job as it is now (what `GET /v1/jobs/{id}` answers), so a finished render replays as done with its file.'
- name: X-Hermoso-User
in: header
required: false
schema:
type: string
description: 'Which profile to act on, when the account holds more than one: an `id` from `GET /v1/brands`. Defaults to the workspace the key is pinned to. Membership is re-authorised on every request, and an id this key cannot act on is a 404 `brand_not_found`, never an empty result.'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ToolCall'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/ToolResult'
'202':
description: 'Only when `wait` was sent: the render is queued or still running. Poll `poll` (also sent as the `Location` header) until `status` is `done` or `error`. Do NOT call the tool again to check on it, that starts and bills a second render.'
content:
application/json:
schema:
$ref: '#/components/schemas/ToolJob'
'400':
description: Invalid request.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Missing, invalid or revoked key — or a channel that is not connected, in which case `error.connector` names it and `error.type` is `connector_error`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: '`error.code: brand_not_found`: `X-Hermoso-User` (or the profile this key is pinned to) names a profile this key cannot act on. `GET /v1/brands` lists the valid ids.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Rate limited. `Retry-After` says how long to wait.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
security:
- bearerAuth: []
servers:
- url: https://app.hermoso.ai
components:
schemas:
ToolList:
type: object
properties:
object:
type: string
enum:
- list
areas:
type: array
description: Every area holding at least one tool available to this workspace, so one call shows the whole map before you page through it.
items:
type: object
properties:
object:
type: string
enum:
- tool_area
id:
type: string
description:
type: string
tool_count:
type: integer
data:
type: array
items:
$ref: '#/components/schemas/Tool'
total:
type: integer
description: Tools matching this filter, across all pages.
has_more:
type: boolean
next_cursor:
type: string
nullable: true
description: Pass it back as `starting_after`.
limit:
type: integer
ToolJob:
type: object
description: A handle to a render that is queued or still running, answered with HTTP 202 when `wait` was sent.
properties:
object:
type: string
enum:
- tool_job
tool:
type: string
description: The tool that was called.
job_id:
type: string
status:
type: string
description: queued or running at the moment of the reply. This enum MAY GROW.
poll:
type: string
description: 'The path to poll: /v1/jobs/{id}. Also sent as the `Location` header.'
poll_url:
type: string
description: The same, as an absolute URL.
retry_after_seconds:
type: integer
description: A sensible gap between polls. Also sent as `Retry-After`.
text:
type: string
description: The tool’s own sentence about the queued render.
ToolResult:
type: object
properties:
object:
type: string
enum:
- tool_result
tool:
type: string
text:
type: string
description: The tool’s answer as a sentence — what a model would read. Always present.
data:
type: object
nullable: true
description: The structured result, when the tool produces one; null when its answer is only prose. Its shape is the tool’s own and moves with the tool.
ignored_arguments:
type: array
items:
type: object
properties:
name:
type: string
description: The key that was sent.
did_you_mean:
type: string
nullable: true
description: An argument THIS tool declares that the key most likely meant, or null.
description: 'Present when the call ran WITHOUT keys the tool does not take: each one, with the argument it most likely meant. The text starts with the same news, and data.ignoredArguments carries it too.'
ToolCall:
type: object
description: 'The TOOL’S OWN arguments, as a flat object. There is no wrapper: to call a tool that takes `query`, send `{"query":"..."}`. Read the shape from GET /v1/tools/{name}.'
additionalProperties: true
Tool:
type: object
properties:
object:
type: string
enum:
- tool
name:
type: string
description: Call it at POST /v1/tools/{name}.
title:
type: string
area:
type: string
nullable: true
description: Which area of the product it belongs to. THIS SET MAY GROW — tolerate a member you have not seen.
description:
type: string
description: 'What it does, written for whoever is choosing: a person reading the index or a model reading the roster.'
read_only:
type: boolean
description: It reads and changes nothing. Read-only tools are metered on the READ rate-limit budget.
destructive:
type: boolean
description: It can remove or overwrite something. Destructive tools carry their own confirmation arguments — read the schema.
idempotent:
type: boolean
description: Calling it twice with the same arguments has the same effect as calling it once. Independent of `Idempotency-Key`, which protects a retry regardless.
requires_connector:
type: string
nullable: true
description: 'The third-party account this tool needs linked, or null when it needs none. Linking is a browser flow: an OAuth consent screen cannot be completed headlessly.'
connector_connected:
type: boolean
nullable: true
description: Whether that connector is linked in this workspace. NULL means the connections could not be read, which is NOT the same as "no" — never render it as a refusal.
rate_class:
type: string
enum:
- read
- write
description: Which rate-limit budget a call to it spends.
invoke:
type: object
properties:
method:
type: string
path:
type: string
input_schema:
type: object
description: JSON Schema for the request body of POST /v1/tools/{name}. The body IS the argument object.
Error:
type: object
required:
- error
properties:
error:
type: object
required:
- type
- code
- message
properties:
type:
type: string
enum:
- authentication_error
- permission_error
- invalid_request_error
- connector_error
- rate_limit_error
- insufficient_credits
- api_error
description: The coarse family — branch on this. New CODES land inside an existing type wherever possible so that branch keeps working; the enum is published for code generation, and a client should still fall through gracefully on a member it does not recognise.
code:
type: string
description: The specific reason. Log this. New values may appear at any time.
message:
type: string
param:
type: string
description: The offending field, when there is one.
connector:
type: string
description: Present when the reason is a channel that is not connected. Reconnect in the app — an OAuth consent screen cannot be completed headlessly.
credits_needed:
type: number
description: 'On `insufficient_credits` from a recast or an edit: what the request needs.'
balance:
type: number
description: 'On `insufficient_credits` from a recast or an edit: what this workspace has.'
unknown_arguments:
type: array
items:
type: object
properties:
name:
type: string
description: The key that was sent.
did_you_mean:
type: string
nullable: true
description: An argument THIS tool declares that the key most likely meant, or null.
description: 'On `invalid_arguments` from POST /v1/tools/{name}: each key the tool does not take, with the argument it most likely meant. Nothing was run.'
upgrade_url:
type: string
description: 'On `real_face_needs_paid`: where to choose a paid plan.'
alternative:
type: string
description: 'On `real_face_needs_paid`: the field that works on every plan instead (`person.describe`).'
person_url:
type: string
description: 'On a recast with `person.describe` whose recast step was refused after the person was made: the AI person, in your Library. Send it as `person.image_url` to retry without making a new one.'
request_id:
type: string
nullable: true
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: hmk_...
description: A Hermoso API key, or an access token from the OAuth authorization-code flow (which is the same thing).
x-refined-from:
- app_hermoso_ai.json
- app_hermoso_ai_rest.json