Skip to main content

Text-to-image on Black Forest Labs' FLUX.1 [dev] guided by Shakker Labs' ControlNet Union Pro 2.0, with up to three of your own FLUX.1 dev LoRAs. One control image sets the pose or layout, the prompt sets everything else, and nothing is filtered.

From $0.036/image

Workbench

Send a request

Quickstart

Examples use the selected endpoint’s public parameter definitions. Replace the API key and example media, then send the request from your server.

SDKs & integrations
POSThttps://api.spicyapi.ai/api/v1/jobs/createTask

API authentication

Keep the API key in the server environment variable SPICY_API_KEY. Use Bearer authentication and Content-Type: application/json for JSON requests.

Authorization: Bearer $SPICY_API_KEY
FLUX.1 Dev ControlNet LoRA · ControlNet Text to Image · Sample output
Sample outputUse this example
# Generate the idempotency key once; reuse it when retrying after a lost response.
export SPICY_IDEMPOTENCY_KEY="${SPICY_IDEMPOTENCY_KEY:-$(uuidgen)}"
curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
  -d '{
  "model": "black-forest-labs/flux-1-dev-controlnet-lora/text-to-image",
  "input": {
    "control_image_url": "https://example.com/reference.png",
    "num_outputs": 1,
    "output_format": "jpeg",
    "prompt": "A ceramic teapot on a linen cloth, soft window light, muted palette",
    "resolution": "1k"
  }
}'

# Copy data.taskId from the response into TASK_ID, then poll the status.
TASK_ID="TASK_ID_FROM_RESPONSE"
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=$TASK_ID" \
  -H "Authorization: Bearer $SPICY_API_KEY"

Generated from this model’s input schema — the same one the API validates every request against.

Input parameters

Place these fields inside the request body’s input object. Names and types match the API. Do not assume a default when none is specified.

control_image_urlstringRequired

The control map: a pose skeleton, a depth map, canny or soft-edge lines, or a grayscale or plain photo. It is not extracted for you. The result keeps its aspect ratio.

Default
Not specified
Constraints
format:"uri"
contentMediaType:["image/jpeg","image/png","image/webp","image/gif"]
promptstringRequired

Describe the picture you want: people, clothing, setting and light. The control image only sets the layout. Put a LoRA trigger phrase first. Up to 5,000 characters.

Default
Not specified
Constraints
minLength:1
maxLength:5000
seedintegerOptional

Keeps results comparable while you change the strength or a LoRA. It does not guarantee the exact same pixels on every run.

Default
Not specified
Constraints
minimum:0
maximum:2147483647
output_formatstringOptional

JPEG by default, or PNG or WebP. The format does not change the price.

Default
"jpeg"
Allowed values
"jpeg""png""webp"
stepsintegerOptional

Denoising steps, 1 to 50; 28 by default. Fewer steps return faster and a little softer. Does not change the price.

Default
28
Constraints
minimum:1
maximum:50
guidance_scalenumberOptional

How closely the image follows the prompt, 0 to 20; 3.5 by default. Does not change the price.

Default
3.5
Constraints
minimum:0
maximum:20
lorasarray<object>Optional

Up to three {path, scale} entries trained for FLUX.1 dev; scale 0 to 4, 1 is neutral. A LoRA for another model family fails the request. Does not change the price.

Default
[]
Constraints
maxItems:3
num_outputsintegerOptionalAffects price

How many images to generate from the same map, 1 to 4; 1 by default. Each image is billed, so four images cost four times one.

Default
1
Constraints
minimum:1
maximum:4
resolutionstringOptional

1k returns a long side of 1,024 pixels and is the default; 1.5k returns 1,536, in the control image's aspect ratio. Both cost the same.

Default
"1k"
Allowed values
"1k""1.5k"
control_guidance_endnumberOptional

The point in the denoising, 0 to 1, where the map stops applying; 0.8 by default. Ending earlier, around 0.65 for pose maps, leaves the details freer. Does not change the price.

Default
0.8
Constraints
minimum:0
maximum:1
control_guidance_startnumberOptional

The point in the denoising, 0 to 1, where the map starts to apply; 0 by default. Must not be later than the end. Does not change the price.

