# PSD API: any Photoshop file as an API

Base URL: `https://psd.anonbusiness.com`

Upload any PSD/PSB. Every layer is mapped to an API field automatically, with no naming rules needed. Then render it with your changes (text, fonts, colors, images, visibility, position…) and get back PNG, JPG, WebP, TIFF, or an edited layered PSD.

Rendering engine: PhotoCraft headless CLI (open-source, Photoshop-compatible), so blend modes, layer styles, smart objects and groups are kept.

---

## Authentication

Every request needs the header:

```
x-api-key: <YOUR_KEY>
```

Requests without the key return `401 {"error":"bad or missing x-api-key"}`.
These are public: `GET /health`, `GET /docs.md`, and result files under `GET /out/...`.

---

## Workflow

1. `POST /templates` (upload the PSD). The response lists every layer as a field, says what each one can change, and includes a ready-made example request.
2. `POST /render/:id` (send changes). The response is the image or file.

Templates are stored. Upload once, then render as many times as you like.

---

## Endpoints

### `POST /templates`: upload a PSD
`multipart/form-data`, field `file` (the .psd/.psb, up to 1 GB), optional field `name`.

```bash
curl -H "x-api-key: $KEY" -F file=@design.psd -F name="Black Friday post" \
  https://psd.anonbusiness.com/templates
```

Response `201`:
```json
{
  "id": "1fb6aa5ae1454b4f",
  "name": "Black Friday post",
  "width": 1080, "height": 1350,
  "layerCount": 7,
  "fonts": [
    { "family": "Bebas Neue", "postscript": "BebasNeue-Regular", "weight": 400, "italic": false,
      "status": "downloaded", "source": "google-fonts" },
    { "family": "Helvetica Neue", "postscript": "HelveticaNeue-Light", "weight": 300, "italic": false,
      "status": "missing", "fix": "upload the .ttf/.otf with POST /fonts (multipart field 'file')" }
  ],
  "missingFonts": ["HelveticaNeue-Light"],
  "fields": [
    { "key": "headline", "id": 6, "name": "Headline", "kind": "text", "group": null,
      "visible": true, "opacity": 1, "blend": "Normal",
      "bounds": { "x": 70, "y": 95, "w": 712, "h": 112 },
      "can": ["visible","opacity","blend","x","y","dx","dy","scale","rotate","image",
              "text","font","fontStyle","size","color","tracking","leading","align"],
      "text": { "value": "NEW COLLECTION", "font": "Bebas Neue", "fontStyle": "Regular",
                "size": 140, "color": "#1d3557", "shape": "Point" } },
    { "key": "copy/price", "id": 8, "name": "Price", "kind": "text", "group": "Copy", "...": "..." },
    { "key": "product_photo", "id": 5, "name": "Product Photo", "kind": "smart",
      "bounds": { "x": 140, "y": 360, "w": 800, "h": 800 }, "contents": { "w": 800, "h": 800 },
      "can": ["visible","opacity","blend","x","y","dx","dy","scale","rotate","image"] }
  ],
  "example": {
    "layers": {
      "headline": { "text": "NEW COLLECTION" },
      "copy/price": { "text": "€49,95" },
      "product_photo": { "image": "https://example.com/photo.jpg" }
    },
    "format": "png"
  }
}
```

Errors: `415` if the file is not a real PSD/PSB, `422` if the engine could not read it.

### `GET /templates`
Lists templates: `[{id, name, width, height, layerCount, created}]`.

### `GET /templates/:id`
Returns the full template (same shape as the upload response).

### `GET /templates/:id/preview`
Returns the original PSD rendered as PNG.

### `DELETE /templates/:id`
Deletes the template.

### `POST /templates/:id/fonts/check`
Re-checks the template's fonts (for example after uploading a missing font) and refreshes the preview. Returns `{fonts, missingFonts}`.

### `POST /render/:id`: render with changes
JSON body:
```json
{
  "layers": {
    "<field key | layer name | layer id>": { "...changes..." }
  },
  "format": "png",
  "quality": 90
}
```
- `format`: `png` (default) | `jpg` | `webp` | `tif` | `psd` (an edited, still-layered PSD)
- `quality`: 1–100, for `jpg` and `webp` only
- Add `?as=url` to get `{"url": "...", "ms": 652, "commands": 9}` instead of raw bytes. The URL is public, stays up for 7 days, and has a random name.
- Without `?as=url`, the response body is the file. Header `X-Render-Ms` gives the render time.

Shortcuts (optional, merged into `layers`):
```json
{ "text":   { "headline": "Hello" },
  "images": { "product_photo": "https://…/p.jpg" },
  "hide":   ["badge"],
  "show":   ["sale_sticker"] }
```

Example:
```bash
curl -H "x-api-key: $KEY" -H "content-type: application/json" \
  "https://psd.anonbusiness.com/render/1fb6aa5ae1454b4f?as=url" -d '{
  "layers": {
    "headline":      { "text": "BLACK FRIDAY", "font": "Anton", "color": "#1d3557" },
    "tagline":       { "text": "only this weekend", "color": "#ffd166" },
    "copy/price":    { "text": "€24,95", "dy": -40 },
    "product_photo": { "image": "https://picsum.photos/id/1080/900/1200" },
    "bg":            { "opacity": 0.6 }
  },
  "format": "jpg", "quality": 90
}'
```

---

## Fields and what they accept

Field `key` = the slugged layer name, prefixed by its group path (`group/sub_group/layer`). Duplicate names get `_2`, `_3`, and so on. In `layers` you can address a field by its `key`, its original layer `name`, or its numeric `id`.

