---
name: faceswap-api
description: Swap a face into photos and videos, or upscale photos to 2K, 4K or 8K, with the faceswap.ing API, check a task's status and download the results. Use when the user asks to face swap or upscale through faceswap.ing, to call its API, or to wire it into a script, a queue worker or an HTTP step in n8n, Make or Zapier.
---

# faceswap.ing API

Base URL: https://faceswap.ing/api/v1. Requests and answers are JSON.

- Full documentation, one Markdown file: https://faceswap.ing/docs/api.md
- OpenAPI 3.1 description of every route and field: https://faceswap.ing/api/v1/openapi.json
- Code samples in curl, JavaScript and Python: https://faceswap.ing/docs/api/quickstart

## The key

- Send the key in the `x-api-key` header on every request.
- Read it from the `FACESWAP_API_KEY` environment variable. If it is not set, stop and ask the user to set it. Do not ask them to paste a key into the conversation, and never print, log or commit one.
- A key spends the swapz of one team and sees only that team's faces, tasks and upscales. If the user wants an agent's spending bounded, suggest a key on a team that holds only what the agent may spend.

## Before a launch

- A launch spends swapz. Before the first launch in a session, and before any launch the user did not ask for in so many words, say what you will send (the face, how many files, which are videos) and wait for a yes.
- Every file is a public HTTPS URL that the service downloads. There is no upload route. The URL's path has to end in the file's extension (`.jpg`, `.png`, `.mp4`, `.mov` and so on): that is how the API tells a photo from a video. A query string after it is fine.
- One launch takes one face and up to 200 target files, photos and videos mixed. Up to 5 photos of the face in all.

## Calls

| Purpose | Call |
| --- | --- |
| Check the key, list saved faces | `GET /faces?pageSize=20` |
| Launch | `POST /faceswap/tasks` |
| Read one task | `GET /faceswap/tasks/{taskId}` |
| Tasks still running | `GET /faceswap/tasks?status=queued,processing&pageSize=100` |

The smallest launch:

```bash
curl -X POST "https://faceswap.ing/api/v1/faceswap/tasks" \
  -H "x-api-key: $FACESWAP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"faceImageUrl": "https://example.com/face.jpg", "targetFiles": [{"url": "https://example.com/photo.jpg"}]}'
```

A URL on our own file host must be the team's: a file in its library, or the `outputUrl` of one of its own completed swaps or upscales, which is taken without a new check. Another team's is refused with `media_not_in_library`.

Optional fields: `faceImageUrls` (more photos of the same person), `preset` (`balanced`, `best`, `fast`), and per target `trim` (`startSeconds`, `endSeconds`, video only), `facePosition` (which face, from 0 left to right) and `swaps` (several saved faces in one target). The OpenAPI document describes every field.

## Reading the answer

- A swap launch that ran answers 200: `data` lists the tasks created and `refusal` is null. Keep every `taskId`: a launch can become several tasks.
- A launch refused part-way also answers 200: `data` lists the tasks created before it stopped, and `refusal` says why the rest did not launch. Tell the user both.
- A launch refused before it created anything answers an error status instead, as below.
- Read `GET /faceswap/tasks/{taskId}` every 5 to 10 seconds until `status` is one of `cancelled`, `completed`, `failed`, `partially_completed`.
- Each file with `status: "completed"` has its result in `outputUrl`. Download it without the key. A file that did not complete has an `errorCode` and costs nothing.
- Results are deleted 30 days after the task ends. Tell the user to keep a copy.

## Upscale photos

- `POST /upscales` enlarges up to 50 photos in one launch, each at its own tier: `2k`, `4k` or `8k`. The photo keeps its proportions. One launch is one upscale.
- Each `files[].sourceUrl` is a public HTTPS URL of a photo, or the `outputUrl` of a completed swap from this key's team, which is taken without a new check. Videos and GIFs are refused.
- A photo costs 8 swapz at `2k`, 15 swapz at `4k` and 30 swapz at `8k`, whatever its size. Before launching, say how many photos, which tier and the total, and wait for a yes, as for a swap.
- A tier that would not make the photo larger is refused with `tier_not_larger`. Offer the next tier up, or leave that photo out.

```bash
KEY=$(uuidgen)  # one per launch; reuse it only to retry this exact request
curl -X POST "https://faceswap.ing/api/v1/upscales" \
  -H "x-api-key: $FACESWAP_API_KEY" \
  -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"files": [{"sourceUrl": "https://example.com/photo.jpg", "tier": "4k"}]}'
```

