> ## Documentation Index
> Fetch the complete documentation index at: https://scrinly.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Screenshot transformations

> Turn a screenshot you already captured into an OG image, thumbnail or any size and format — without capturing the page again.

Transformations work on a screenshot you already captured and stored. Capture once with
`store: true`, keep the `screenshotId` from the response, then turn it into as many sizes
as you need — the page is not loaded again. Transformations are deterministic image
processing, not AI editing.

```bash theme={null}
# 1. Capture and store. The response includes "screenshotId": "scr_…"
curl -X POST "https://api.scrinly.com/render/screenshot" \
  -H "Authorization: Bearer $SCRINLY_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com","fullPage":true,"store":true}'

# 2. Transform it
curl "https://api.scrinly.com/render/screenshot/$SCREENSHOT_ID/transform?preset=og-image" \
  -H "Authorization: Bearer $SCRINLY_API_KEY"
```

That is the whole API: pick a **preset** or set **width / height / format**, and
optionally say **what to cut** first — a saved `region`, one `part` of it, or `bounds`.
The `screenshotId` must belong to your account and remain unexpired; regions need a
capture with `regions=true`, everything else does not.

## Use the dashboard

In the signed-in Playground, capture a stored screenshot and switch from **Capture**
to **Transform**. You can also open **Transform screenshot** from a monitor run.
Neither action captures a page or consumes credits.

Each output has a **Source** (complete screenshot, a saved region or part, a saved crop,
or custom bounds), a **Preset**, and optional size, fit, format and quality overrides —
the preset's values show as placeholders. The target overlay and **Draw bounds** sit in
the workspace toolbar next to the Screenshot/Regions switch; click a region box or drag
on the screenshot to set an output's source. **Use as source** on a region crop selects
that saved crop. The **Code** tab shows the exact request, including a plain URL for a
single output.

<img src="https://mintcdn.com/scrinly/fDodVNGnE5I6chMg/assets/playground/transform.png?fit=max&auto=format&n=fDodVNGnE5I6chMg&q=85&s=160be33f36f0518e47074fd68449beef" alt="Playground Transform mode with a saved region part selected, resize controls, and the original Tailwind CSS screenshot." width="1440" height="1000" data-path="assets/playground/transform.png" />

*Configuration example in an isolated documentation session. The demo reference and expiry are illustrative; no transformation was submitted.*

**Generate outputs** shows the maximum reservation before submitting one asynchronous job.
Closing the modal stops polling, not server processing; reopening resumes the same
job while it remains in the browser session. Successful siblings survive partial
failure, and the result shows confirmed credits rather than estimated refunds.

Customer storage credentials are one-shot. Output previews use direct provider
URLs. The dashboard does not maintain a screenshot library or transformation
history, and expired or oversized sources are never automatically recaptured.

## Send a transformation

