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

HeaderRequiredValue
AuthorizationyesBearer <key>
Content-Typeyesapplication/json
Idempotency-Keyrecommended on paid callssee Idempotency
{
  "input": { },
  "webhook_id": "01929a10-0c3d-7f44-8a21-0b1c2d3e4f50",
  "metadata": { "order_id": "4411", "customer": "acme" }
}
FieldTypeRequiredMeaning
inputobjectyesThe 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_idstring or nullnoA 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.
metadataobject or nullnoUp 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"]
  }
}
StatusWhen
202 Acceptedthe tool started one or more generations (returns_generation: true); follow them by polling or webhook
200 OKthe 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 callbilling.credits_chargedcredits_are_final
free calls: lists, catalog, quotes, status reads0true
a call that starts generations (202)the sum charged at start for every generation startedfalse
a paid call that finishes in the request (analyze_video, analyze_audio, filmlab_edit)what it chargedtrue
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, and result.partial_failure says what happened.
  • result.generation_ids lists only what started, and billing counts 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 200 with the original response body, including the original generation_ids and billing, and charges nothing more. Its Morphic-Credits-Charged header is 0.
  • 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 answers 409 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.

  1. Check what the call did with list_generations, or with GET /v1/direct/usage filtered by time.
  2. Send a new Idempotency-Key only 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.

On this page