| kind | what it is | accepted changes |
|---|---|---|
| `text` | type layer | all common keys + `text`, `font`, `fontStyle`, `size`, `color`, `tracking`, `leading`, `align` |
| `smart` | smart object | common keys (`image` replaces the contents) |
| `image` | pixel layer (incl. Background) | common keys |
| `shape` | vector shape layer | common keys |
| `fill` | solid/gradient/pattern fill layer | common keys |
| `group` | layer group | `visible`, `opacity`, `blend`, `x`, `y`, `dx`, `dy`, `image` |
| `adjustment` | adjustment layer | `visible`, `opacity`, `blend` |

Common keys:

| key | type | meaning |
|---|---|---|
| `visible` | bool | show or hide |
| `opacity` | 0–1 (or 0–100) | layer opacity |
| `blend` | string | `Normal`, `Multiply`, `Screen`, `Overlay`, `SoftLight`, `HardLight`, `ColorDodge`, `ColorBurn`, `Darken`, `Lighten`, `Difference`, `Exclusion`, `Hue`, `Saturation`, `Color`, `Luminosity`, … |
| `x`, `y` | px | move the layer's top-left corner to this absolute position |
| `dx`, `dy` | px | move relative to the current position |
| `scale` | % | scale around the centre (100 = unchanged) |
| `rotate` | degrees | rotate around the centre |
| `image` | URL or `data:` URL | put a new image in (see below) |

Text keys:

| key | type | meaning |
|---|---|---|
| `text` | string | new text (styles kept; `\n` = new line/paragraph) |
| `font` | family name | e.g. `"Anton"`. Downloaded automatically if it is a free font (see Fonts) |
| `fontStyle` | string | e.g. `"Bold"`, `"Bold Italic"`, `"Light"` |
| `size` | pt | font size |
| `color` | `"#rrggbb"` | text colour |
| `tracking` | 1/1000 em | letter spacing |
| `leading` | pt or `"auto"` | line spacing |
| `align` | `left`/`center`/`right`/`justify` | paragraph alignment |

### Images
`image` accepts an `http(s)` URL or a `data:image/...;base64,...` URL. Allowed formats are PNG, JPG, WebP, GIF and PSD, up to 60 MB. SVG is not accepted. URLs must be public internet addresses; private and internal addresses are refused.

Extra options next to `image`:

| key | values | default | meaning |
|---|---|---|---|
| `fit` | `cover` / `contain` / `none` | `cover` | cover fills and crops; contain fits fully inside |
| `mode` | `contents` / `clip` / `replace` | `contents` for smart objects, `clip` for others | see below |

- `contents` (smart objects only): replaces the smart object's contents. The image is first resized to the original contents' size, so the object's transform, warp and perspective still apply. This is the right mode for mockups.
- `clip`: places the image over the layer and clips it to that layer's shape and pixels. Good for photo frames and shapes.
- `replace`: places the image in the layer's bounding box and hides the original layer.

---

## Fonts

On upload, the API reads every text run's exact font (PostScript name, family, weight, italic) and installs any that are missing:

1. Already installed: used as is.
2. Google Fonts (about 1,800 free families): the exact static weight/italic TTF.
3. Fontsource: fallback for other open-source fonts.
4. Not found (commercial fonts such as Helvetica Neue, Proxima Nova, Adobe Fonts): reported in `missingFonts`, and text renders with a fallback font until you upload the font once:

```bash
curl -H "x-api-key: $KEY" -F file=@HelveticaNeue-Light.otf https://psd.anonbusiness.com/fonts
# then refresh the template:
curl -X POST -H "x-api-key: $KEY" https://psd.anonbusiness.com/templates/<id>/fonts/check
```

A `font` given at render time is fetched the same way. If it cannot be found, the request fails with a `400`.

`GET /fonts` lists the fonts uploaded or downloaded so far. `POST /fonts` (field `file`) accepts `.ttf`, `.otf`, `.ttc` and `.otc` and returns `{file, faces:[{family, style, postscript}]}`.

---

## Errors

All errors are JSON: `{"error": "...", ...extra}`.

| status | when | extra |
|---|---|---|
| 400 | unknown field | `unknown`, `available` (all field keys) |
| 400 | change not allowed for that layer kind | `can` (allowed keys) |
| 400 | bad image URL/type/size, private address, bad font | `fix` for fonts |
| 401 | missing or wrong `x-api-key` | |
| 404 | template not found | |
| 415 | upload is not a PSD/PSB | |
| 422 | the engine could not read or render the file | engine message |

---

## Limits

- PSD upload: 1 GB. Images: 60 MB each.
- Render timeout: 3 minutes. Typical render: 0.6–0.9 s for a 1080×1350 design.
- Result files under `/out/` are public URLs with random names.

---

## Minimal client (JavaScript)

```js
const BASE = "https://psd.anonbusiness.com", KEY = process.env.PSD_API_KEY;

// 1. upload
const fd = new FormData();
fd.append("file", new Blob([fs.readFileSync("design.psd")]), "design.psd");
const tpl = await (await fetch(`${BASE}/templates`, { method: "POST", headers: { "x-api-key": KEY }, body: fd })).json();

// 2. fill every text field + every smart object automatically
const layers = {};
for (const f of tpl.fields) {
  if (f.kind === "text") layers[f.key] = { text: f.text.value.toUpperCase() };
  if (f.kind === "smart") layers[f.key] = { image: "https://picsum.photos/1200" };
}
const r = await fetch(`${BASE}/render/${tpl.id}?as=url`, {
  method: "POST", headers: { "x-api-key": KEY, "content-type": "application/json" },
  body: JSON.stringify({ layers, format: "png" }),
});
console.log(await r.json()); // { url, ms, commands }
```