<CodeGroup>
  ```bash cURL (URL) theme={null}
  curl "https://api.scrinly.com/render/screenshot/$SCREENSHOT_ID/transform?preset=og-image&region=region-1" \
    -H "Authorization: Bearer $SCRINLY_API_KEY"
  ```

  ```bash cURL (JSON) theme={null}
  curl -X POST "https://api.scrinly.com/render/screenshot/$SCREENSHOT_ID/transform" \
    -H "Authorization: Bearer $SCRINLY_API_KEY" \
    -H 'Content-Type: application/json' \
    -d '{"preset":"og-image","region":"region-1"}'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    `https://api.scrinly.com/render/screenshot/${process.env.SCREENSHOT_ID}/transform`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SCRINLY_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ preset: "og-image", region: "region-1" }),
    },
  );
  const result = await response.json();
  ```

  ```python Python theme={null}
  import os
  import requests

  response = requests.post(
      f"https://api.scrinly.com/render/screenshot/{os.environ['SCREENSHOT_ID']}/transform",
      headers={"Authorization": f"Bearer {os.environ['SCRINLY_API_KEY']}"},
      json={"preset": "og-image", "region": "region-1"},
      timeout=150,
  )
  result = response.json()
  ```
</CodeGroup>

The URL form (`GET`) takes the same fields as query parameters and describes one output.
It defaults to `cache=true`, so fetching the same link again reuses the stored output
instead of charging again. `outputs`, `webhookSecret` and storage credentials are POST-only.
Use region and part IDs from your own saved manifest; the example ID is a placeholder.
Authentication requires the account's bearer API key. No provider key is needed.

## Presets

| `preset` | Size | Fit | Format |
| - | - | - | - |
| `og-image` | 1200 × 630 | cover | WebP, quality 80 |
| `twitter-card` | 1200 × 600 | cover | WebP, quality 80 |
| `thumbnail` | 400 wide | contain | WebP, quality 75 |
| `square` | 1080 × 1080 | cover | JPEG, quality 85 |
| `full-width` | 1920 wide | contain | WebP, quality 80 |

Any field you set overrides the preset: `{"preset":"og-image","format":"png"}` is a PNG
OG image; `{"preset":"thumbnail","width":240}` is a 240-wide thumbnail.

On a whole full-page screenshot, the fixed-shape presets (`og-image`, `twitter-card`,
`square`) use the **top of the page** at their aspect ratio — the hero, not whatever sits
in the middle of a long page. Set `region`, `bounds` or `fit` to choose differently.

## Choose what to cut

| Fields | Source |
| - | - |
| none | Complete screenshot |
| `region` | A saved region, e.g. `"region":"region-1"` (capture with `regions=true`) |
| `region` + `part` | One part of that region, by id (`"region-1-part-0"`) or index (`0`), cut from the original pixels |
| `region` + `part` + `savedCrop: true` | The stored crop file for that part |
| `bounds` | A rectangle: `"0,100,800,600"` (x,y,width,height) or `{"x":0,"y":100,"width":800,"height":600}` |

Use one of `region` or `bounds`, not both. Original-pixel sources read the stored screenshot,
whose bytes, dimensions, MIME type, size and SHA-256 are verified first. A **saved crop**
reads the part's own stored file instead — saved JPEG crops have already been encoded once,
and result provenance records the parent screenshot hash and original bounds. Arbitrary
source URLs, uploaded images, page headers, cookies and capture settings are not accepted.

<Note>
  **Source size.** Cutting original pixels requires the *whole* screenshot to fit within
  64 MP and 24 MiB, even when you only select a small region. That covers most full-page
  captures, including 2× ones (a 3024-wide page up to about 21,000 px tall). For longer pages,
  use `savedCrop: true` on a region part: the limits then apply to the crop, not the page.
</Note>

## Size and encoding

* `width` / `height`: give one to keep the aspect ratio (rounded to a positive integer),
  or both to set the box.
* `fit` (with both sides): `contain` (default) fits everything with centered padding;
  `cover` fills the box with a centered crop; `fill` stretches.
* `format`: `jpeg`, `png` or `webp`. Omitted, the output keeps the source's family.
* `quality`: 1–100, default 80. PNG is lossless and ignores it.
* `background`: six-digit hex such as `"#ffffff"` for `contain` padding and JPEG
  transparency. Without it, PNG and WebP keep transparency and JPEG uses white.

With no size and no preset, the screenshot is only re-encoded. Every output is one
select → resize → encode step; only the final image is encoded. JPEG output is re-encoded:
lossless cropping or preservation of original JPEG bytes is not promised. Browser/codec
upgrades can change encoded bytes; provenance records the actual engine identity.

## Multiple outputs and credits

Put several outputs in an `outputs` array (POST only). Each is a preset name, or an
object for anything more specific. Top-level fields are defaults for every output:

```json theme={null}
{
  "region": "region-1",
  "outputs": ["og-image", "thumbnail", {"preset": "square", "format": "png"}],
  "cache": true
}
```

An output that says what to cut (`region`, `part`, `bounds`) replaces the default
selection rather than combining with it; every other default still applies.

All specifications validate before any credit reservation. One credit per output is
reserved before source or cache-artifact downloads. Each newly generated successful output
costs one credit. Failed outputs and verified cache hits refund that reservation.
Read `charged`, `refunded` and `net` rather than inferring settlement from HTTP status.

Single-output success returns an `assetId`, parent `screenshotId`, resolved `source`,
the `executedPlan` (the resize/encode steps that ran), provenance, direct-storage `media`, `referenceExpiresAt`,
`cacheHit` and credits. Provenance includes source/output hashes, selected bounds and
the transform, browser and codec versions.

Multiple outputs return ordered `results`, each with an `index`, success/error and credits,
plus aggregate credits. HTTP `200` means all outputs succeeded; `207` means partial success.
All failed outputs produce a typed error response. Successful siblings remain available.
If settlement cannot be confirmed, an availability error supplies a job ID for free polling;
it does not claim a completed refund. Do not blindly submit a second paid request.

## Storage, caching and retention

Outputs are always stored: omit `store` or set it to `true`; `store:false` is rejected.
Default storage uses `transforms`. Existing [B2/S3/R2 overrides](/docs/guides/storage), custom
prefixes and direct-provider URLs remain supported. No image proxy is introduced.
Single-output success includes `X-Scrinly-Storage-URL`; multiple outputs use individual
`media.url` values, without an ambiguous top-level storage header.

Caching is opt-in with `cache:true` on POST and on by default for the URL form. Identity is account-, source-reference-, geometry-,
operation-, destination- and engine-specific. Cached artifacts are downloaded and hash-verified,
not trusted from their keys or a HEAD response alone. Missing or mismatched artifacts are
regenerated; infrastructure failures do not silently trigger paid regeneration.
Equivalent outputs within one request can reuse the first output only with caching enabled.

Hits reuse the original `assetId` and metadata expiry. Cache lifetime is at most 24 hours,
bounded by both the parent reference and derived metadata expiry; hits do not renew it.
Reference retention is operator-controlled, initially 30 days. Expiry removes metadata only,
not stored images. Customer objects may become unavailable earlier under customer retention
policies. There is no asset lookup or transformation-history endpoint in this release.

## Async jobs and webhooks

Set `async:true` for the existing `202` job envelope and poll `/status/{jobId}` using
the same account's API key. Polling is free; other accounts receive `404`.
Add `webhookUrl` and optionally `webhookSecret` to request asynchronous signed delivery.
Events are `transform.completed`, `transform.partial`, and `transform.failed`, with the
settled response inside `result`. Existing [webhook signature verification](/docs/guides/async-jobs)
applies. Delivery retries do not rerun transformations or alter credit settlement.

## Limits and failures

| Limit | Maximum |
| - | - |
| JSON request body | 256 KiB |
| Outputs per request | 16 |
| Encoded source / encoded output | 24 MiB / 8 MiB |
| Source pixels (whole screenshot, or the saved crop) | 64 MP |
| Intermediate or final output | 8 MP each |
| Aggregate final-output pixels | 32 MP |
| Either image axis | 32,768 pixels |
| Processing / upload per output | 20 / 30 seconds |
| Overall execution | 120 seconds, including resumed work |

Outputs run sequentially. A deadline preserves completed outputs and fails/refunds the
remaining outputs. Missing or cross-account references return `404`; expired owned references
return `410`. Typed failures cover invalid bounds, unsupported format, input size, source
availability/hash mismatch, processing, storage and persistence. Public errors never include
provider payloads or source URLs.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.