Quote and run work

Price a tool call, submit it once with a request_id, follow the job it returns, and cancel or recover safely.

Updated

Use the tools directly when your own code or agent decides how the work is made: one still from a chosen model, a graph you wrote, an edit, a render of your own timeline, or a Python script. Every call that can spend follows the same pattern: price it, submit it once with a request_id, then follow the job it returns. To hand a whole ad to Wavemaker's agent instead, use the session API: Make an ad.

One paid call, start to finish
  1. Find

    Search for a model, route or graph and read its inputs.

  2. Quote

    Price it with quote_only; nothing is held.

  3. Submit

    Send it once with a request_id and an optional ceiling.

  4. Follow

    Read the job until it succeeds, fails or is cancelled.

  5. Collect

    Read each output artifact and download its bytes.

Which tool does the work

WorkTool and arguments
A provider route, a model profile or a Comfy graphgenerate with exactly one of route, model or graph; up to 8 takes with count
Trim, crop, resize, reframe, fit an exact ad size, cut a flat view from a 360 image, upscale, retime, grade, mix, extract audio, prepare a logoedit with source and op (trim, crop, resize, reframe, ads_format, view, upscale, retime, grade, mix, extract_audio, logo_prepare)
Render a timeline.json, or package an HTML5 banner you wrotecompose with timeline or html
Python with the SDK, Pillow and fonts in a sandboxrun with code
Measure or critique mediawatch, listen, compare, review

search, read and watch never spend. generate, edit, compose, listen, compare, review and run can. edit with op: "logo_prepare" measures and answers without a charge. The older compute.*, jobs.* and media.quote_composition operations are not served; they answer HTTP 501 operation_not_ported.

1. Find a model, route or graph

search with kinds: ["model"], ["route"] or ["graph"], then read the result's wm://model/{id}, wm://route/{id} or wm://graph/{id} for what it needs. A route's read returns its request schema and whether it is available. Neither search nor read spends.

To check arguments without pricing them, add validate_only: true. generate answers {valid, problems, available} and nothing is quoted or held. A route's problems each have a path and a message; a graph's also carry a code. For a graph, see Graph portability.

2. Quote

Add quote_only: true to any spending call. The reply's data.quote is:

{quote_id, p50_microcredits, hold_microcredits, expires_at, eta_s?}
  • p50_microcredits: the typical charge.
  • hold_microcredits: what a submit reserves from the balance. Compare it with your budget.
  • expires_at: Unix seconds. Tool quotes last 30 minutes.
  • eta_s: present only where the run time has been measured.
  • With count above 1, p50 and hold cover every take.

A quote places no hold and starts nothing. Amounts are microcredits: 1 credit is 1,000,000 microcredits. generate, edit and run quotes also carry id, equal to quote_id, for older clients.

To hold exactly what you were quoted with generate, edit or run, submit with quote_id (and count: 1 for generate). A quote pins one take; quote_id with a larger count is refused with invalid_request. compose takes no quote_id. An expired quote, or one bound to other work, is refused with quote_expired.

3. Submit once

Send the real call with a request_id and, if you want a ceiling, max_microcredits. Save the request_id before you send.

curl --fail-with-body \
  --header "Authorization: Bearer $WAVEMAKER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"request_id": "campaign-123:hero-still-v1",
           "args": {"route": "gemini.image", "max_microcredits": 8000000,
                    "inputs": {"prompt": "A cold brew can on a sunlit commuter train seat"},
                    "params": {"model": "gemini-3-pro-image", "aspect_ratio": "1:1", "image_size": "1K"}}}' \
  https://api.wavemaker.io/v1/tools/generate
# → 200 {"ok": true, "data": {"jobs": [{"id": "3f2c...", "uri": "wm://job/3f2c...", "state": "queued"}]},
#        "job": {"id": "3f2c...", "uri": "wm://job/3f2c...", "state": "queued", ...},
#        "cost": {"held_microcredits": ..., ...}}

The reply comes at once. data.jobs[] lists one job per take, and the envelope's job is the first. The ceiling in this example is 8 credits; set yours from the hold_microcredits your own quote returned, since a ceiling below the hold is refused.

What a submit reserves

  • Each take holds its quoted amount from the workspace balance when the job is admitted.
  • max_microcredits is a ceiling for the whole call, split evenly across takes. A ceiling below a take's hold is refused with cap_exceeded before anything starts.
  • A balance below the hold is refused with insufficient_credits (402), with the amounts needed and available in details.
  • A spending cap set for the workspace or a member can also refuse it with cap_exceeded.
  • Credits granted at sign-up can't fund calls through an API key or a connected app until the workspace has paid for credits or a plan. Those calls are refused with insufficient_credits and details.reason: "starter_credits_need_payment".
  • When the job ends, its hold settles to measured usage, and the rest is released. If measured usage comes out above the hold, the overrun is charged only as far as your ceiling and balance allow; past that, the job is charged at its hold.
  • A route whose provider has your own saved key runs on that key and holds 0 credits. See Credentials.

