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.
- Find
Search for a model, route or graph and read its inputs.
- Quote
Price it with quote_only; nothing is held.
- Submit
Send it once with a request_id and an optional ceiling.
- Follow
Read the job until it succeeds, fails or is cancelled.
- Collect
Read each output artifact and download its bytes.
Which tool does the work
| Work | Tool and arguments |
|---|---|
| A provider route, a model profile or a Comfy graph | generate 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 logo | edit 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 wrote | compose with timeline or html |
| Python with the SDK, Pillow and fonts in a sandbox | run with code |
| Measure or critique media | watch, 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
countabove 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_microcreditsis a ceiling for the whole call, split evenly across takes. A ceiling below a take's hold is refused withcap_exceededbefore anything starts.- A balance below the hold is refused with
insufficient_credits(402), with the amounts needed and available indetails. - 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_creditsanddetails.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
| State | Meaning | Money |
|---|---|---|
queued | Admitted and waiting for a worker or provider. | Its hold is reserved. |
running | Working. | Still held. |
succeeded | Done. Outputs are under result.artifacts[]. | Settled to measured usage; the rest of the hold is released. |
failed | Ended with error (code, message, retryable). | Released if it never started; otherwise charged only for work measured up to the failure, never above the hold. |
cancelled | Stopped 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).
- Price both tiers with
quote_only. Each answers{quote: {quote_id, p50_microcredits, hold_microcredits, expires_at, eta_s}, eta_s}and holds nothing. - Render the draft with its own
request_id.wait_swaits up to 40 seconds, then returns the job. - Check it with
watch,listenorreview, and edit the timeline as often as needed. - Render the final with a new
request_id. It is its own render with its own hold; nothing promotes a draft. deliverthe final.deliverrefuses a draft withnot_deliverableanddetails.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
| Limit | Value | Refusal |
|---|---|---|
Takes per generate | 8 (count) | invalid_request |
read wait | 85 seconds | invalid_request above it |
compose wait | 40 seconds, then the job | – |
run time | timeout_s 1–3,600 (default 300) | invalid_request |
run code | 200,000 characters, 50 files | invalid_request |
| JSON body | 2 MB | too_large (413); upload media instead |
| Request rate | per credential | rate_limited (429) with retry_after_s |
Tool calls in one agent run or run sandbox | 10 per second of requested output media, at least 300 | rate_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 happened | Safe next step |
|---|---|
| Lost reply to a submit | Resend the identical call with the same request_id. You get the original jobs back. |
| Lost job id | Resend with the same request_id, which returns the same jobs. |
| HTTP 401 while watching | Get a current credential and keep reading the same job. A 401 is not a generation failure. |
| Timeout or HTTP 5xx on a submit | The call may have been admitted. Resend with the same request_id, never a new one. |
busy or rate_limited | Wait retry_after_s, then retry with the same request_id. |
insufficient_credits or cap_exceeded | Nothing started. Top up or raise the cap, then submit. |
Job failed | Read 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.