Default
0
Constraints
minimum:0
maximum:1
control_strengthnumberOptional

How strongly the result follows the control map, 0 to 1; 0.7 by default. Pose maps work well around 0.9; at 0.3 our pose was ignored. Values above 1 are rejected. Does not change the price.

Default
0.7
Constraints
minimum:0
maximum:1

Track the task

HTTP 202 means the task was accepted, not completed. Save the returned taskId and query that task. Do not create another task while waiting for its result.

Accepted response
HTTP/1.1 202 Accepted
Location: /api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3

{
  "code": 200,
  "msg": "success",
  "request_id": "req_example_create",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "state": "queued",
    "estimatedCost": "0.750000"
  }
}

The accepted response includes the estimate so your app can show the same amount in task history and billing.

Read the job
curl --get "https://api.spicyapi.ai/api/v1/jobs/recordInfo" \
  --data-urlencode "taskId=$TASK_ID" \
  -H "Authorization: Bearer $SPICY_API_KEY"

Start polling at about 2 seconds and gradually increase the interval. Stop at a terminal state. After a connection failure, query the original taskId. Retry creation only with the original idempotency key and body.

Or let the callback find you
import { verifyWebhook } from "@spicyapi/sdk";

// express.raw keeps the bytes we signed; express.json never matches.
app.post("/hooks/spicy", express.raw({ type: "application/json" }), (req, res) => {
  const { payload } = verifyWebhook({
    rawBody: req.body,
    timestamp: req.header("X-Webhook-Timestamp"),
    signature: req.header("X-Webhook-Signature"),
    payloadVersion: Number(req.header("X-Webhook-Payload-Version")),
    secret: process.env.SPICY_WEBHOOK_SECRET,
  });
  res.status(200).end();
  void handleFinishedTask(payload);
});

Pass callBackUrl when you create the task and the finished record is POSTed to you — no polling loop at all. Every delivery carries X-Webhook-Timestamp, X-Webhook-Signature and X-Webhook-Payload-Version. The signature is a base64 HMAC-SHA256 over taskId.timestamp.sha256(body), so it must be checked against the raw bytes: parsing the JSON and re-serialising it changes them and the check will fail. Failed deliveries are retried, so the same taskId can arrive twice — make the handler idempotent. Answer 2xx first and do the work afterwards; a slow handler is treated as a failed delivery.

Read the result

Consume output only when state is succeeded. On failure, use errorCode, errorMessage and request_id to investigate. Example costs and states do not represent an actual bill.

Completed response example
{
  "code": 200,
  "msg": "success",
  "request_id": "req_example_record",
  "data": {
    "taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
    "model": "black-forest-labs/flux-1-dev-controlnet-lora/text-to-image",
    "state": "succeeded",
    "output": {
      "assets": [
        {
          "key": "tasks/2026/08/28/job_01k3m8x9q2z4v7n5p6r8s0t1w3/3f7a1c9e5b2d48f0a6c1e3d5b7f9a1c3.png",
          "mime": "image/png",
          "url": "https://example.r2.cloudflarestorage.com/results/job_01k3m8x9q2z4v7n5p6r8s0t1w3/result.png?X-Amz-Signature=SIGNATURE_FROM_RESPONSE",
          "expiresAt": "2026-08-28T12:20:08.000Z"
        }
      ]
    },
    "cost": "0.750000",
    "settled": true,
    "createdAt": "2026-08-28T12:00:00Z",
    "completedAt": "2026-08-28T12:00:30Z"
  }
}

For each available asset, open or download assets[].url directly without an API Authorization header. The signed URL expires at expiresAt; poll the task again to refresh it while the asset remains within its retention period. A pending or unavailable asset has no URL. Treat signed URLs as private.

SDKs & integrations

Use SpicyAPI in your app, terminal, AI assistant or visual workflow. Choose a setup guide below.

Use it in ComfyUI

Install the SpicyAPI nodes from ComfyUI Manager or with the command below, then search for FLUX.1 Dev ControlNet LoRA · ControlNet Text to Image on the canvas. The node has the same options as this page and shows its price before it runs.

cd ComfyUI/custom_nodes
git clone https://github.com/spicyapi-ai/comfyui-spicyapi