Connect a REST API

Import the vendor's OpenAPI document, choose and classify operations, shape their results, preview a call, then attach the tool to an agent or serve it from a hosted API
Connect a REST API to my agent
Connect a vendor’s REST API to my Speechify agent as an OpenAPI tool: store the vendor’s OAuth client credentials in the vault, import its OpenAPI document, choose the read operations the agent needs and classify them, add response mappings that keep only the fields it needs, preview one call, then create the tool and attach it to my agent. Follow [https://docs.speechify.ai/build/guides/hosted-apis/connect-a-rest-api.md](https://docs.speechify.ai/build/guides/hosted-apis/connect-a-rest-api.md).

Agents is in beta. Shapes on /v1/agents/* and /v1/credentials may change before general availability.

An openapi tool gives an agent a vendor’s REST API from the vendor’s own OpenAPI document. You pick the operations the agent may call, say what each one does to the world, and trim what comes back; Speechify’s servers make every call with a credential from your vault. There is no connector to write or host.

When to use it

The vendor offersUse
A REST API with an OpenAPI document, and one credential for your workspaceAn openapi tool (this page)
An MCP serverAn mcp tool
Nothing standard, or logic you need to own (per-user tokens, filtering, joins)A webhook tool or your own MCP server

What the platform handles for you: the OAuth token and its renewal, a per-minute ceiling you set once and every agent, run and route shares, a vendor 429 reported to the agent as a wait rather than a failure, and a result trimmed to the fields you chose. What it will not do is transform results with logic: there are no filters, conditionals or arithmetic, and anything richer belongs in your own connector.

1. Store the vendor credential

POST
/v1/credentials
curl -X POST https://api.speechify.ai/v1/credentials \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Vendor API client",
"kind": "oauth2_client_credentials",
"config": {
"oauth2_client_credentials": {
"token_url": "https://auth.vendor.example/oauth/token",
"client_id": "9f3c1e7a2b4d4c8e",
"client_secret": "<client-secret>",
"scopes": [
"issues:read"
],
"token_endpoint_auth_method": "client_secret_basic"
}
}
}'

For a vendor that issues OAuth client credentials, create a credential of kind oauth2_client_credentials:

{
"name": "Vendor API client",
"kind": "oauth2_client_credentials",
"config": {
"oauth2_client_credentials": {
"token_url": "https://auth.vendor.example/oauth/token",
"client_id": "9f3c1e7a2b4d4c8e",
"client_secret": "<client-secret>",
"scopes": ["issues:read"],
"token_endpoint_auth_method": "client_secret_basic"
}
}
}
  • token_endpoint_auth_method is how the platform authenticates to token_url: client_secret_post (the default) sends the id and secret in the form body, and client_secret_basic sends them in an HTTP Basic header. Use whichever the vendor’s documentation names.
  • scopes are sent space-separated as scope, and audience, when set, is sent as audience.
  • A vendor that issues a static API token instead takes a credential of kind bearer, sent as Authorization: Bearer <token>.

Keep the credential’s id. The secret is never returned again: every read shows client_secret_set: true instead. A credential with no project_id can back tools in every project; one scoped to a project can only back tools in that project.

2. Import the document

POST
/v1/agents/tool-definitions/import-openapi
curl -X POST https://api.speechify.ai/v1/agents/tool-definitions/import-openapi \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"spec_url": "https://api.vendor.example/v1/openapi.json",
"auth": {
"type": "oauth2_client_credentials",
"credential_id": "cred_01kc4p2q8r5s7t9v1w3x5y7z9a"
}
}'

Pass the document’s URL, and the credential when the vendor serves its document behind the same auth as its API:

curl -X POST https://api.speechify.ai/v1/agents/tool-definitions/import-openapi \
-H "Authorization: Bearer $SPEECHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"spec_url": "https://api.vendor.example/v1/openapi.json",
"auth": { "type": "oauth2_client_credentials", "credential_id": "cred_01kc4p2q8r5s7t9v1w3x5y7z9a" }
}'

Or paste the document inline as spec, a JSON object or a string holding JSON or YAML, up to 2 MiB. Nothing is saved.

Read the answer in this order:

  1. error: set when nothing compiled, with the reason (a refused URL, a credential that did not resolve, a fetch that failed, a document that does not parse). The call still answers 200.
  2. operations: every operation that compiled, in document order, each already in the shape the tool takes.
  3. skipped: every operation that could not be represented, with the reason, such as a request body that is not JSON.
  4. base_url: the document’s first https server. Use the vendor’s regional or tenant host instead when your account has one.

3. Choose and classify operations

Copy the operations the agent should have into the tool’s operations, at most 40 per tool. Fewer is better: a model picks more reliably from a dozen well-described functions than from a catalogue.

Keep each operation’s id as the import returned it, or rename it before saving; do not derive ids yourself, since the import collapses and suffixes them in ways the document does not show. The agent calls each operation as the function <tool name>__<operation id>, which must fit 64 characters. Shorten the tool name, or rename an operation’s id, when a pair does not fit.

Give every operation an action_class. It decides what runs without a person:

action_classMeansOn a durable run
readA lookup with no side effectRuns unattended
communicate_internalWrites inside your own estateRuns unattended
communicate_externalReaches a third partyWaits for approval
data_exportMoves a data set outWaits for approval
financialMoves money or commits to a chargeWaits for approval
irreversible_otherAnything else that cannot be undoneWaits for approval

An operation that declares nothing is read when it is a GET or HEAD, and otherwise takes the tool’s own action_class, which is irreversible_other when the tool declares none. Only a read operation can be previewed in step 5, and only a read operation on a tool whose approval is null or auto can be served from a hosted API route. A live voice or text session cannot pause for approval, so an operation that needs one is refused there and the agent is told it can run from a durable run.

The document’s security schemes are not imported: the tool authenticates with its own auth. If the document lists an API key as a header or query parameter, do not keep that parameter, because the agent would have to supply the key’s value. Only bearer and oauth2_client_credentials auth are supported.

4. Shape what the agent reads

A vendor’s answer is often far larger than what the agent needs, and the agent reads only a bounded window of it. Add a response mapping to an operation to decide which fields survive:

"response": {
"max_items": 50,
"pick": ["issues[].id", "issues[].title", "issues[].severity"],
"derive": { "issues[].url": "https://app.vendor.example/issues/{{id}}" }
}

The three steps apply in this order:

  1. max_items keeps the first N elements of a root array, or of every top-level array of a root object.
  2. pick keeps only the named paths, up to 50. issues[].id keeps issues as an array whose objects hold only id; [].id addresses a root array.
  3. derive adds string fields, up to 20. The key is where the field lands, and {{path}} placeholders in the value resolve relative to that scope, so the example gives every issue its own url.

A path the answer does not have leaves nothing, and a placeholder with no value renders empty, both silently. That is why the next step exists.

5. Preview one call

POST
/v1/agents/tool-definitions/test-openapi-call
curl -X POST https://api.speechify.ai/v1/agents/tool-definitions/test-openapi-call \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"config": {
"base_url": "https://api.vendor.example/v1",
"auth": {
"type": "oauth2_client_credentials",
"credential_id": "cred_01kc4p2q8r5s7t9v1w3x5y7z9a"
},
"operations": [
{
"id": "list_open_issues",
"method": "GET",
"path": "/issues",
"params": [
{
"name": "severity",
"in": "query",
"schema": {
"type": "string"
}
}
],
"response": {
"pick": [
"issues[].id",
"issues[].severity"
],
"max_items": 20
}
}
]
},
"operation": "list_open_issues",
"arguments": {
"severity": "critical"
}
}'

Send the whole config you are about to save, the operation’s id, and the arguments the agent would pass:

curl -X POST https://api.speechify.ai/v1/agents/tool-definitions/test-openapi-call \
-H "Authorization: Bearer $SPEECHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"config": {
"base_url": "https://api.vendor.example/v1",
"auth": { "type": "oauth2_client_credentials", "credential_id": "cred_01kc4p2q8r5s7t9v1w3x5y7z9a" },
"operations": [{
"id": "list_open_issues",
"method": "GET",
"path": "/issues",
"params": [{ "name": "severity", "in": "query", "schema": { "type": "string" } }],
"action_class": "read",
"response": { "pick": ["issues[].id", "issues[].severity"], "max_items": 20 }
}]
},
"operation": "list_open_issues",
"arguments": { "severity": "critical" }
}'

The call really reaches the vendor, so it counts against the vendor’s own limits, but it never saves anything and runs read operations only. It needs the content.manage permission, like creating a tool.

Read the result in this order:

  1. error is set only when no call was made: a config the create endpoint would refuse, an operation the config does not have, a write operation, or a credential that does not resolve.
  2. outcome says how the call ended. Only ok is a preview that worked.
  3. On ok, compare raw (the vendor’s answer) with mapped (the same answer after your mapping). observation is exactly what the agent reads.
outcomeWhat happenedWhat to change
okThe vendor answered with a success statusCheck that mapped kept what the agent needs
invalid_argumentsA required param or body was missing, so nothing was sentarguments, or the operation’s params
rate_limitedThe vendor answered 429Wait, then retry; set max_requests_per_minute below the vendor’s limit
upstream_errorThe vendor answered 4xx or 5xx; status has the codeA 401 or 403 is usually the credential or its scopes, a 404 the path or base_url
unreachableThe vendor could not be reachedbase_url
unsupported_responseThe vendor answered a media type the agent cannot readPick an operation that answers JSON or text

applied is also true when every pick path missed and the answer became empty, so read mapped rather than trusting applied.

6. Create the tool

POST
/v1/agents/tool-definitions
curl -X POST https://api.speechify.ai/v1/agents/tool-definitions \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "vendor_issues",
"description": "Read open security issues from the vendor'\''s REST API.",
"kind": "openapi",
"config": {
"auth": {
"credential_id": "cred_01kc4p2q8r5s7t9v1w3x5y7z9a",
"type": "oauth2_client_credentials"
},
"base_url": "https://api.vendor.example/v1",
"max_requests_per_minute": 15,
"operations": [
{
"action_class": "read",
"id": "list_open_issues",
"method": "GET",
"params": [
{
"in": "query",
"name": "severity",
"schema": {
"enum": [
"low",
"medium",
"high",
"critical"
],
"type": "string"
}
}
],
"path": "/issues",
"response": {
"derive": {
"issues[].url": "https://app.vendor.example/issues/{{id}}"
},
"max_items": 50,
"pick": [
"issues[].id",
"issues[].title",
"issues[].severity"
]
},
"summary": "List open issues"
},
{
"id": "get_issue",
"method": "GET",
"params": [
{
"in": "path",
"name": "issue_id",
"required": true,
"schema": {
"type": "integer"
}
}
],
"path": "/issues/{issue_id}",
"summary": "Get one issue"
}
],
"spec_url": "https://api.vendor.example/v1/openapi.json"
}
}'
  • base_url is https only and wins over the document’s servers.
  • Keep spec_url so a later import can refresh the operations.
  • max_requests_per_minute is one budget shared by every agent, run and hosted API route that calls this tool. Set it below the vendor’s own limit, so an agent never spends the allowance your team’s other integrations need.
  • timeout_ms bounds one call, 30000 by default.

To change the tool later, send the full config to PATCH /v1/agents/tool-definitions/{tool_definition_id}: it replaces the stored config whole.

7. Give it to an agent

Attach the tool to an agent:

curl -X PUT https://api.speechify.ai/v1/agents/$AGENT_ID/tools/$TOOL_ID \
-H "Authorization: Bearer $SPEECHIFY_API_KEY"

Or name it in a skill’s tool_ids with POST /v1/skills, and every agent that attaches the skill gets the tool’s operations.

The tool works on every surface an agent runs on: a voice call, a text conversation and a durable run. Its calls are made by Speechify’s servers on all of them, so the vendor credential never leaves the vault for a call.

Refresh when the vendor changes

The tool stores the operations you chose, never the document, so a vendor renaming or reshaping its API cannot change an agent’s functions on its own. To pick up the vendor’s changes, import the same spec_url again, compare the answer with the tool’s operations, and send the updated config.

Limits

LimitValue
Operations per tool40; import the same document into a second tool when an agent needs more
Base URLs per tool1; a vendor with regional hosts needs the tool pointed at your region
Credentials per tool1, of kind bearer or oauth2_client_credentials
Document size on import2 MiB
pick paths / derive fields per operation50 / 20

Errors

WhereAnswerWhat to do
Create or update400 validation_failedRead the message: it names the rule broken, and usually the operations[i] it applies to
Create or update400 validation_failed on auth.credential_idThe credential does not exist in this workspace, is a different kind from auth.type, or is scoped to another project
Create409 tool_name_takenerror.details.held_by names what holds the name; attach it, free it, or rename
Import, preview, create403 forbiddenThe caller needs the content.manage permission
Import200 with errorThe reason is in error; nothing compiled
Preview200 with errorNo call was made; the reason is in error

Next