Replays

Sending the same request_id with the same arguments returns the original jobs and charges nothing twice. Changing only max_microcredits, quote_id, rush or wait_s still replays. Reusing a request_id for different arguments, another project or another tool never starts that work: it is refused with request_conflict (409), or conflict from compose. New work needs a new request_id.

4. Follow the original job

read of wm://job/{id} with wait_s waits for the job to finish, up to 85 seconds, and returns it:

{id, uri: "wm://job/{id}", state: "succeeded", kind, pool, receipt?: "wm://receipt/{id}",
 result?: {artifacts: [{uri: "wm://artifact/{sha256}", sha256, size, mime, role}]}, error?}

To follow several jobs, read with uris (up to 20); they share one wait. Add detail: "full" for the job's whole record, including what it ran. A job another workspace owns is not_found.

Job states

StateMeaningMoney
queuedAdmitted and waiting for a worker or provider.Its hold is reserved.
runningWorking.Still held.
succeededDone. Outputs are under result.artifacts[].Settled to measured usage; the rest of the hold is released.
failedEnded with error (code, message, retryable).Released if it never started; otherwise charged only for work measured up to the failure, never above the hold.
cancelledStopped at your request.Released if it never started; otherwise the work done so far settles.

succeeded, failed and cancelled are final. A finished job's receipt (wm://receipt/{id}) reads its settled cost. read of an output's wm://artifact/{sha256} returns its facts and a signed download link, valid for at most an hour.

A wait is not a failure

A wait that ends while the job is still queued or running means you stopped watching. It doesn't mean the job failed, and it cancels nothing. Read again; never resubmit to check progress.

5. Cancel

To stop a job, write with {"uri": "wm://job/{id}", "cancel": true}. It needs write and returns the job. A job that hadn't started has its hold released. A running graph job has its sandbox stopped, and the work done so far settles once. Cancelling a job that already ended changes nothing.

To stop a hosted agent turn instead, interrupt it. See Make an ad.

Draft, then final

compose renders a timeline at tier: "draft" (fast, about a 540 px short side, never deliverable) or tier: "final" (the default).

  1. Price both tiers with quote_only. Each answers {quote: {quote_id, p50_microcredits, hold_microcredits, expires_at, eta_s}, eta_s} and holds nothing.
  2. Render the draft with its own request_id. wait_s waits up to 40 seconds, then returns the job.
  3. Check it with watch, listen or review, and edit the timeline as often as needed.
  4. Render the final with a new request_id. It is its own render with its own hold; nothing promotes a draft.
  5. deliver the final. deliver refuses a draft with not_deliverable and details.reason: "draft_tier".

compose checks a timeline before it renders, quote_only included, and refuses a bad one with invalid_timeline and its path.

Rush

generate with a graph or model profile takes rush: true to ask to start sooner when the GPU queue has a wait. Rush applies only when the service offers it: the reply's rush object says whether it was applied, and if not, why (not_offered, no_wait or no_faster). When it is not applied, nothing extra is held and the job runs as a standard job. When it is applied, the hold adds a surcharge on the job's GPU credits, charged only if the job did start sooner. quote_only with rush: true shows the offer. A route job is never rushed. An agent rushes only within its person's daily Rush budget, which is off unless that person sets one.

Limits on calls

LimitValueRefusal
Takes per generate8 (count)invalid_request
read wait85 secondsinvalid_request above it
compose wait40 seconds, then the job–
run timetimeout_s 1–3,600 (default 300)invalid_request
run code200,000 characters, 50 filesinvalid_request
JSON body2 MBtoo_large (413); upload media instead
Request rateper credentialrate_limited (429) with retry_after_s
Tool calls in one agent run or run sandbox10 per second of requested output media, at least 300rate_limited, not retryable within that run

A workspace's model and route policy can refuse a generate or edit with workspace_policy_refused (403). The service never substitutes another route. See HTTP API.

Recovery

What happenedSafe next step
Lost reply to a submitResend the identical call with the same request_id. You get the original jobs back.
Lost job idResend with the same request_id, which returns the same jobs.
HTTP 401 while watchingGet a current credential and keep reading the same job. A 401 is not a generation failure.
Timeout or HTTP 5xx on a submitThe call may have been admitted. Resend with the same request_id, never a new one.
busy or rate_limitedWait retry_after_s, then retry with the same request_id.
insufficient_credits or cap_exceededNothing started. Top up or raise the cap, then submit.
Job failedRead error.code and fix the cause. To try again, submit with a new request_id; the old id only returns the failed job.

See Errors, limits and safe retries for every code.

Related guidance