Calling a tool
Call any Direct API tool with one request shape, read its result and billing, and retry paid calls safely with an Idempotency-Key.
Every capability is one tool, called the same way: POST /v1/direct/tools/{tool_name}. The tool catalog lists them all.
Request
| Header | Required | Value |
|---|---|---|
Authorization | yes | Bearer <key> |
Content-Type | yes | application/json |
Idempotency-Key | recommended on paid calls | see Idempotency |
{
"input": { },
"webhook_id": "01929a10-0c3d-7f44-8a21-0b1c2d3e4f50",
"metadata": { "order_id": "4411", "customer": "acme" }
}| Field | Type | Required | Meaning |
|---|---|---|---|
input | object | yes | The tool's arguments, validated against its input_schema. Send {} for a tool that takes none. Every tool that creates something (a generation, an import, an upload, a timeline) requires project_id, from list_projects. |
webhook_id | string or null | no | A registered webhook endpoint to notify when each generation this call starts reaches a final status. Only meaningful on tools with returns_generation: true; refused elsewhere. |
metadata | object or null | no | Up to 20 string keys (40 characters or fewer) with string values (500 characters or fewer). Stored on every generation the call starts and echoed back in every read and webhook. Morphic never interprets it. |
A full call, with an idempotency key:
curl -s https://mcp.morphic.com/v1/direct/tools/generate_image \
-H "Authorization: Bearer $MORPHIC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4411-hero-image" \
-d '{"input": {"prompt": "A red vintage bicycle against a pastel wall", "project_id": "01928f4e-5b7c-7d2a-9e11-3c4b5a6d7e8f"}, "metadata": {"order_id": "4411"}}'The tools that render video on Morphic's servers (video_compose, the video_* transforms and burn_subtitles) do not accept webhook_id or metadata yet. They answer 422 tool_refused when either is set; poll their generations instead.
Response
Every success has the same envelope. The tool's own output sits in result, as its output_schema describes.
{
"request_id": "0192a7b3-91d0-7c55-b1aa-6e5f4c3b2a10",
"tool": "generate_image",
"result": {
"generation_id": "0192a7b3-a1c4-7e02-8f9d-1a2b3c4d5e6f",
"generation_ids": ["0192a7b3-a1c4-7e02-8f9d-1a2b3c4d5e6f"],
"status": "queued",
"project_id": "01928f4e-5b7c-7d2a-9e11-3c4b5a6d7e8f",
"project_name": "Spring campaign",
"file_id": "01928f4e-6c8d-7a10-b2c3-d4e5f6a7b8c9",
"model_id": "mdl_3hv8tq1nzc6e",
"estimated_credits": 8,
"credits_settle_on_output_duration": false
},
"billing": {
"credits_charged": 8,
"credits_are_final": false,
"credits_settle_on_output_duration": false,
"generation_ids": ["0192a7b3-a1c4-7e02-8f9d-1a2b3c4d5e6f"]
}
}| Status | When |
|---|---|
202 Accepted | the tool started one or more generations (returns_generation: true); follow them by polling or webhook |
200 OK | the tool finished in the call (lists, catalog, quotes, analysis), or an idempotent replay |
Billing block
billing is present on every successful response, free calls included.
| Kind of call | billing.credits_charged | credits_are_final |
|---|---|---|
| free calls: lists, catalog, quotes, status reads | 0 | true |
a call that starts generations (202) | the sum charged at start for every generation started | false |
a paid call that finishes in the request (analyze_video, analyze_audio, filmlab_edit) | what it charged | true |
a refused call (4xx) | no billing block; nothing charged |
The block reports what the call charged when it returned. Refunds and settlement happen later and appear on the generation, never on the original response. Responses also carry Morphic-Credits-Charged: <integer> for logging at your gateway. Credits and usage explains how a generation is charged, settled and refunded.
Tool refusals
When the input is well formed but cannot run (a model your plan lacks, an aspect ratio the model does not offer, an asset of the wrong type), the call answers 422 tool_refused. The message names the field, the value and the values that would have worked. Nothing was started and nothing was charged.
{
"description": "aspect_ratio: \"21:9\" is not offered by model mdl_3hv8tq1nzc6e. Offered: 1:1, 4:3, 3:4, 16:9, 9:16. Models offering 21:9 for text_to_image: mdl_9wd4mf2kqs7b.",
"code": "tool_refused",
"data": { "request_id": "0192a7b3-…", "tool": "generate_image" },
"error": { "error_code": "TOOL_REFUSED", "message": "aspect_ratio: \"21:9\" is not offered by model mdl_3hv8tq1nzc6e. …" }
}Partial starts
A call with variations above 1 starts one generation per variation. If a later variation is refused after earlier ones started:
- The response is still
202, andresult.partial_failuresays what happened. result.generation_idslists only what started, andbillingcounts only what started.- Retry only the missing variations.
Idempotency
A network failure can land after a request was sent and before its answer arrived. To retry a paid call safely, send an Idempotency-Key header. Use your own job or order id.
Idempotency-Key: order-4411-hero-image- The first request with a key runs normally.
- A retry with the same key, tool and body within 24 hours does not run again. It answers
200with the original response body, including the originalgeneration_idsandbilling, and charges nothing more. ItsMorphic-Credits-Chargedheader is0. - The same key with a different tool or body answers
409 idempotency_conflict. - A retry while the original is still running answers
409 idempotency_in_progress. - A retry of a paid call whose original failed (
5xx) or never answered after it may have run answers409 idempotency_outcome_unknown. It is never run again under the same key: check its result, then use a new key. - Keys are scoped to the API key that sent them, up to 200 characters.
Free calls accept the header too but rarely need it.
Retrying a paid call after a 5xx
A paid call that answers 500, 503 or 504 may have started work and charged for it before it failed.
- Check what the call did with
list_generations, or withGET /v1/direct/usagefiltered by time. - Send a new
Idempotency-Keyonly for work that did not start.
When no answer arrived at all (a dropped connection), retry with the same key: you get the stored answer, or 409 idempotency_in_progress while it still runs.
If you have any trouble or have any questions, feel free to reach out. We're happy to help. Write to us at support@morphic.com.