---
title: Restore faces in a photo
url: https://base_url.placeholder/docs/ops/restore-face
group: Concepts
---

# Restore faces in a photo

`restore_face` repairs the faces in a scanned, blurred or heavily compressed photograph — the op you point at a box of family negatives or at a decade of low-resolution profile pictures. One call, one [job handle](https://base_url.placeholder/docs/jobs), one price per image.

## What it does

A model trained on faces finds each one and redraws it: eyes, skin and hair come back from a scan that had almost nothing left. The result is a new asset, so the original file — which is the archival copy — stays exactly as it was. The default model also enlarges in the same pass: `parameters.scaleFactor` is how many times the size it was handed the result comes back, 2 unless you set it — 1 keeps the size.

Reach for it for old prints, negatives, VHS frames and anything that has been through a chat app twice. Do not reach for it to make an already-sharp photo bigger — that is [upscale](https://base_url.placeholder/docs/ops/upscale) — and do not reach for it to change a face: that is [edit](https://base_url.placeholder/docs/ops/edit), with an instruction.

## Call it

One op, six surfaces, one vocabulary: the `op` name below is the same string everywhere — the SDKs' named helpers only spell it in their language's case, `ops.restoreFace` in JavaScript — because every surface enumerates `GET /api/v1/ops` instead of carrying its own list. The request is the catalogue's own `example` for this op, so it is the one the service has checked.

#### JavaScript

```js
// pnpm add imagestep
const job = await client.ops.restoreFace("ast_1a2b", { parameters: { scaleFactor: 2 }, wait: true });
const [out] = await client.jobs.outputs(job);
```

#### Python

```python
# pip install imagestep
job = client.ops.restore_face("ast_1a2b", parameters={"scaleFactor": 2}, wait=True)
[out] = client.jobs.outputs(job)
```

#### CLI

```sh
imagestep jobs submit --op restore_face --asset-ids ast_1a2b --params '{"scaleFactor":2}' --wait
```

#### curl

```sh
# "wait" holds the response until the job is done (60 s at most): 200 with the finished job,
# or 202 with the handle — then GET /api/v1/jobs/<id>?wait=30 waits again
curl -sS -X POST https://api.imagestep.dev/api/v1/jobs \
  -H "Authorization: ApiKey $IMAGESTEP_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"op":"restore_face","assetIds":["ast_1a2b"],"parameters":{"scaleFactor":2},"wait":30}'
```

#### MCP

```text
# tool call
transform  {"op":"restore_face","asset_ids":["ast_1a2b"],"parameters":{"scaleFactor":2}}
```

#### n8n

```json
{
  "nodes": [
    {
      "parameters": {
        "resource": "op",
        "operation": "run",
        "op": "restore_face",
        "inputMode": "assetIds",
        "assetIds": "ast_1a2b",
        "parameters": "{\"scaleFactor\":2}"
      },
      "name": "ImageStep",
      "type": "n8n-nodes-imagestep.imageStep",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    }
  ],
  "connections": {}
}
```

The [n8n](https://base_url.placeholder/docs/n8n) tab is a workflow to paste onto the canvas: the **restore_face** entry of the node's Op dropdown, which is filled from the same catalogue. Full references: [SDKs](https://base_url.placeholder/docs/sdk), [CLI](https://base_url.placeholder/docs/cli), [MCP](https://base_url.placeholder/docs/mcp), [REST](https://base_url.placeholder/docs/api).

## Parameters, model and price

The entry as the catalogue stood when this page was built; `GET https://api.imagestep.dev/api/v1/ops/restore_face` has it live — the same entry the SDKs, the MCP server and the n8n node enumerate. The rows are fields of the request body. `parameters` goes to the model: its keys are the model's own, listed under `parameters` for each model in `GET /api/v1/ai-models`, and a key the model does not declare, or a value outside its options or bounds, is `400 invalid_param` naming it.

### restore_face

ai job type ai-edit · input scaled to ≤ 1024 px on the long edge

Fix faces in old or low-quality photos. The default model also enlarges the result, by parameters.scaleFactor.

| Parameter | Type | What it does |
| --- | --- | --- |
| model | string | A model whose categories include 'face_restoration' — GET /api/v1/ai-models lists them. Any other model is 400 invalid_param, with the ones that fit in details.allowed. default replicate/tencentarc/gfpgan |
| parameters | object | Passed to the model. The default model reads scaleFactor — how many times the size it was handed the repaired image comes back, 2 unless set, 1 to keep it — and version, the GFPGAN release to run. Another model's are its parameters in GET /api/v1/ai-models; a key or value a model does not declare is 400 invalid_param. |

AI op: per-item USD from the model's price; credits are charged per item. Price a batch with POST /api/v1/jobs?dryRun=true before spending. Default model replicate/tencentarc/gfpgan: $0.0028 per item.

That is how it is priced. What one request costs — another model, a bigger scale factor, forty images — is the [dry run](https://base_url.placeholder/docs/jobs#price), which prices the exact body you are about to send without creating anything.

## In a workflow

An archive is a batch, and a batch wants two things: a fixed order of steps, and somewhere to look afterwards. Save the order as a [preset](https://base_url.placeholder/docs/presets) so every photograph gets the same treatment, and put the outputs in a collection so you can find them.

```json
{
  "name": "Archive restore",
  "slug": "archive-restore",
  "description": "Restore the faces, then colour the print.",
  "steps": [
    {
      "op": "restore_face"
    },
    {
      "op": "colorize"
    }
  ]
}
```

#### JavaScript

```js
import { readFile } from "node:fs/promises";

const preset = await client.presets.create(JSON.parse(await readFile("archive-restore.json", "utf8")));
console.log(preset.slug, preset.version); // "archive-restore" 1
```

#### Python

```python
import json

preset = client.presets.create(json.load(open("archive-restore.json")))
print(preset["slug"], preset["version"])  # "archive-restore" 1
```

#### CLI

```sh
imagestep preset create -f archive-restore.json -o json
```

#### curl

```sh
curl -s https://api.imagestep.dev/api/v1/presets -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" -d @archive-restore.json
```

#### MCP

```text
# tool call — an agent has no disk to read, so the document travels in the call
save_preset  {"name":"Archive restore","slug":"archive-restore","description":"Restore the faces, then colour the print.","steps":[{"op":"restore_face"},{"op":"colorize"}]}
```

#### JavaScript

```js
const price = await client.presets.run("archive-restore", ["<scan-id>","<another-scan-id>"], { dryRun: true }); // nothing is created
console.log(price.estimatedCredits, price.steps);

const job = await client.presets.run("archive-restore", ["<scan-id>","<another-scan-id>"], { wait: true });
const outputs = await client.jobs.outputs(job);
```

#### Python

```python
price = client.presets.run("archive-restore", ["<scan-id>","<another-scan-id>"], dry_run=True)  # nothing is created
print(price["estimatedCredits"], price.get("steps"))

job = client.presets.run("archive-restore", ["<scan-id>","<another-scan-id>"], wait=True)
outputs = client.jobs.outputs(job)
```

#### CLI

```sh
imagestep jobs estimate --preset-id archive-restore --asset-ids <scan-id>,<another-scan-id>
imagestep jobs submit --preset-id archive-restore --asset-ids <scan-id>,<another-scan-id> --wait -o json
```

#### curl

```sh
# price it — the same body, nothing is created
curl -s "https://api.imagestep.dev/api/v1/jobs?dryRun=true" -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \
  -d '{"presetId":"archive-restore","assetIds":["<scan-id>","<another-scan-id>"]}'

# run it; "wait" holds the response until the job is done (60 s at most, then 202 and you wait again)
curl -s https://api.imagestep.dev/api/v1/jobs -H "Authorization: ApiKey $IMAGESTEP_API_KEY" -H "Content-Type: application/json" \
  -d '{"presetId":"archive-restore","assetIds":["<scan-id>","<another-scan-id>"],"wait":30}'
```

#### MCP

```text
# tool calls — the first prices it and creates nothing
run_preset  {"preset":"archive-restore","asset_ids":["<scan-id>","<another-scan-id>"],"dry_run":true}
run_preset  {"preset":"archive-restore","asset_ids":["<scan-id>","<another-scan-id>"]}
```

A box of prints is a long job: submit it without waiting and come back with the handle ([waiting](https://base_url.placeholder/docs/jobs#wait)), and give it a `collection` so the restored prints are one [search](https://base_url.placeholder/docs/assets#search) away.

Ingesting the scans is its own step: upload from disk, or hand the service a list of URLs and let it fetch them — both on the [assets page](https://base_url.placeholder/docs/assets#in). Either way, bytes you already hold come back as the asset you have (same SHA-1), so re-running an import does not double your library.

## Limits

The default model is handed at most 1024 px on the long edge, so its result is at most that times `scaleFactor` — 2048 px at the default, whatever the scan's resolution. A scan larger than that comes back smaller than it went in: keep it, and restore a crop of the faces when you need more of them. Small, distant or heavily occluded faces are where the model has least to work from and invents most. A photo with no face in it is not an error — it comes back with nothing repaired (still resized by `scaleFactor`), and it is still an item you paid for, so filter first if you are running over a mixed library. Items that fail carry their own [code](https://base_url.placeholder/docs/errors#items) and are not charged.

## FAQ

**Does it work on more than one face?**

Yes — the model finds the faces in the image and repairs each. A crowd at the back of a wide shot is where results get uneven, because there is very little to work from.

**Is the result the same person?**

It is a plausible reconstruction, not a recovery. The model invents detail that was never in the file, so treat the output as a restoration, and keep the scan.

**Should I restore first or upscale first?**

Restore first, then upscale. restore_face is trained on faces and works from what the scan has; an upscaler run first enlarges the damage along with everything else.

**What about the rest of the photo?**

This op is about faces. For the background, follow it with upscale, or colorize if the photograph is black and white — save the pair as a preset and it stays one job.

**What does it cost?**

Credits per image at the model's published per-image price. The dry run prices the exact batch before anything is created or charged.
