Skip to main content

Face Swap Plus is SpicyAPI's image face swap that matches skin tone between the two photos automatically and decides whether to blend the face alone or the whole head. Output is PNG, JPEG or WebP, with an optional seed.

From $0.16/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
Face Swap Plus · Image Editing · 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": "spicyapi/face-swap-v1-plus/edit",
  "input": {
    "image_url": "https://example.com/reference.png",
    "output_format": "jpeg",
    "reference_image_url": "https://example.com/reference.png"
  }
}'

# 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.

reference_image_urlstringRequired

The image supplying the facial features. Skin tone is reconciled for you, but lighting direction is not, so a reference lit from the opposite side still leaves a visible seam at the jaw. Its own framing, background and wardrobe are ignored — only the face is read — and a larger, sharper face here is the single biggest quality lever.

Default
Not specified
Constraints
minLength:1
contentMediaType:["image/jpeg","image/png","image/webp","image/gif"]
image_urlstringRequired

The image being changed. This person's pose, wardrobe, lighting and background are preserved; the head region is what gets replaced, and how far that blend reaches — face only or the whole head — is decided per pair rather than by a setting. One clearly visible face per image, reasonably frontal and large enough in frame that the features are legible; crop a group shot to the subject first. JPEG, PNG or WebP at a URL that resolves without a login and is still live when the task runs.

Default
Not specified
Constraints
minLength:1
contentMediaType:["image/jpeg","image/png","image/webp","image/gif"]
seedintegerOptional

Reuse a seed to compare two runs over the same image pair against an identical draw, so the one thing you changed is the only thing moving. It is a comparison tool, not a guarantee of an identical result. Leave it empty for a fresh draw, which is usually what you want in production.

Default
Not specified
Constraints
minimum:0
maximum:2147483647
output_qualityintegerOptional

Requested image quality for the result, from 10 to 100 (default 95). It does not change the price.

Default
95
Constraints
minimum:10
maximum:100
output_formatstringOptional

The file format of the returned image. JPEG is used when you set nothing; pick PNG when you need a lossless file to edit further and WebP when the result goes straight to the web. All three count as one output image, so the choice never changes the price — pick what your pipeline expects rather than converting afterwards and paying for a re-encode.

Default
"jpeg"
Allowed values
"png""jpeg""webp"
promptstringOptional

Optional, and in practice you should build as though it were not here. The model accepts the field, but five real calls on 2026-09-20 — two asking for sunglasses, one for a different hairstyle, two for a changed expression — all came back indistinguishable from the same request with it left empty. Nothing errors and nothing warns when that happens, so a call that leans on it will look like it worked. Leave it out.

Default
Not specified
Constraints
maxLength:200

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": "spicyapi/face-swap-v1-plus/edit",
    "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 Face Swap Plus · Image Editing 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