- The answer is the upscale itself. Keep its `id` and read `GET /upscales/{upscaleId}` every 5 to 10 seconds until `status` is one of `cancelled`, `completed`, `failed`, `partially_completed`. Each completed photo has its result in `outputUrl`, with `outputWidth` and `outputHeight`.
- A refused upscale answers an HTTP error, not 200, and holds nothing. 400: a photo cannot be taken as asked, so tell the user; `sourceUrl` names it when we refused it before sending, and is absent when the upscale service refused a photo we could not measure, which it does not name. `sourceUrl` also names the photo on `moderation_unavailable` and `media_checking`. 402 `insufficient_swapz`: say how many swapz are missing and stop. 502 `launch_failed` or `unpriced_queue`: try once more later. 503 `moderation_unavailable`, `media_checking` or `service_busy`: wait the 30 seconds in `Retry-After` and send the same request with the same `Idempotency-Key`.
- A key's webhook receives `upscale.completed` when an upscale ends, and `data.upscale` is what the read answers.
- Upscaled photos are deleted 30 days after the upscale ends, like swap results.

## When something is refused

Every error is an `application/problem+json` document. Branch on the HTTP status, then on its `code`. Never on `detail`, whose wording changes. A swap launch and an upscale launch are refused the same way.

| Answer | What to do |
| --- | --- |
| 401 `api_key_missing`, `api_key_invalid` | The key is missing, mistyped or revoked. Ask the user to check `FACESWAP_API_KEY`. |
| 403 `organization_suspended` | The workspace is suspended. Stop. |
| 404 `not_found` | The task is not in this key's team. |
| 422 `validation` | The body does not match the schema. `property` points at the field. Fix it and resend. |
| 409 `idempotency_key_in_progress` | The first launch with this `Idempotency-Key` is still running. Wait the seconds in `Retry-After`, then send the same request with the same key. |
| 422 `idempotency_key_reused` | This key was sent before with a different body. Nothing was launched. Use a new key. |
| 429 `rate_limited` | Wait the seconds in `Retry-After`, then send the same request. |
| 402 `insufficient_swapz` | Tell the user how many swapz are missing (`shortfall.needed` minus `shortfall.available`) and stop. Do not retry. |
| 403 `declaration_required` | The account holder has to open https://faceswap.ing once and confirm the declaration. Stop. |
| 503 `moderation_unavailable`, `media_checking`, `service_busy` | Nothing was held. Wait the 30 seconds in `Retry-After` and send the same request once more. |
| 502 `launch_failed` | A fault on the service's side. Nothing was held. Try once more later. |
| 400 `moderation_refused`, `file_too_large`, `video_too_long` and other limit codes | That file will be refused again. Tell the user which file and why. |
| 400 `media_not_in_library` | A URL on our file host that is not this team's: not in its library, and not the `outputUrl` of one of its own swaps or upscales. Use the user's own copy of the file instead. |

## Limits and retries

- Per key: 120 reads every 60 seconds and 30 launches every 60 seconds, counted apart. Swap and upscale launches share the launch count.
- Every GET is safe to repeat.
- Send every launch with an `Idempotency-Key` header holding a new UUID, made once per launch. If the answer is lost (a timeout, a dropped connection, a 5xx), send the same request again with the same key: when the first one created tasks, you get its response back with `Idempotent-Replayed: true` and nothing is launched or spent twice. Keys last 24 hours.
- A launch refused before anything was held (`insufficient_swapz`, `moderation_unavailable`, `service_busy`, `launch_failed`) gives its key back: the same request with the same key runs when sent again after the cause is fixed.
- 409 `idempotency_key_in_progress`: the first request with that key is still running. Wait the seconds in `Retry-After` and send the same request with the same key. If it keeps answering 409 for minutes, list the newest tasks before launching again under a new key.
- 422 `idempotency_key_reused`: that key was used for a different body. Never change a request and keep its key; a different launch gets a new key.

```bash
KEY=$(uuidgen)  # one per launch; reuse it only to retry this exact request
curl -X POST "https://faceswap.ing/api/v1/faceswap/tasks" \
  -H "x-api-key: $FACESWAP_API_KEY" \
  -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"faceImageUrl": "https://example.com/face.jpg", "targetFiles": [{"url": "https://example.com/photo.jpg"}]}'
```

- Webhooks are registered by a person in the app, per key, never by a request. A delivery can arrive twice; deduplicate on the `webhook-id` header, and verify the signature over the raw body as the quickstart shows.

## Writing integration code

Use plain HTTP (`fetch`, `requests`); there is no SDK. Keep the key in an environment variable or a secret store, out of front-end code. Start from the samples in the quickstart, which handle 429, polling, webhooks and downloads.
