Build with the QikTools API
Generate on-brand images and product video ads from your own code, Zapier or Make — with signed webhooks for long renders and the same flat credit prices as the app.
Included with the Studio plan · billed in credits · no extra fees.
curl -X POST https://www.qiktools.com/api/v1/generate \
-H "Authorization: Bearer $QIKTOOLS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "capability": "text-to-image",
"prompt": "a green glass bottle on wet stone",
"engine": "flash", "resolution": "2K" }'
{ "success": true,
"imageUrl": "https://…/result.png",
"creditsCharged": 6 }One endpoint, plain HTTPS. Works from any language, Zapier, Make or n8n.
Long renders run in the background and report back, HMAC-SHA256 signed.
Pay only for results: a failed generation's credits come straight back (the one exception is a provider-charged safety refusal).
Hashed keys, public-https webhooks only, per-account pacing and job caps.
What is the QikTools API?
The QikTools API lets your own code, or an automation tool such as Zapier, Make or n8n, create on-brand AI images and product video ads. You send one HTTPS request with an API key; your Brand Vault is applied unless you switch it off for that request.
Each request is paid in credits at the same prices as the app, and the response says how many were charged. Long video renders run in the background and report back to your webhook. A failed generation’s credits come straight back, except when an AI provider’s own safety system refused the request and charged for it.
Key facts
- Endpoint
- POST /api/v1/generate
- What it makes
- Images and product video ads
- Who can use it
- Studio plan accounts
- Billing
- Credits, at the app's prices
- Background jobs
- Up to 5 at a time per account
- Webhook deliveries
- Signed, up to 3 attempts
Quick start
https://www.qiktools.com, requests and responses are JSON, and every request needs your key.- Create a key under Settings → Developer (Studio plan). Copy it straight away — it's shown once. Keep it in an environment variable or secret manager, never in browser code or a public repository.
- Make a request. Images come back in the response; videos take minutes, so give them a
webhook_url(or poll the job). - Use the result. The response carries a hosted file URL — download what you need and store it yourself.
Generate an image
curl -X POST https://www.qiktools.com/api/v1/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"capability": "text-to-image",
"prompt": "a serene mountain lake at sunrise",
"engine": "flash",
"resolution": "2K",
"aspectRatio": "16:9"
}'Response — 200 OK
{
"success": true,
"imageUrl": "https://…/image.png",
"creditsCharged": 6,
"settings": { "engine": "flash", "resolution": "2K", "aspectRatio": "16:9" }
}Request builder
Charged when the request is accepted and refunded automatically if generation fails, except a provider-charged safety refusal (provider_safety_charged).
Your request
curl -X POST https://www.qiktools.com/api/v1/generate \
-H "Authorization: Bearer $QIKTOOLS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"capability": "text-to-image",
"prompt": "a serene mountain lake at sunrise, editorial photo",
"engine": "flash",
"resolution": "2K",
"aspectRatio": "16:9"
}'Authentication
Send your secret key as a bearer token on every request:
Authorization: Bearer qt_live_…- Only a fingerprint of your key is stored, so it can't be shown again — if you lose it, revoke it and create a new one.
- Revoke a key in Settings → Developer at any time; it stops working immediately.
- Keys belong to a Studio account. If the account leaves the Studio plan, its keys stop working (
403 plan_required). - Usage is charged to the workspace your account is working in — your personal credits, or your team's if you're on a team and have switched to its workspace (the workspace menu at the top of the site). There is never an automatic fallback to the other one;
GET /api/v1/accounttells you which one is in use.
Endpoints
| Method & path | What it does |
|---|---|
POST /api/v1/generate | Create a generation: an image, or a product video. |
GET /api/v1/jobs/{job_id} | Status and result of an asynchronous job. |
GET /api/v1/account | Your plan and the credits a request would draw on — handy to test a key and check the balance. |
Check your key and balance
curl https://www.qiktools.com/api/v1/account \
-H "Authorization: Bearer YOUR_API_KEY"Response — 200 OK
{
"plan": "studio",
"credits": 1840, // the wallet requests are charged to
"credits_source": "personal", // "team" when that wallet belongs to your team
"on_team": false
}Models
engine value picks one AI model. Model names are written the way their makers write them.| engine | Model | Capability | Good for |
|---|---|---|---|
"standard" | FLUX.1 [schnell]by Black Forest Labs | text-to-image | Fast drafts at one fixed size |
"flash" | Nano Banana 2.1by Google | text-to-image | Sharp everyday images up to 4K |
"pro" | Nano Banana Proby Google | text-to-image | Detailed scenes and clear text up to 4K |
"kling" | Kling O1by Kuaishou | product-commercial | 5 or 10 second product clips, with an optional voiceover |
"seedance" | Seedance 2.5by ByteDance | product-commercial | Longer clips with native sound and spoken lines |
Seedance 2.5 is reached through Runway's developer API. Model and company names are trademarks of their respective owners; QikTools is an independent service and isn't affiliated with, sponsored or endorsed by them.
Powered by RunwayText-to-image
POST /api/v1/generate with "capability": "text-to-image". Finished in seconds, so the response is the image.| Parameter | Type | Description |
|---|---|---|
capabilityrequired | string | Must be "text-to-image". |
promptrequired | string | What to draw. Up to 4,000 characters. |
engineoptional | string default "standard" | "standard" (FLUX.1 [schnell] — fast, fixed size), "flash" (Nano Banana 2.1) or "pro" (Nano Banana Pro) — higher detail and control. standard ignores resolution and aspectRatio. See Models. |
resolutionoptional | string default "1K" | "1K", "2K" or "4K" — flash and pro only. |
aspectRatiooptional | string default "1:1" | 16:9 1:1 9:16 21:9 4:3 3:2 5:4 4:5 3:4 2:3 1:4. Not every shape exists at every resolution (3:4 and 2:3 stop at 2K; 1:4 is 1K on flash only) — an unsupported combination is a 400 before anything is charged. |
apply_brandoptional | boolean default true | Apply your Brand Vault (voice, colors, exclusions). Send false to skip it for this request. |
webhook_urloptional | string | Optional. Makes the call asynchronous — see Async jobs. |
webhook_secretoptional | string | Optional, 8+ characters. Signs every webhook delivery. |
Product commercial
POST /api/v1/generate with "capability": "product-commercial". Turns a product photo and a direction into a short video ad, optionally with a spoken voiceover. Renders take minutes.| Parameter | Type | Description |
|---|---|---|
capabilityrequired | string | Must be "product-commercial". |
productImageUrlrequired | string (https URL) | A publicly reachable photo of the product. Up to 2,000 characters. |
visualPromptrequired | string | How the ad should look and move. Up to 4,000 characters. |
voiceoverScriptoptional | string | What the voice says. Up to 700 characters. On Kling O1 it's recorded and laid over the clip at no extra charge — keep it to about 2 words per second of video (a slightly long line is sped up a little to fit; a much longer one is cut at the end of the clip). Seedance 2.5 speaks it natively. |
engineoptional | string default "kling" | "kling" (Kling O1 — 5 or 10 seconds) or "seedance" (Seedance 2.5 — 4 to 30 seconds, choose the resolution). See Models. |
durationSecondsoptional | number default 5 | Kling O1 snaps to 5 or 10. Seedance 2.5 is clamped to 4–30. The response's settings shows what was used. |
resolutionoptional | string default "720p" | Seedance 2.5 only: "480p", "720p" or "1080p". Ignored for Kling O1. |
aspectRatiooptional | string default "9:16" | 9:16 16:9 1:1 4:3 3:4 21:9 on Seedance 2.5. Kling O1 makes 9:16, 16:9 and 1:1 only — any other shape is refused with invalid_request (nothing is charged), never swapped. |
apply_brandoptional | boolean default true | Apply your Brand Vault. Send false to skip it for this request. A Seedance clip makes its own sound, so its prompt is sent exactly as you wrote it: a model that speaks could say the vault's words out loud. |
webhook_urloptional | string | Strongly recommended for video — see Async jobs. |
webhook_secretoptional | string | Optional, 8+ characters. Signs every webhook delivery. |
webhook_url the request stays open until the render finishes — minutes, which most HTTP clients and gateways will time out on. If your client gives up, the render may still complete and be charged; with a webhook you get an immediate 202 and can't lose track of it.Async jobs & webhooks
Start a video render
curl -X POST https://www.qiktools.com/api/v1/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"capability": "product-commercial",
"productImageUrl": "https://example.com/product.png",
"visualPrompt": "The bottle turns slowly on a marble counter in soft morning light",
"voiceoverScript": "Meet the bottle that keeps your water cold all day.",
"engine": "seedance",
"durationSeconds": 8,
"resolution": "720p",
"aspectRatio": "9:16",
"webhook_url": "https://your-app.example.com/qiktools-webhook",
"webhook_secret": "a-long-random-string-you-choose"
}'Response — 202 Accepted
{
"job_id": "5b6f0d9c-…",
"status": "processing",
"capability": "product-commercial",
"credits_charged": 207,
"settings": { "engine": "seedance", "durationSeconds": 8, "resolution": "720p", "aspectRatio": "9:16" },
"poll_url": "/api/v1/jobs/5b6f0d9c-…"
}Prefer to poll?
Check on a job
curl https://www.qiktools.com/api/v1/jobs/JOB_ID \
-H "Authorization: Bearer YOUR_API_KEY"Job — 200 OK
{
"job_id": "5b6f0d9c-…",
"status": "succeeded", // "processing" | "succeeded" | "failed"
"capability": "product-commercial",
"result": { "video_url": "https://…/video.mp4", "has_voice": true },
"error": null, // a message when status is "failed"
"credits_charged": 207, // 0 for a refunded failure; the kept credits for a provider_safety_charged refusal
"created_at": "2026-09-25T12:00:00Z",
"completed_at": "2026-09-25T12:03:41Z",
"webhook": { "state": "delivered", "attempts": 1 } // null if you gave no webhook_url
}What your webhook receives
A POST with a JSON body, once the job finishes or fails:
Body
{
"id": "delivery id",
"event": "job.succeeded", // or "job.failed"
"created": 1767225600,
"data": {
"job_id": "5b6f0d9c-…",
"status": "succeeded", // or "failed" (with an "error" message)
"capability": "product-commercial",
"credits_charged": 207,
"result": { "video_url": "https://…/video.mp4", "has_voice": true }
}
}| Header | Value |
|---|---|
X-Qiktools-Event | The event name, e.g. job.succeeded. |
X-Qiktools-Delivery | A unique id for this delivery (the same value as the body's id). |
X-Qiktools-Signature | Only if you sent a webhook_secret: t=<unix seconds>,v1=<hex>, an HMAC-SHA256 of "t.body" with your secret. |
Verify the signature
import crypto from "node:crypto";
// Verify against the RAW request body, exactly as received (before any JSON parsing).
export function verifyQiktoolsWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(signatureHeader.split(",").map((part) => part.split("=")));
if (!parts.t || !parts.v1) return false;
// Reject old deliveries so a captured request can't be replayed later.
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;
const expected = crypto.createHmac("sha256", secret).update(parts.t + "." + rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express example — use the raw body for this route:
// app.post("/qiktools-webhook", express.raw({ type: "application/json" }), (req, res) => {
// const ok = verifyQiktoolsWebhook(req.body.toString("utf8"), req.get("X-Qiktools-Signature") ?? "", process.env.QIKTOOLS_WEBHOOK_SECRET);
// if (!ok) return res.sendStatus(400);
// const event = JSON.parse(req.body.toString("utf8"));
// // ...handle event.event ("job.succeeded" / "job.failed"), then:
// res.sendStatus(200);
// });https:// and reachable on the public internet (private, internal and localhost addresses are refused, and redirects aren't followed). Respond with any 2xx within 10 seconds. Timeouts, network errors and 5xx/408/429 responses are retried up to 3 attempts in total; any other 4xx is treated as a refusal and not retried. Whatever happens, the result stays available from GET /api/v1/jobs/{job_id}, which also reports the delivery state, for 30 days after the job finishes (then the job is deleted and that address answers 404).Safe retries (Idempotency-Key)
Idempotency-Key header with POST /api/v1/generate and a retry can never charge you twice — after a timeout, a dropped connection or a crash on your side.Generate with a key
# One key per generation (here a UUID). Retrying with the SAME key and body is always safe.
curl -X POST https://www.qiktools.com/api/v1/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0f8c7e52-4c1b-4c8e-9a51-6d2f0b7a91c3" \
-d '{ "capability": "text-to-image", "prompt": "a serene mountain lake at sunrise", "engine": "flash" }'- Make up one key per generation — a UUID is ideal (1 to 255 visible characters, no spaces) — and send the same key with every retry of that generation.
- Same key, same body: you get the first request's answer again — the same image or video link, or the same
job_idfor an async call — with the headerIdempotent-Replayed: true. Nothing is generated or charged again. The body is compared as JSON, so key order and spacing don't matter. - Same key, different body: refused with
422 idempotency_key_mismatch. Use a new key for a new request. - The first request is still running:
409 idempotency_key_in_usewith aRetry-Afterheader — ask again after that many seconds. - Keys belong to the API key that sent them (another key of yours can use the same value) and are kept for 24 hours; after that a key can be used again.
- A request refused before anything was charged (a field error, too few credits, our safety checks refusing it) doesn't use up its key: fix the problem and retry with the same key. Once a request was charged, its answer is the key's answer — including a failure that was refunded — so retry a failed generation with a new key.
- We keep a fingerprint of the body (never the body itself) and the answer we sent, for those 24 hours.
Pricing
Text-to-image (per image)
| Engine | 1K | 2K | 4K |
|---|---|---|---|
| standard · FLUX.1 [schnell] (fixed size) | 1 | — | — |
| flash · Nano Banana 2.1 | 4 | 6 | 12 |
| pro · Nano Banana Pro | 13 | 13 | 26 |
Product commercial — Kling O1
| Length | Credits |
|---|---|
| 5 seconds | 49 |
| 10 seconds | 97 |
Product commercial — Seedance 2.5 (priced by length)
| Resolution | ≈ per second | 4s | 5s | 8s | 10s | 15s |
|---|---|---|---|---|---|---|
| 480p | 17.3 | 69 | 87 | 138 | 173 | 259 |
| 720p | 25.9 | 104 | 130 | 207 | 259 | 388 |
| 1080p | 58.6 | 235 | 293 | 469 | 586 | 879 |
Credits cost from $0.0185 (Teams and Studio annual) to $0.025 each. Any other length between 4 and 30 seconds is priced the same way: the whole clip, rounded up to a whole credit (about the per-second figure times the length).
Security best practices
Store keys in environment variables or a secret manager. Never put one in browser code, a mobile app or a public repository — anyone who can read it can spend your credits.
Create a separate key for each app, Zap or script (up to 10). If one leaks, revoke just that one in Settings → Developer; the others keep working.
Settings → Developer shows when each key was last used. A key you don't recognise being used is a key to revoke straight away.
Set a webhook_secret and check the X-Qiktools-Signature header with the function above, rejecting deliveries older than 5 minutes. Webhook URLs must be public https addresses.
We keep only a SHA-256 hash of each key, so we can't show it again — copy it when it's created. Webhook URLs are never stored either, only their host.
A key only works while the account is on the Studio plan, can never spend more than the wallet holds, runs at most 5 background jobs at once and is paced at 120 requests a minute.
Errors & limits
Every error is JSON: { "error": "message for a person", "code": "stable_identifier" }. Branch on code, show error.
| Status | code | What it means |
|---|---|---|
| 400 | invalid_request | Something in the request is wrong; the message says what. Nothing was charged. |
| 401 | missing_api_key | No Authorization: Bearer header. |
| 401 | invalid_api_key | The key is wrong or has been revoked. |
| 402 | insufficient_credits | Not enough credits in the wallet this account is spending from (see GET /api/v1/account). Nothing was charged, and the other wallet is never touched. |
| 402 | team_limit_reached | This account spends its team's credits and has used the monthly allowance its team owner set. Nothing was charged; switch the account to Personal credits or ask the owner to raise the allowance. |
| 403 | plan_required | The key's account is no longer on the Studio plan. |
| 400 | content_blocked | Our safety checks refused the request (see the AI & Acceptable Use Policy). Nothing was charged. Repeated refusals can restrict the account. |
| 403 | media_blocked | A file in the request was removed after a report and can't be used. Nothing was charged. |
| 404 | not_found | No such job on your account. |
| 409 | idempotency_key_in_use | A request with this Idempotency-Key is still running. Nothing more was charged; retry after the Retry-After seconds to get its answer. |
| 409 | idempotency_key_unresolved | The first request with this key was charged and finished, but its answer couldn't be kept. Nothing more was charged; check GET /api/v1/account and use a new key to run it again. |
| 422 | idempotency_key_mismatch | This Idempotency-Key was already used with a different body. Nothing was charged; use a new key for a new request. |
| 422 | provider_safety_charged | Our checks passed, but the model's provider refused the request under its own safety rules and charged for the attempt, so the credits are kept (charged: true) — see Terms → provider safety refusals. Change the request before retrying. |
| 451 | region_blocked | Paid AI isn't available from the country the request came from. Nothing was charged. |
| 429 | too_many_active_jobs | You have 5 async jobs running. Retry after the Retry-After seconds. |
| 413 | payload_too_large | The request body is over 64 KB. |
| 429 | rate_limited | More than 120 requests a minute from one account, or several recent generations that didn't complete. Wait for the Retry-After seconds (also in retry_after_seconds). |
| 500 | generation_failed | The generation didn't complete. Your credits were refunded automatically. |
| 503 | screening_unavailable | We couldn't run our safety checks just now. Nothing was charged; retry shortly. |
| 503 | idempotency_unavailable | We couldn't check your Idempotency-Key just now, so nothing was run or charged. Retry with the same key shortly. |
| 503 | paused | QikTools has paused all new paid work for a short while. Nothing was charged; retry in a few minutes. |
| 503 | tool_paused | The tool behind this capability is paused for now — error says why and until when. Nothing was charged and the same Idempotency-Key can be used again; retry after the Retry-After seconds. Jobs that already started finish normally. |
| 503 | label_failed | The video couldn't be given its visible AI-generated label in time, so it wasn't delivered. Your credits were refunded automatically; try again (an async job tries the label twice before it fails). |
| 504 | time_budget_exceeded | A synchronous request ran past the time a single request is allowed. It was stopped and your credits were refunded. For long jobs, pass a webhook_url so the job runs asynchronously. |
| 500 | internal_error | Something went wrong on our side. Safe to retry a request that hadn't started a job. |
- Up to 5 async jobs can run at once per account.
- A job still running after 20 minutes is failed and refunded automatically — you are never charged for output that doesn't arrive, except when the model's provider refused it under its own safety rules and charged for the attempt (
provider_safety_charged; an async job then endsfailedwith that code and the credits kept). - Retrying after a timeout? Send the same
Idempotency-Keyas the first try and you'll never pay twice — see Safe retries. - Need higher limits or something we don't offer yet? Email hello@qiktools.com.
Zapier, Make & n8n
- Add a Webhooks by Zapier → Custom Request step (Make: HTTP → Make a request; n8n: HTTP Request).
- Method
POST, URLhttps://www.qiktools.com/api/v1/generate, body type JSON, and the body from the examples above. - Headers:
Authorization: Bearer YOUR_API_KEYandContent-Type: application/json. - For video, set
webhook_urlto a Catch Hook trigger URL (Zapier: Webhooks by Zapier → Catch Hook; Make: Custom webhook) in a second scenario — it receives the finished video URL. Those triggers can't verify HMAC signatures, so leavewebhook_secretout there and treat the URL itself as the secret.
Versioning
- Every endpoint lives under
/api/v1. Within v1 we only add things — new optional fields, new values and new response fields — so write integrations that ignore fields they don't recognise. - Anything that could break an integration ships as a new version, announced to key holders by email in advance.
- Questions or a feature you need? Email hello@qiktools.com.
API questions
- What can the QikTools API make?
Two things: AI images from a text prompt (
text-to-image) and product video ads from a product photo (product-commercial), with an optional voiceover. Your Brand Vault is applied unless you sendapply_brand: false.- How is the API billed?
In credits from your account, at the same prices as the app. Every response reports the credits charged, and a failed generation's credits are refunded automatically (the one exception is a provider-charged safety refusal).
- Which plan includes API access?
The Studio plan. Create keys under Settings → Developer. If the account leaves the Studio plan, its keys stop working.
- How do long video renders work?
Add a
webhook_urland the request returns straight away with a job ID. The result is posted to your webhook (signed with HMAC-SHA256 when you set awebhook_secret), with up to 3 delivery attempts, or you can pollGET /api/v1/jobs/{id}. Each account can run up to 5 jobs at once; a job still running after 20 minutes is failed and refunded.- Can I retry a request without paying twice?
Yes. Send an
Idempotency-Keyheader (a UUID per generation) and reuse it for that generation's retries: the same key with the same body returns the first answer, or the same job, and is never charged again. The same key with a different body is refused, and keys are kept for 24 hours per API key.- Can I call it from Zapier, Make or n8n?
Yes. It's plain HTTPS and JSON, so any tool that can send an HTTP request with an Authorization header can call it.