cloudlayer.io Complete Documentation for LLMs

Every cloudlayer.io documentation page inlined in one document: API reference, guides, SDKs, examples, and integrations for AI assistants and answer engines.

# cloudlayer.io — Complete Documentation

> Platform for captures, documents, and forms: capture reachable URLs, design templates visually or in HTML, merge data, and generate PDFs and images through repeatable workflows.

This document inlines every published documentation page. The index version
lives at https://cloudlayer.io/llms.txt


## API Reference

### API Overview

Source: https://cloudlayer.io/docs/api-overview/

# API Overview

The cloudlayer.io API lets you generate PDFs and images from HTML, URLs, and templates. All endpoints follow REST conventions and return standard HTTP status codes.

## Base URL

All API requests are made to:

<div class="v1-only">

```
https://api.cloudlayer.io/v1
```

Every endpoint path in this documentation is relative to this base URL. For example, `POST /v1/html/pdf` means you send your request to `https://api.cloudlayer.io/v1/html/pdf`.

</div>

<div class="v2-only">

```
https://api.cloudlayer.io/v2
```

Every endpoint path in this documentation is relative to this base URL. For example, `POST /v2/html/pdf` means you send your request to `https://api.cloudlayer.io/v2/html/pdf`.

</div>

---

## Authentication

Every request must include your API key in the `X-API-Key` header. You can find and manage your API keys in the [cloudlayer.io dashboard](https://beta-app.cloudlayer.io).

```
X-API-Key: your-api-key-here
```

Requests without a valid API key receive a `401 Unauthorized` response.

### Example: Authenticated Request

<div class="v1-only">

**cURL**

```bash
curl -X GET "https://api.cloudlayer.io/v1/account" \
  -H "X-API-Key: your-api-key-here"
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v1/account", {
  method: "GET",
  headers: {
    "X-API-Key": "your-api-key-here",
  },
});

const data = await response.json();
```

**Python (requests)**

```python
import requests

response = requests.get(
    "https://api.cloudlayer.io/v1/account",
    headers={"X-API-Key": "your-api-key-here"},
)

data = response.json()
```

</div>

<div class="v2-only">

**cURL**

```bash
curl -X GET "https://api.cloudlayer.io/v2/account" \
  -H "X-API-Key: your-api-key-here"
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/account", {
  method: "GET",
  headers: {
    "X-API-Key": "your-api-key-here",
  },
});

const data = await response.json();
```

**Python (requests)**

```python
import requests

response = requests.get(
    "https://api.cloudlayer.io/v2/account",
    headers={"X-API-Key": "your-api-key-here"},
)

data = response.json()
```

</div>

> **Security tip:** Never expose your API key in client-side code. Always call the cloudlayer.io API from your server.

---

## Content Types

Most endpoints accept JSON request bodies. Set the `Content-Type` header accordingly:

| Scenario | Content-Type |
| --- | --- |
| JSON body (most endpoints) | `application/json` |
| Multipart form data (template uploads) | `multipart/form-data` |
| GET requests | No `Content-Type` needed |

<div class="v1-only">

Response content types depend on the endpoint:

| Scenario | Response Content-Type |
| --- | --- |
| Document generation | `application/pdf`, `image/png`, `image/jpeg`, or `image/webp` |
| Account, jobs, assets, storage | `application/json` |

All document generation responses return the raw file binary directly.

</div>

<div class="v2-only">

All v2 responses return JSON:

| Scenario | Response Content-Type |
| --- | --- |
| Document generation (async — default) | `application/json` (job metadata) |
| Document generation (sync, `async: false`) | `application/json` (completed job with asset URL) |
| Account, jobs, assets, storage | `application/json` |

v2 always returns JSON with job metadata. Only v1 returns raw binary file content directly.

</div>

---

## Synchronous vs. Asynchronous Requests

<div class="v1-only">

All document generation requests in v1 are processed **synchronously**. The API waits for the document to be generated and returns the raw file binary (PDF or image) directly in the response body.

```bash
curl -X POST "https://api.cloudlayer.io/v1/html/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{"html": "PGgxPkhlbGxvIFdvcmxkPC9oMT4="}' \
  --output result.pdf
```

The response is the raw PDF binary. Use `--output` (cURL) or handle the response as a binary stream.

> **Tip:** For large or complex documents that take a long time to render, consider switching to the v2 API which supports asynchronous processing.

</div>

<div class="v2-only">

By default, document generation endpoints in v2 process your request **asynchronously**. The API returns a JSON response immediately with the job ID, and the generated file is stored and accessible via the [Assets](/docs/assets/) endpoint once processing completes.

### Asynchronous (default)

```bash
curl -X POST "https://api.cloudlayer.io/v2/html/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{"html": "PGgxPkhlbGxvIFdvcmxkPC9oMT4="}'
```

Returns a JSON response immediately:

```json
{
  "id": "1705312200000",
  "status": "pending"
}
```

Poll the [Jobs](/docs/jobs/) endpoint or use [Webhooks](/docs/webhooks/) to be notified when the job completes. Once complete, retrieve the result from the [Assets](/docs/assets/) endpoint.

### Synchronous

To process a request synchronously, include `"async": false` in your request body. The API still returns JSON, but waits for processing to complete before responding:

```bash
curl -X POST "https://api.cloudlayer.io/v2/html/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{"html": "PGgxPkhlbGxvIFdvcmxkPC9oMT4=", "async": false}'
```

The response is JSON with the completed job details, including the asset URL for downloading the generated file.

> **Note:** In v2, `storage` also defaults to `true`. When `async` is `true` (the default), the generated file is automatically stored and accessible via the Assets endpoint. You can set `"storage": false` to disable storage, but this requires `"async": false` as well.

</div>

---

## Rate Limits

All accounts are rate-limited to **60 requests per 60 seconds**. This limit applies to all endpoints and all plans equally. Exceeding the limit returns a `429 Too Many Requests` response.

Separately, your plan has a **usage quota** — a total number of API calls allowed per billing period. The `cl-calls-remaining` response header shows how many calls remain in your current period.

| Header | Description |
| --- | --- |
| `cl-calls-remaining` | Number of API calls remaining in your current billing period |
| `Retry-After` | Seconds to wait before retrying, sent on a `429` response |

**Best practice:** Implement retry logic with exponential backoff for `429` responses, honoring `Retry-After` when it is present. Monitor `cl-calls-remaining` to track usage against your plan quota.

### Concurrency

Rate limiting and concurrency are different controls and you can meet either one first. The rate limit above caps how many requests you may *send* in a window. Concurrency caps how many renders run *at the same time*, and unlike the rate limit it varies by plan: extra requests queue rather than failing, so a batch finishes more slowly rather than returning `429`.

The concurrency included with each plan is listed on the [pricing page](https://cloudlayer.io/pricing/) alongside the render allowance.

---

## HTTP Status Codes

| Code | Meaning | Description |
| --- | --- | --- |
| `200` | OK | Request succeeded. Response body contains the result. |
| `400` | Bad Request | The request body is malformed or missing required parameters. |
| `401` | Unauthorized | Missing or invalid API key, or account has exceeded its usage limits. |
| `404` | Not Found | The requested resource does not exist (e.g., invalid route). |
| `429` | Too Many Requests | Rate limit exceeded (60 requests per 60 seconds). Retry with exponential backoff. |
| `500` | Internal Server Error | Something went wrong on our end. Contact support if it persists. |

---

## Error Response Format

When an error occurs, the API returns a JSON body with the status code and a message:

```json
{
  "statusCode": 400,
  "message": "The 'html' field is required and must be a base64-encoded string."
}
```

| Field | Type | Description |
| --- | --- | --- |
| `statusCode` | number | The HTTP status code (matches the response status). |
| `message` | string | A human-readable description of the error. |

Some error responses may also include a `status` field with the value `"error"`:

```json
{
  "status": "error",
  "message": "Generation failed: page timeout exceeded."
}
```

---

## Common Request Parameters

Several parameters appear across multiple document generation endpoints. These are documented in detail on each endpoint page, but here is a quick reference:

<div class="v2-only">

### Async & Storage Parameters (v2 only)

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `async` | boolean | `true` | Process the request asynchronously. When `true`, the API returns immediately with a job ID. Set to `false` to wait for processing to complete before the API responds (response is still JSON). |
| `storage` | boolean | `true` | Store the generated file in cloudlayer.io storage. When `true`, the file is accessible via the [Assets](/docs/assets/) endpoint. Requires a plan that supports storage. |

</div>

### Base Parameters (all generation endpoints)

| Parameter | Type | Description |
| --- | --- | --- |
| `autoScroll` | boolean | Scroll the page to trigger lazy-loaded content before capture. |
| `delay` | number | Wait time in milliseconds after the page loads before capturing. |
| `filename` | string | Set the `Content-Disposition` filename for the response. |
| `timeout` | number | Maximum time in milliseconds to wait for the page to load. |
| `viewPort` | object | Configure the browser viewport dimensions and device emulation. |
| `waitUntil` | string | When to consider navigation complete (`load`, `domcontentloaded`, `networkidle0`, `networkidle2`). |

### PDF-Specific Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| `format` | string | Page size (`letter`, `legal`, `tabloid`, `a0`-`a6`, etc.). |
| `margin` | object | Page margins with unit support (`px`, `in`, `cm`, `mm`). |
| `landscape` | boolean | Use landscape orientation. |
| `printBackground` | boolean | Include background colors and images. |
| `headerTemplate` | object | Custom header for each page. |
| `footerTemplate` | object | Custom footer for each page. |

### Image-Specific Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| `imageType` | string | Output format: `png`, `jpg`, `jpeg`, `webp`, or `svg`. |
| `transparent` | boolean | Render with a transparent background (PNG and WebP only). |
| `trim` | boolean | Trim whitespace from the edges of the image. |

---

## List Results

List endpoints (`GET /jobs`, `GET /assets`) return the 10 most recent items, ordered by creation date (newest first). There are no pagination query parameters.

---

## Next Steps

- [HTML to PDF](/docs/html-to-pdf/) — Generate PDFs from raw HTML
- [URL to PDF](/docs/url-to-pdf/) — Capture any web page as a PDF
- [Template to PDF](/docs/template-to-pdf/) — Generate PDFs from reusable templates
- [HTML to Image](/docs/html-to-image/) — Generate images from raw HTML
- [URL to Image](/docs/url-to-image/) — Capture any web page as an image
- [Template to Image](/docs/template-to-image/) — Generate images from reusable templates

### HTML to PDF

Source: https://cloudlayer.io/docs/html-to-pdf/

# HTML to PDF

Generate a PDF document from raw HTML content. The HTML is sent as a base64-encoded string in the request body.

## Endpoint

<div class="v1-only">

```
POST /v1/html/pdf
```

All requests are processed synchronously. The response body is the raw PDF file.

</div>

<div class="v2-only">

```
POST /v2/html/pdf
```

Requests default to **asynchronous** processing (`async: true`, `storage: true`). Add `"async": false` to receive the raw PDF directly. See [Sync vs. Async](/docs/api-overview/#synchronous-vs-asynchronous-requests) for details.

</div>

---

## Quick Start

Encode your HTML as base64 and send it in the `html` field:

<div class="v1-only">

**cURL**

```bash
# Base64 encode your HTML
HTML_BASE64=$(echo '<html><body><h1>Hello World</h1><p>Generated by cloudlayer.io</p></body></html>' | base64)

curl -X POST "https://api.cloudlayer.io/v1/html/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d "{\"html\": \"$HTML_BASE64\"}" \
  --output result.pdf
```

**JavaScript (fetch)**

```javascript
const html = `<html>
  <body>
    <h1>Hello World</h1>
    <p>Generated by cloudlayer.io</p>
  </body>
</html>`;

const response = await fetch("https://api.cloudlayer.io/v1/html/pdf", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html: btoa(html),
  }),
});

const pdf = await response.arrayBuffer();
// Save to file, send to client, etc.
```

**Python (requests)**

```python
import base64
import requests

html = """<html>
  <body>
    <h1>Hello World</h1>
    <p>Generated by cloudlayer.io</p>
  </body>
</html>"""

response = requests.post(
    "https://api.cloudlayer.io/v1/html/pdf",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "html": base64.b64encode(html.encode()).decode(),
    },
)

with open("result.pdf", "wb") as f:
    f.write(response.content)
```

</div>

<div class="v2-only">

**cURL**

```bash
# Base64 encode your HTML
HTML_BASE64=$(echo '<html><body><h1>Hello World</h1><p>Generated by cloudlayer.io</p></body></html>' | base64)

curl -X POST "https://api.cloudlayer.io/v2/html/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d "{\"html\": \"$HTML_BASE64\"}"
```

Response:

```json
{
  "id": "abc123",
  "status": "pending"
}
```

**JavaScript (fetch)**

```javascript
const html = `<html>
  <body>
    <h1>Hello World</h1>
    <p>Generated by cloudlayer.io</p>
  </body>
</html>`;

const response = await fetch("https://api.cloudlayer.io/v2/html/pdf", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html: btoa(html),
  }),
});

const { id, status } = await response.json();
// Poll job status or use webhooks to know when the PDF is ready
```

**Python (requests)**

```python
import base64
import requests

html = """<html>
  <body>
    <h1>Hello World</h1>
    <p>Generated by cloudlayer.io</p>
  </body>
</html>"""

response = requests.post(
    "https://api.cloudlayer.io/v2/html/pdf",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "html": base64.b64encode(html.encode()).decode(),
    },
)

job = response.json()
# Poll job status or use webhooks to know when the PDF is ready
```

> **Tip:** v2 always returns JSON. Use the job `id` to poll the [Jobs](/docs/jobs/) endpoint or configure [Webhooks](/docs/webhooks/) for completion notifications. The generated file is accessible via the [Assets](/docs/assets/) endpoint. Add `"async": false` to wait for processing to complete — the response is still JSON but includes the completed job details.

</div>

---

## Parameters

### Required

| Parameter | Type | Description |
| --- | --- | --- |
| `html` | string | **Required.** Base64-encoded HTML content. The HTML can include inline CSS, `<style>` tags, and `<link>` references to external stylesheets. |

### Base Parameters

These parameters control page loading behavior, viewport configuration, and general output settings.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `autoScroll` | boolean | `false` | Automatically scroll the page to the bottom before generating the PDF. Useful for triggering lazy-loaded images and infinite scroll content. |
| `delay` | number | `0` | Time in milliseconds to wait after the page has loaded (and after `waitUntil` is satisfied) before generating the PDF. Use this to allow JavaScript animations or dynamic content to finish rendering. |
| `filename` | string | — | Sets the `Content-Disposition` header filename on the response. When set, browsers will prompt to save the file with this name. Include the `.pdf` extension (e.g., `"invoice-2024.pdf"`). |
| `generatePreview` | object | — | Generate a thumbnail preview image alongside the PDF. See [generatePreview](#generatepreview-object) for details. |
| `height` | string | — | Override the page height. Accepts CSS units (e.g., `"11in"`, `"297mm"`, `"1200px"`). When set, takes precedence over the `format` height. |
| `width` | string | — | Override the page width. Accepts CSS units (e.g., `"8.5in"`, `"210mm"`, `"800px"`). When set, takes precedence over the `format` width. |
| `inline` | boolean | `false` | When `true`, sets the `Content-Disposition` header to `inline` instead of `attachment`, causing browsers to display the PDF in the browser tab rather than downloading it. |
| `landscape` | boolean | `false` | Generate the PDF in landscape orientation. |
| `preferCSSPageSize` | boolean | `false` | When `true`, use the CSS `@page` size declaration instead of the `format` parameter. Useful when your CSS already defines the page dimensions via `@page { size: A4; }`. |
| `projectId` | string | — | Associate this generation with a specific project for organization and filtering in the dashboard. |
| `scale` | number | `1` | Scale of the web page rendering. Valid range: `0.1` to `2`. Values less than 1 shrink the content; values greater than 1 enlarge it. |
| `timeout` | number | `30000` | Maximum time in milliseconds to wait for the page to load. If the page does not load within this time, the request fails with a timeout error. Maximum value depends on your plan. |
| `timeZone` | string | — | Set the browser timezone for rendering. Uses IANA timezone names (e.g., `"America/New_York"`, `"Europe/London"`, `"Asia/Tokyo"`). Affects `Date` objects and time-related rendering. |
| `viewPort` | object | — | Configure the browser viewport. See [viewPort](#viewport-object) for details. |
| `waitForSelector` | object | — | Wait for a specific CSS selector to appear in the DOM before generating. See [waitForSelector](#waitforselector-object) for details. |
| `waitUntil` | string | — | When to consider the page navigation complete. See [waitUntil](#waituntil) for options. |

### PDF Parameters

These parameters control PDF-specific output settings like page size, margins, headers, and footers.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `pageRanges` | string | — | Paper ranges to print, e.g., `"1-5"`, `"8"`, `"1-3, 5"`. An empty string means all pages. |
| `format` | string | `"letter"` | Page size format. See [format](#format-options) for all options. |
| `margin` | object | — | Page margins. See [margin](#margin-object) for details. |
| `printBackground` | boolean | `false` | Print background graphics (colors, images, gradients). Set to `true` to include backgrounds. |
| `headerTemplate` | object | — | Custom page header. See [headerTemplate / footerTemplate](#headertemplate--footertemplate-object) for details. |
| `footerTemplate` | object | — | Custom page footer. See [headerTemplate / footerTemplate](#headertemplate--footertemplate-object) for details. |

---

## Parameter Details

### `generatePreview` Object

Generate a thumbnail preview image of the first page of the PDF.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `width` | number | `300` | Width of the preview image in pixels. |
| `height` | number | `400` | Height of the preview image in pixels. |
| `type` | string | `"png"` | Image format: `"png"`, `"jpg"`, or `"webp"`. |
| `quality` | number | `80` | Image quality (1-100). Only applies to `jpg` and `webp`. |
| `maintainAspectRatio` | boolean | `true` | Maintain the aspect ratio of the PDF page in the preview. When `true`, the image fits within the given dimensions without distortion. |

```json
{
  "html": "...",
  "generatePreview": {
    "width": 600,
    "height": 800,
    "type": "png",
    "quality": 90,
    "maintainAspectRatio": true
  }
}
```

### `viewPort` Object

Configure the browser viewport used for rendering. This affects how responsive layouts, media queries, and CSS breakpoints behave.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `width` | number | `1440` | Viewport width in pixels. |
| `height` | number | `900` | Viewport height in pixels. |
| `deviceScaleFactor` | number | `1` | Device scale factor (DPR). Set to `2` for Retina-style rendering. |
| `isMobile` | boolean | `false` | Emulate a mobile device (affects `navigator.userAgent` and viewport meta tag behavior). |
| `hasTouch` | boolean | `false` | Enable touch event support. |
| `isLandscape` | boolean | `false` | Set the viewport to landscape orientation. |

```json
{
  "html": "...",
  "viewPort": {
    "width": 1920,
    "height": 1080,
    "deviceScaleFactor": 2
  }
}
```

### `waitForSelector` Object

Wait for a specific element to appear in the DOM before generating the PDF. Useful for single-page applications or pages that render content dynamically.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `selector` | string | — | **Required.** CSS selector to wait for (e.g., `"#content-loaded"`, `".chart-rendered"`). |
| `options` | object | — | Options for the wait behavior. |
| `options.visible` | boolean | `false` | Wait for the element to be visible (not just present in the DOM). |
| `options.hidden` | boolean | `false` | Wait for the element to be hidden or removed from the DOM. |
| `options.timeout` | number | `30000` | Maximum time in milliseconds to wait for the selector. |

```json
{
  "html": "...",
  "waitForSelector": {
    "selector": "#charts-loaded",
    "options": {
      "visible": true,
      "timeout": 10000
    }
  }
}
```

### `waitUntil`

Controls when the page is considered "loaded." Choose the strategy that best fits your content:

| Value | Description |
| --- | --- |
| `"load"` | Wait for the `load` event (all resources including images and stylesheets). |
| `"domcontentloaded"` | Wait for the `DOMContentLoaded` event (HTML parsed, but images/stylesheets may still be loading). Fastest option. |
| `"networkidle0"` | Wait until there are no more than 0 network connections for at least 500ms. Best for pages that make many API calls. |
| `"networkidle2"` | Wait until there are no more than 2 network connections for at least 500ms. Good default for most pages. |

### Format Options

Standard paper sizes for the `format` parameter:

| Value | Dimensions |
| --- | --- |
| `"letter"` | 8.5in x 11in |
| `"legal"` | 8.5in x 14in |
| `"tabloid"` | 11in x 17in |
| `"ledger"` | 17in x 11in |
| `"a0"` | 33.1in x 46.8in |
| `"a1"` | 23.4in x 33.1in |
| `"a2"` | 16.54in x 23.4in |
| `"a3"` | 11.7in x 16.54in |
| `"a4"` | 8.27in x 11.7in |
| `"a5"` | 5.83in x 8.27in |
| `"a6"` | 4.13in x 5.83in |

### Margin Object

Set page margins using CSS units. Supported units: `px`, `in`, `cm`, `mm`.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `top` | string | `"0"` | Top margin (e.g., `"1in"`, `"25mm"`, `"20px"`). |
| `bottom` | string | `"0"` | Bottom margin. |
| `left` | string | `"0"` | Left margin. |
| `right` | string | `"0"` | Right margin. |

```json
{
  "html": "...",
  "margin": {
    "top": "1in",
    "bottom": "1in",
    "left": "0.75in",
    "right": "0.75in"
  }
}
```

> **Note:** When using `headerTemplate` or `footerTemplate`, you must set sufficient top or bottom margins to make space for them. Headers and footers render inside the margin area.

### `headerTemplate` / `footerTemplate` Object

Add custom headers and footers to each page of the PDF. The template content is rendered inside the margin area, so make sure your `margin.top` or `margin.bottom` is large enough to accommodate the template.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `selector` | string | — | CSS selector of an element within your HTML to use as the header/footer content. The element's inner HTML is extracted and used as the template. |
| `method` | string | `"template"` | How to extract the template content: `"template"` extracts inner HTML, `"extract"` extracts the element with its computed styles. |
| `margin` | object | — | Override margins specifically for the header/footer area. Same format as the page `margin` object. |
| `style` | string | — | Additional CSS to apply to the header/footer. Use this for positioning, font sizes, etc. |
| `imageStyle` | string | — | CSS styles applied specifically to images within the header/footer. Useful for controlling logo sizing. |

Templates support these special CSS classes that are automatically populated:

| CSS Class | Value |
| --- | --- |
| `date` | Current date |
| `title` | Document title |
| `url` | Document URL |
| `pageNumber` | Current page number |
| `totalPages` | Total number of pages |

```json
{
  "html": "...",
  "margin": {
    "top": "100px",
    "bottom": "80px"
  },
  "headerTemplate": {
    "selector": "#page-header",
    "method": "extract",
    "style": "font-size: 10px; width: 100%; text-align: center; padding: 10px 0;",
    "imageStyle": "max-height: 40px; width: auto;"
  },
  "footerTemplate": {
    "selector": "#page-footer",
    "method": "extract",
    "style": "font-size: 9px; width: 100%; text-align: center; padding: 10px 0;"
  }
}
```

> **Tip:** Include the header and footer elements in your HTML content (they can be hidden with `display: none`), then reference them with the `selector` field. The `extract` method preserves their styling.

---

## Full Example

Generate a styled invoice PDF with custom margins, headers, and footers:

**cURL**

```bash
HTML=$(cat <<'HTML'
<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: Arial, sans-serif; padding: 40px; }
    .invoice-header { display: flex; justify-content: space-between; margin-bottom: 40px; }
    .company { font-size: 24px; font-weight: bold; color: #333; }
    table { width: 100%; border-collapse: collapse; margin-top: 20px; }
    th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
    th { background: #f8f9fa; font-weight: 600; }
    .total { font-size: 18px; font-weight: bold; text-align: right; margin-top: 20px; }
    #page-footer { display: none; }
  </style>
</head>
<body>
  <div class="invoice-header">
    <div class="company">Acme Corp</div>
    <div>Invoice #1042 — 2024-01-15</div>
  </div>
  <table>
    <thead>
      <tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr>
    </thead>
    <tbody>
      <tr><td>Widget A</td><td>10</td><td>$25.00</td><td>$250.00</td></tr>
      <tr><td>Widget B</td><td>5</td><td>$40.00</td><td>$200.00</td></tr>
      <tr><td>Service Fee</td><td>1</td><td>$75.00</td><td>$75.00</td></tr>
    </tbody>
  </table>
  <div class="total">Total: $525.00</div>
  <div id="page-footer">
    Page <span class="pageNumber"></span> of <span class="totalPages"></span>
  </div>
</body>
</html>
HTML
)

HTML_BASE64=$(echo "$HTML" | base64 -w 0)

curl -X POST "https://api.cloudlayer.io/v2/html/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d "{
    \"html\": \"$HTML_BASE64\",
    \"format\": \"letter\",
    \"margin\": {
      \"top\": \"0.75in\",
      \"bottom\": \"1in\",
      \"left\": \"0.75in\",
      \"right\": \"0.75in\"
    },
    \"printBackground\": true,
    \"footerTemplate\": {
      \"selector\": \"#page-footer\",
      \"method\": \"template\",
      \"style\": \"font-size: 9px; width: 100%; text-align: center; color: #999;\"
    },
    \"filename\": \"invoice-1042.pdf\"
  }"
```

**JavaScript (fetch)**

```javascript
const html = `<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: Arial, sans-serif; padding: 40px; }
    .invoice-header { display: flex; justify-content: space-between; margin-bottom: 40px; }
    .company { font-size: 24px; font-weight: bold; color: #333; }
    table { width: 100%; border-collapse: collapse; margin-top: 20px; }
    th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
    th { background: #f8f9fa; font-weight: 600; }
    .total { font-size: 18px; font-weight: bold; text-align: right; margin-top: 20px; }
  </style>
</head>
<body>
  <div class="invoice-header">
    <div class="company">Acme Corp</div>
    <div>Invoice #1042</div>
  </div>
  <table>
    <thead><tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr></thead>
    <tbody>
      <tr><td>Widget A</td><td>10</td><td>$25.00</td><td>$250.00</td></tr>
      <tr><td>Widget B</td><td>5</td><td>$40.00</td><td>$200.00</td></tr>
    </tbody>
  </table>
  <div class="total">Total: $450.00</div>
</body>
</html>`;

const response = await fetch("https://api.cloudlayer.io/v2/html/pdf", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html: btoa(html),
    format: "letter",
    margin: {
      top: "0.75in",
      bottom: "0.75in",
      left: "0.75in",
      right: "0.75in",
    },
    printBackground: true,
    filename: "invoice-1042.pdf",
  }),
});

const job = await response.json();
// job.id contains the job ID — poll or use webhooks for completion
```

**Python (requests)**

```python
import base64
import requests

html = """<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: Arial, sans-serif; padding: 40px; }
    table { width: 100%; border-collapse: collapse; margin-top: 20px; }
    th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
    th { background: #f8f9fa; }
    .total { font-size: 18px; font-weight: bold; text-align: right; margin-top: 20px; }
  </style>
</head>
<body>
  <h1>Invoice #1042</h1>
  <table>
    <thead><tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr></thead>
    <tbody>
      <tr><td>Widget A</td><td>10</td><td>$25.00</td><td>$250.00</td></tr>
      <tr><td>Widget B</td><td>5</td><td>$40.00</td><td>$200.00</td></tr>
    </tbody>
  </table>
  <div class="total">Total: $450.00</div>
</body>
</html>"""

response = requests.post(
    "https://api.cloudlayer.io/v2/html/pdf",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "html": base64.b64encode(html.encode()).decode(),
        "format": "letter",
        "margin": {
            "top": "0.75in",
            "bottom": "0.75in",
            "left": "0.75in",
            "right": "0.75in",
        },
        "printBackground": True,
        "filename": "invoice-1042.pdf",
    },
)

job = response.json()
# job["id"] contains the job ID — poll or use webhooks for completion
```

---

## Tips

- **External resources:** Your HTML can reference external CSS, fonts, and images via URLs. Make sure they are publicly accessible, or the renderer will not be able to load them.
- **Web fonts:** Use `<link>` tags to load Google Fonts or other web fonts. Set `waitUntil` to `"networkidle0"` to ensure fonts finish loading before rendering.
- **Large documents:** For documents with many pages, increase the `timeout` value and consider using async mode.
- **Print styles:** Use `@media print` CSS rules to control what appears in the PDF. The renderer respects print media queries.
- **Base64 encoding:** Make sure your base64 encoding does not include line breaks. Use `base64 -w 0` on Linux or equivalent.

### URL to PDF

Source: https://cloudlayer.io/docs/url-to-pdf/

# URL to PDF

Generate a PDF from a live web page URL. Supports both simple GET requests and full-featured POST requests with authentication, cookies, and batch processing.

## Endpoints

The **GET** endpoint is a convenience method for simple captures with minimal
configuration. The **POST** endpoint supports the full range of parameters.

<div class="v1-only">

```
GET  /v1/url/pdf
POST /v1/url/pdf
```

All v1 requests are processed synchronously. The response body is the raw PDF file.

</div>

<div class="v2-only">

```
GET  /v2/url/pdf
POST /v2/url/pdf
```

v2 requests default to **asynchronous** processing (`async: true`, `storage: true`). Add `"async": false` to receive the raw PDF directly. See [Sync vs. Async](/docs/api-overview/#synchronous-vs-asynchronous-requests) for details.

</div>

---

## Quick Start (GET)

The simplest way to capture a web page as a PDF — just pass the URL as a query parameter:

<div class="v1-only">

**cURL**

```bash
curl -X GET "https://api.cloudlayer.io/v1/url/pdf?url=https://example.com" \
  -H "X-API-Key: your-api-key-here" \
  --output example.pdf
```

**JavaScript (fetch)**

```javascript
const url = encodeURIComponent("https://example.com");

const response = await fetch(
  `https://api.cloudlayer.io/v1/url/pdf?url=${url}`,
  {
    headers: {
      "X-API-Key": "your-api-key-here",
    },
  }
);

const pdf = await response.arrayBuffer();
```

**Python (requests)**

```python
import requests

response = requests.get(
    "https://api.cloudlayer.io/v1/url/pdf",
    params={"url": "https://example.com"},
    headers={"X-API-Key": "your-api-key-here"},
)

with open("example.pdf", "wb") as f:
    f.write(response.content)
```

</div>

<div class="v2-only">

**cURL**

```bash
curl -X GET "https://api.cloudlayer.io/v2/url/pdf?url=https://example.com" \
  -H "X-API-Key: your-api-key-here"
```

Response:

```json
{
  "id": "abc123",
  "status": "pending"
}
```

**JavaScript (fetch)**

```javascript
const url = encodeURIComponent("https://example.com");

const response = await fetch(
  `https://api.cloudlayer.io/v2/url/pdf?url=${url}`,
  {
    headers: {
      "X-API-Key": "your-api-key-here",
    },
  }
);

const job = await response.json();
```

**Python (requests)**

```python
import requests

response = requests.get(
    "https://api.cloudlayer.io/v2/url/pdf",
    params={"url": "https://example.com"},
    headers={"X-API-Key": "your-api-key-here"},
)

job = response.json()
```

</div>

---

## Quick Start (POST)

Use POST for full control over the output:

<div class="v1-only">

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v1/url/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "format": "a4",
    "margin": {
      "top": "1in",
      "bottom": "1in",
      "left": "0.75in",
      "right": "0.75in"
    },
    "printBackground": true,
    "waitUntil": "networkidle0"
  }' \
  --output example.pdf
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v1/url/pdf", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com",
    format: "a4",
    margin: { top: "1in", bottom: "1in", left: "0.75in", right: "0.75in" },
    printBackground: true,
    waitUntil: "networkidle0",
  }),
});

const pdf = await response.arrayBuffer();
```

**Python (requests)**

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v1/url/pdf",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "url": "https://example.com",
        "format": "a4",
        "margin": {"top": "1in", "bottom": "1in", "left": "0.75in", "right": "0.75in"},
        "printBackground": True,
        "waitUntil": "networkidle0",
    },
)

with open("example.pdf", "wb") as f:
    f.write(response.content)
```

</div>

<div class="v2-only">

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v2/url/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "format": "a4",
    "margin": {
      "top": "1in",
      "bottom": "1in",
      "left": "0.75in",
      "right": "0.75in"
    },
    "printBackground": true,
    "waitUntil": "networkidle0"
  }'
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/url/pdf", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com",
    format: "a4",
    margin: { top: "1in", bottom: "1in", left: "0.75in", right: "0.75in" },
    printBackground: true,
    waitUntil: "networkidle0",
  }),
});

const job = await response.json();
```

**Python (requests)**

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v2/url/pdf",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "url": "https://example.com",
        "format": "a4",
        "margin": {"top": "1in", "bottom": "1in", "left": "0.75in", "right": "0.75in"},
        "printBackground": True,
        "waitUntil": "networkidle0",
    },
)

job = response.json()
```

> **Tip:** v2 always returns JSON. Use the job `id` to poll the [Jobs](/docs/jobs/) endpoint or configure [Webhooks](/docs/webhooks/) for completion notifications. The generated file is accessible via the [Assets](/docs/assets/) endpoint. Add `"async": false` to wait for processing to complete — the response is still JSON but includes the completed job details.

</div>

---

## GET Parameters

The GET endpoint accepts these query parameters:

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `url` | string | Yes | — | The URL of the web page to capture. Must be URL-encoded. |
| `timeout` | number | No | `30000` | Maximum time in milliseconds to wait for the page to load. |

---

## POST Parameters

### URL Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `url` | string | Yes | — | The URL of the web page to capture. |
| `authentication` | object | No | — | Credentials for HTTP Basic Authentication. See [authentication](#authentication-object). |
| `cookies` | array | No | — | Cookies to set before navigating to the URL. See [cookies](#cookies-array). |
| `batch` | object | No | — | Object with a `urls` property containing an array of URLs to combine into a single multi-page PDF. See [Batch Processing](#batch-processing). |

### Base Parameters

These parameters control page loading behavior, viewport configuration, and general output settings.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `autoScroll` | boolean | `false` | Scroll the page to the bottom before capturing. Triggers lazy-loaded content. |
| `delay` | number | `0` | Wait time in milliseconds after the page loads before generating the PDF. |
| `filename` | string | — | Set the `Content-Disposition` filename on the response. |
| `generatePreview` | object | — | Generate a thumbnail preview image. See [HTML to PDF — generatePreview](/docs/html-to-pdf/#generatepreview-object). |
| `height` | string | — | Override page height with a CSS value (e.g., `"11in"`, `"297mm"`). |
| `width` | string | — | Override page width with a CSS value (e.g., `"8.5in"`, `"210mm"`). |
| `inline` | boolean | `false` | Display the PDF inline in the browser instead of triggering a download. |
| `landscape` | boolean | `false` | Generate the PDF in landscape orientation. |
| `preferCSSPageSize` | boolean | `false` | Use the CSS `@page` size instead of the `format` parameter. |
| `projectId` | string | — | Associate this request with a project for dashboard organization. |
| `scale` | number | `1` | Page rendering scale (`0.1` to `2`). |
| `timeout` | number | `30000` | Maximum time in milliseconds to wait for page load. |
| `timeZone` | string | — | Browser timezone (IANA name, e.g., `"America/New_York"`). |
| `viewPort` | object | — | Browser viewport configuration. See [HTML to PDF — viewPort](/docs/html-to-pdf/#viewport-object). |
| `waitForSelector` | object | — | Wait for a CSS selector before generating. See [HTML to PDF — waitForSelector](/docs/html-to-pdf/#waitforselector-object). |
| `waitUntil` | string | `"networkidle2"` | Navigation completion strategy: `"load"`, `"domcontentloaded"`, `"networkidle0"`, `"networkidle2"`. |

### PDF Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `pageRanges` | string | — | Page ranges to include (e.g., `"1-5"`, `"1,3,5-7"`). |
| `format` | string | `"letter"` | Page size: `"letter"`, `"legal"`, `"tabloid"`, `"ledger"`, `"a0"` through `"a6"`. |
| `margin` | object | — | Page margins with CSS units. See [HTML to PDF — margin](/docs/html-to-pdf/#margin-object). |
| `printBackground` | boolean | `true` | Include background colors and images in the PDF. |
| `headerTemplate` | object | — | Custom page header. See [HTML to PDF — headerTemplate](/docs/html-to-pdf/#headertemplate--footertemplate-object). |
| `footerTemplate` | object | — | Custom page footer. See [HTML to PDF — footerTemplate](/docs/html-to-pdf/#headertemplate--footertemplate-object). |

---

## Parameter Details

### `authentication` Object

Provide HTTP Basic Authentication credentials. The browser will send these credentials when navigating to the URL.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `username` | string | Yes | The username for Basic Auth. |
| `password` | string | Yes | The password for Basic Auth. |

```json
{
  "url": "https://staging.example.com/report",
  "authentication": {
    "username": "admin",
    "password": "s3cur3p@ss"
  }
}
```

### `cookies` Array

Set cookies in the browser before navigating to the URL. Each cookie is an object with the following fields:

| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | string | Yes | — | Cookie name. |
| `value` | string | Yes | — | Cookie value. |
| `domain` | string | No | — | Domain the cookie applies to. Defaults to the URL's domain. |
| `path` | string | No | `"/"` | URL path the cookie applies to. |
| `expires` | number | No | — | Unix timestamp (seconds) when the cookie expires. Omit for a session cookie. |
| `httpOnly` | boolean | No | `false` | Mark the cookie as HTTP-only (not accessible via JavaScript). |
| `secure` | boolean | No | `false` | Mark the cookie as secure (only sent over HTTPS). |
| `sameSite` | string | No | `"Lax"` | SameSite attribute: `"Strict"`, `"Lax"`, or `"None"`. |

```json
{
  "url": "https://app.example.com/dashboard",
  "cookies": [
    {
      "name": "session_id",
      "value": "abc123def456",
      "domain": "app.example.com",
      "path": "/",
      "secure": true,
      "httpOnly": true
    },
    {
      "name": "theme",
      "value": "dark",
      "domain": "app.example.com"
    }
  ]
}
```

> **Use case:** Cookies let you capture authenticated pages (dashboards, admin panels, user profiles) by passing the user's session cookie. This is often simpler than Basic Auth for applications with cookie-based session management.

---

## Batch Processing

Combine multiple URLs into a single multi-page PDF. Each URL is rendered as a separate section in the output document.

Pass an object with a `urls` array in the `batch` parameter:

```json
{
  "batch": {
    "urls": [
      "https://example.com/report/page-1",
      "https://example.com/report/page-2",
      "https://example.com/report/page-3"
    ]
  },
  "format": "a4",
  "printBackground": true,
  "waitUntil": "networkidle0"
}
```

> **Note:** When using `batch`, the `url` parameter is ignored. All other parameters (format, margin, viewport, etc.) apply to every URL in the batch.

### Batch Example

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v2/url/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "batch": {
      "urls": [
        "https://example.com/chapter-1",
        "https://example.com/chapter-2",
        "https://example.com/chapter-3"
      ]
    },
    "format": "letter",
    "margin": {
      "top": "1in",
      "bottom": "1in",
      "left": "1in",
      "right": "1in"
    },
    "printBackground": true,
    "filename": "full-report.pdf"
  }'
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/url/pdf", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    batch: {
      urls: [
        "https://example.com/chapter-1",
        "https://example.com/chapter-2",
        "https://example.com/chapter-3",
      ],
    },
    format: "letter",
    margin: { top: "1in", bottom: "1in", left: "1in", right: "1in" },
    printBackground: true,
    filename: "full-report.pdf",
  }),
});

const job = await response.json();
```

**Python (requests)**

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v2/url/pdf",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "batch": {
            "urls": [
                "https://example.com/chapter-1",
                "https://example.com/chapter-2",
                "https://example.com/chapter-3",
            ],
        },
        "format": "letter",
        "margin": {"top": "1in", "bottom": "1in", "left": "1in", "right": "1in"},
        "printBackground": True,
        "filename": "full-report.pdf",
    },
)

job = response.json()
```

---

## Authentication Example

Capture a page behind HTTP Basic Auth:

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v2/url/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://staging.internal.example.com/admin/report",
    "authentication": {
      "username": "report-viewer",
      "password": "s3cur3p@ss"
    },
    "format": "a4",
    "printBackground": true,
    "waitUntil": "networkidle0",
    "timeout": 60000
  }'
```

---

## Cookie Authentication Example

Capture an authenticated dashboard by injecting session cookies:

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v2/url/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://app.example.com/dashboard",
    "cookies": [
      {
        "name": "auth_token",
        "value": "eyJhbGciOiJIUzI1NiIs...",
        "domain": "app.example.com",
        "secure": true,
        "httpOnly": true,
        "sameSite": "Lax"
      }
    ],
    "format": "letter",
    "landscape": true,
    "printBackground": true,
    "waitUntil": "networkidle0",
    "delay": 2000
  }'
```

---

## Tips

- **Dynamic content:** Use `waitUntil: "networkidle0"` for SPAs and pages that fetch data via AJAX. Add a `delay` if content renders after API calls complete.
- **Authenticated pages:** Prefer `cookies` over `authentication` for modern web apps with session-based auth. Use `authentication` for sites with HTTP Basic Auth (common in staging environments).
- **Batch limits:** Batch processing is subject to your plan's timeout limits. For large batches, consider using async mode.
- **CORS and firewalls:** The URL must be publicly accessible from cloudlayer.io servers. For internal/private URLs, consider using the [HTML to PDF](/docs/html-to-pdf/) endpoint instead.
- **Mobile rendering:** Set `viewPort.isMobile` to `true` and `viewPort.width` to `375` (or similar) to capture the mobile version of a responsive site.

### Template to PDF

Source: https://cloudlayer.io/docs/template-to-pdf/

# Template to PDF

Generate a PDF from a template with dynamic data. Use predefined templates from the cloudlayer.io template gallery or provide your own custom HTML template. Templates support Nunjucks syntax for data binding.

## Endpoint

<div class="v1-only">

```
POST /v1/template/pdf
```

All requests are processed synchronously. The response body is the raw PDF file.

</div>

<div class="v2-only">

```
POST /v2/template/pdf
```

Requests default to **asynchronous** processing (`async: true`, `storage: true`). Add `"async": false` to receive the raw PDF directly. See [Sync vs. Async](/docs/api-overview/#synchronous-vs-asynchronous-requests) for details.

</div>

Supports two content types:

| Content Type | Use Case |
| --- | --- |
| `application/json` | Inline templates (base64-encoded) or predefined template IDs with JSON data. |
| `multipart/form-data` | Upload template files directly alongside JSON data. |

---

## Quick Start: Predefined Template

Use a template from the cloudlayer.io gallery by referencing its `templateId`:

<div class="v1-only">

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v1/template/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "professional-invoice",
    "data": {
      "invoiceNumber": "INV-2024-001",
      "companyName": "Acme Corp",
      "customerName": "Jane Smith",
      "items": [
        {"name": "Widget A", "quantity": 10, "price": 25.00},
        {"name": "Widget B", "quantity": 5, "price": 40.00}
      ],
      "total": 450.00
    }
  }' \
  --output invoice.pdf
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v1/template/pdf", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    templateId: "professional-invoice",
    data: {
      invoiceNumber: "INV-2024-001",
      companyName: "Acme Corp",
      customerName: "Jane Smith",
      items: [
        { name: "Widget A", quantity: 10, price: 25.0 },
        { name: "Widget B", quantity: 5, price: 40.0 },
      ],
      total: 450.0,
    },
  }),
});

const pdf = await response.arrayBuffer();
```

**Python (requests)**

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v1/template/pdf",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "templateId": "professional-invoice",
        "data": {
            "invoiceNumber": "INV-2024-001",
            "companyName": "Acme Corp",
            "customerName": "Jane Smith",
            "items": [
                {"name": "Widget A", "quantity": 10, "price": 25.00},
                {"name": "Widget B", "quantity": 5, "price": 40.00},
            ],
            "total": 450.00,
        },
    },
)

with open("invoice.pdf", "wb") as f:
    f.write(response.content)
```

</div>

<div class="v2-only">

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v2/template/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "professional-invoice",
    "data": {
      "invoiceNumber": "INV-2024-001",
      "companyName": "Acme Corp",
      "customerName": "Jane Smith",
      "items": [
        {"name": "Widget A", "quantity": 10, "price": 25.00},
        {"name": "Widget B", "quantity": 5, "price": 40.00}
      ],
      "total": 450.00
    }
  }'
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/template/pdf", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    templateId: "professional-invoice",
    data: {
      invoiceNumber: "INV-2024-001",
      companyName: "Acme Corp",
      customerName: "Jane Smith",
      items: [
        { name: "Widget A", quantity: 10, price: 25.0 },
        { name: "Widget B", quantity: 5, price: 40.0 },
      ],
      total: 450.0,
    },
  }),
});

const job = await response.json();
```

**Python (requests)**

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v2/template/pdf",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "templateId": "professional-invoice",
        "data": {
            "invoiceNumber": "INV-2024-001",
            "companyName": "Acme Corp",
            "customerName": "Jane Smith",
            "items": [
                {"name": "Widget A", "quantity": 10, "price": 25.00},
                {"name": "Widget B", "quantity": 5, "price": 40.00},
            ],
            "total": 450.00,
        },
    },
)

job = response.json()
```

> **Tip:** v2 always returns JSON. Use the job `id` to poll the [Jobs](/docs/jobs/) endpoint or configure [Webhooks](/docs/webhooks/) for completion notifications. The generated file is accessible via the [Assets](/docs/assets/) endpoint. Add `"async": false` to wait for processing to complete — the response is still JSON but includes the completed job details.

</div>

---

## Quick Start: Custom Template

Provide your own HTML template as a base64-encoded string:

**cURL**

```bash
TEMPLATE=$(cat <<'HTML'
<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: Arial, sans-serif; padding: 40px; }
    h1 { color: #2563eb; }
    .detail { margin: 8px 0; }
    .label { font-weight: bold; color: #666; }
  </style>
</head>
<body>
  <h1>{{title}}</h1>
  <div class="detail"><span class="label">Date:</span> {{date}}</div>
  <div class="detail"><span class="label">Prepared for:</span> {{recipient}}</div>
  <div style="margin-top: 20px;">{{ content | safe }}</div>
</body>
</html>
HTML
)

TEMPLATE_BASE64=$(echo "$TEMPLATE" | base64 -w 0)

curl -X POST "https://api.cloudlayer.io/v2/template/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d "{
    \"template\": \"$TEMPLATE_BASE64\",
    \"data\": {
      \"title\": \"Monthly Report\",
      \"date\": \"January 2024\",
      \"recipient\": \"Jane Smith\",
      \"content\": \"<p>This is the report content with <strong>HTML</strong> support.</p>\"
    }
  }"
```

**JavaScript (fetch)**

```javascript
const template = `<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: Arial, sans-serif; padding: 40px; }
    h1 { color: #2563eb; }
    .detail { margin: 8px 0; }
    .label { font-weight: bold; color: #666; }
  </style>
</head>
<body>
  <h1>{{title}}</h1>
  <div class="detail"><span class="label">Date:</span> {{date}}</div>
  <div class="detail"><span class="label">Prepared for:</span> {{recipient}}</div>
  <div style="margin-top: 20px;">{{ content | safe }}</div>
</body>
</html>`;

const response = await fetch("https://api.cloudlayer.io/v2/template/pdf", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    template: btoa(template),
    data: {
      title: "Monthly Report",
      date: "January 2024",
      recipient: "Jane Smith",
      content:
        "<p>This is the report content with <strong>HTML</strong> support.</p>",
    },
  }),
});

const job = await response.json();
```

**Python (requests)**

```python
import base64
import requests

template = """<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: Arial, sans-serif; padding: 40px; }
    h1 { color: #2563eb; }
  </style>
</head>
<body>
  <h1>{{title}}</h1>
  <p>Prepared for: {{recipient}}</p>
  <div>{{ content | safe }}</div>
</body>
</html>"""

response = requests.post(
    "https://api.cloudlayer.io/v2/template/pdf",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "template": base64.b64encode(template.encode()).decode(),
        "data": {
            "title": "Monthly Report",
            "recipient": "Jane Smith",
            "content": "<p>Report content here.</p>",
        },
    },
)

job = response.json()
```

---

## Parameters

### Template Parameters

You must provide either `templateId` (for a predefined template) or `template` (for a custom template), but not both.

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `templateId` | string | Conditional | — | The ID of a predefined template from the cloudlayer.io template gallery. Browse available templates in the [dashboard](https://beta-app.cloudlayer.io). |
| `template` | string | Conditional | — | A custom HTML template as a base64-encoded string. Supports Nunjucks syntax for data binding. |
| `data` | object | No | `{}` | Key-value pairs of data to inject into the template. Keys correspond to Nunjucks placeholders in the template (e.g., `{{name}}` is populated by `data.name`). |

### Base Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `autoScroll` | boolean | `false` | Scroll the page to trigger lazy-loaded content before capture. |
| `delay` | number | `0` | Wait time in milliseconds after the page loads before generating. |
| `filename` | string | — | Set the `Content-Disposition` filename on the response. |
| `generatePreview` | object | — | Generate a thumbnail preview image. See [HTML to PDF — generatePreview](/docs/html-to-pdf/#generatepreview-object). |
| `height` | string | — | Override page height with a CSS value. |
| `width` | string | — | Override page width with a CSS value. |
| `inline` | boolean | `false` | Display the PDF inline in the browser instead of downloading. |
| `landscape` | boolean | `false` | Generate in landscape orientation. |
| `preferCSSPageSize` | boolean | `false` | Use CSS `@page` size instead of `format`. |
| `projectId` | string | — | Associate with a project for dashboard organization. |
| `scale` | number | `1` | Page rendering scale (`0.1` to `2`). |
| `timeout` | number | `30000` | Maximum page load time in milliseconds. |
| `timeZone` | string | — | Browser timezone (IANA name). |
| `viewPort` | object | — | Browser viewport configuration. See [HTML to PDF — viewPort](/docs/html-to-pdf/#viewport-object). |
| `waitForSelector` | object | — | Wait for a CSS selector before generating. See [HTML to PDF — waitForSelector](/docs/html-to-pdf/#waitforselector-object). |
| `waitUntil` | string | `"networkidle2"` | Navigation completion strategy. |

### PDF Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `pageRanges` | string | — | Page ranges to include (e.g., `"1-5"`, `"1,3,5"`). |
| `format` | string | `"letter"` | Page size: `"letter"`, `"legal"`, `"tabloid"`, `"ledger"`, `"a0"` through `"a6"`. |
| `margin` | object | — | Page margins. See [HTML to PDF — margin](/docs/html-to-pdf/#margin-object). |
| `printBackground` | boolean | `true` | Include background colors and images. |
| `headerTemplate` | object | — | Custom page header. See [HTML to PDF — headerTemplate](/docs/html-to-pdf/#headertemplate--footertemplate-object). |
| `footerTemplate` | object | — | Custom page footer. See [HTML to PDF — footerTemplate](/docs/html-to-pdf/#headertemplate--footertemplate-object). |

---

## Multipart Uploads

For uploading template files directly (instead of base64-encoding), use `multipart/form-data`:

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v2/template/pdf" \
  -H "X-API-Key: your-api-key-here" \
  -F "template=@./my-template.html" \
  -F 'data={"invoiceNumber":"INV-001","total":450.00}'
```

**JavaScript (fetch)**

```javascript
const formData = new FormData();
formData.append("template", new Blob([templateHtml], { type: "text/html" }), "template.html");
formData.append("data", JSON.stringify({
  invoiceNumber: "INV-001",
  total: 450.00,
}));

const response = await fetch("https://api.cloudlayer.io/v2/template/pdf", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    // Do not set Content-Type — fetch sets it automatically with the boundary
  },
  body: formData,
});

const job = await response.json();
```

**Python (requests)**

```python
import json
import requests

with open("my-template.html", "rb") as f:
    template_file = f.read()

response = requests.post(
    "https://api.cloudlayer.io/v2/template/pdf",
    headers={"X-API-Key": "your-api-key-here"},
    files={"template": ("template.html", template_file, "text/html")},
    data={"data": json.dumps({"invoiceNumber": "INV-001", "total": 450.00})},
)

job = response.json()
```

---

## Template Syntax

Templates use [Nunjucks](https://mozilla.github.io/nunjucks/) syntax for data binding. Here are the most common patterns:

### Variable Output

```html
<!-- Escaped output (HTML entities are escaped) -->
<p>Hello, {{name}}!</p>

<!-- Unescaped output (raw HTML is rendered) -->
<div>{{ htmlContent | safe }}</div>
```

### Conditionals

```html
{% if isPaid %}
  <span class="badge paid">Paid</span>
{% else %}
  <span class="badge unpaid">Unpaid</span>
{% endif %}
```

### Loops

```html
<table>
  <thead>
    <tr><th>Item</th><th>Qty</th><th>Price</th></tr>
  </thead>
  <tbody>
    {% for item in items %}
    <tr>
      <td>{{item.name}}</td>
      <td>{{item.quantity}}</td>
      <td>${{item.price}}</td>
    </tr>
    {% endfor %}
  </tbody>
</table>
```

### Nested Data

```html
<div>
  <p>{{company.name}}</p>
  <p>{{company.address.street}}</p>
  <p>{{company.address.city}}, {{company.address.state}} {{company.address.zip}}</p>
</div>
```

---

## Full Example: Invoice with Custom Template

**JavaScript (fetch)**

```javascript
const template = `<!DOCTYPE html>
<html>
<head>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: 'Helvetica Neue', Arial, sans-serif; color: #333; padding: 60px; }
    .header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; }
    .company-name { font-size: 28px; font-weight: 700; color: #1a1a1a; }
    .invoice-meta { text-align: right; }
    .invoice-number { font-size: 20px; font-weight: 600; color: #2563eb; }
    .customer { margin-bottom: 30px; padding: 20px; background: #f8f9fa; border-radius: 8px; }
    table { width: 100%; border-collapse: collapse; margin: 20px 0; }
    th { background: #2563eb; color: white; padding: 12px 16px; text-align: left; }
    td { padding: 12px 16px; border-bottom: 1px solid #eee; }
    tr:nth-child(even) { background: #f8f9fa; }
    .total-row { font-size: 18px; font-weight: 700; background: #f0f4ff !important; }
    .footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; font-size: 12px; color: #999; }
  </style>
</head>
<body>
  <div class="header">
    <div class="company-name">{{companyName}}</div>
    <div class="invoice-meta">
      <div class="invoice-number">{{invoiceNumber}}</div>
      <div>Date: {{date}}</div>
      <div>Due: {{dueDate}}</div>
    </div>
  </div>
  <div class="customer">
    <strong>Bill To:</strong><br>
    {{customerName}}<br>
    {{customerEmail}}
  </div>
  <table>
    <thead>
      <tr><th>Item</th><th>Quantity</th><th>Unit Price</th><th>Total</th></tr>
    </thead>
    <tbody>
      {% for item in items %}
      <tr>
        <td>{{item.name}}</td>
        <td>{{item.quantity}}</td>
        <td>\${{item.price}}</td>
        <td>\${{item.total}}</td>
      </tr>
      {% endfor %}
      <tr class="total-row">
        <td colspan="3" style="text-align: right;">Total</td>
        <td>\${{grandTotal}}</td>
      </tr>
    </tbody>
  </table>
  {% if notes %}
  <div style="margin-top: 20px; padding: 15px; background: #fffbeb; border-radius: 8px;">
    <strong>Notes:</strong> {{notes}}
  </div>
  {% endif %}
  <div class="footer">
    Thank you for your business. Payment is due within 30 days.
  </div>
</body>
</html>`;

const response = await fetch("https://api.cloudlayer.io/v2/template/pdf", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    template: btoa(template),
    data: {
      companyName: "Acme Corp",
      invoiceNumber: "INV-2024-042",
      date: "2024-01-15",
      dueDate: "2024-02-14",
      customerName: "Jane Smith",
      customerEmail: "jane@example.com",
      items: [
        { name: "Web Development", quantity: 40, price: "150.00", total: "6,000.00" },
        { name: "Design Services", quantity: 20, price: "125.00", total: "2,500.00" },
        { name: "Hosting (Annual)", quantity: 1, price: "299.00", total: "299.00" },
      ],
      grandTotal: "8,799.00",
      notes: "Please include the invoice number in your payment reference.",
    },
    format: "letter",
    margin: { top: "0.5in", bottom: "0.5in", left: "0.5in", right: "0.5in" },
    printBackground: true,
    filename: "invoice-2024-042.pdf",
  }),
});

const job = await response.json();
```

---

## Tips

- **Nunjucks `| safe` filter:** Use `{{ variable | safe }}` when your data contains HTML that should be rendered, not escaped. Use `{{variable}}` for plain text to prevent XSS.
- **External resources:** Custom templates can reference external CSS, fonts, and images via URLs. Make sure they are publicly accessible.
- **Template reuse:** If you use the same template repeatedly, consider adding it to the template gallery via the dashboard and referencing it by `templateId`. This reduces payload size and simplifies your API calls.
- **Debugging templates:** Test your Nunjucks template locally using the [Nunjucks documentation and playground](https://mozilla.github.io/nunjucks/) before sending it to the API.
- **Multipart vs. JSON:** Use multipart uploads when your template file is large or when you want to avoid base64 encoding overhead.

### HTML to Image

Source: https://cloudlayer.io/docs/html-to-image/

# HTML to Image

Generate an image (PNG, JPG, or WebP) from raw HTML content. The HTML is sent as a base64-encoded string in the request body.

## Endpoint

<div class="v1-only">

```
POST /v1/html/image
```

All requests are processed synchronously. The response body is the raw image file.

</div>

<div class="v2-only">

```
POST /v2/html/image
```

Requests default to **asynchronous** processing (`async: true`, `storage: true`). Add `"async": false` to receive the raw image directly. See [Sync vs. Async](/docs/api-overview/#synchronous-vs-asynchronous-requests) for details.

</div>

---

## Quick Start

<div class="v1-only">

**cURL**

```bash
HTML_BASE64=$(echo '<html><body style="padding:40px;font-family:Arial"><h1 style="color:#2563eb">Hello World</h1><p>Generated by cloudlayer.io</p></body></html>' | base64)

curl -X POST "https://api.cloudlayer.io/v1/html/image" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d "{\"html\": \"$HTML_BASE64\"}" \
  --output result.png
```

**JavaScript (fetch)**

```javascript
const html = `<html>
  <body style="padding: 40px; font-family: Arial;">
    <h1 style="color: #2563eb;">Hello World</h1>
    <p>Generated by cloudlayer.io</p>
  </body>
</html>`;

const response = await fetch("https://api.cloudlayer.io/v1/html/image", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html: btoa(html),
  }),
});

const image = await response.arrayBuffer();
```

**Python (requests)**

```python
import base64
import requests

html = """<html>
  <body style="padding: 40px; font-family: Arial;">
    <h1 style="color: #2563eb;">Hello World</h1>
    <p>Generated by cloudlayer.io</p>
  </body>
</html>"""

response = requests.post(
    "https://api.cloudlayer.io/v1/html/image",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "html": base64.b64encode(html.encode()).decode(),
    },
)

with open("result.png", "wb") as f:
    f.write(response.content)
```

</div>

<div class="v2-only">

**cURL**

```bash
HTML_BASE64=$(echo '<html><body style="padding:40px;font-family:Arial"><h1 style="color:#2563eb">Hello World</h1><p>Generated by cloudlayer.io</p></body></html>' | base64)

curl -X POST "https://api.cloudlayer.io/v2/html/image" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d "{\"html\": \"$HTML_BASE64\"}"
```

Response:

```json
{
  "id": "abc123",
  "status": "pending"
}
```

**JavaScript (fetch)**

```javascript
const html = `<html>
  <body style="padding: 40px; font-family: Arial;">
    <h1 style="color: #2563eb;">Hello World</h1>
    <p>Generated by cloudlayer.io</p>
  </body>
</html>`;

const response = await fetch("https://api.cloudlayer.io/v2/html/image", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html: btoa(html),
  }),
});

const { id, status } = await response.json();
// Poll job status or use webhooks to know when the image is ready
```

**Python (requests)**

```python
import base64
import requests

html = """<html>
  <body style="padding: 40px; font-family: Arial;">
    <h1 style="color: #2563eb;">Hello World</h1>
    <p>Generated by cloudlayer.io</p>
  </body>
</html>"""

response = requests.post(
    "https://api.cloudlayer.io/v2/html/image",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "html": base64.b64encode(html.encode()).decode(),
    },
)

job = response.json()
# Poll job status or use webhooks to know when the image is ready
```

> **Tip:** v2 always returns JSON. Use the job `id` to poll the [Jobs](/docs/jobs/) endpoint or configure [Webhooks](/docs/webhooks/) for completion notifications. The generated file is accessible via the [Assets](/docs/assets/) endpoint. Add `"async": false` to wait for processing to complete — the response is still JSON but includes the completed job details.

</div>

---

## Parameters

### Required

| Parameter | Type | Description |
| --- | --- | --- |
| `html` | string | **Required.** Base64-encoded HTML content. The HTML can include inline CSS, `<style>` tags, and `<link>` references to external stylesheets. |

### Image Parameters

These parameters control the image output format and processing.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `imageType` | string | `"png"` | Output image format. Options: `"png"`, `"jpg"`, `"jpeg"`, `"webp"`, or `"svg"`. |
| `quality` | number | — | Image quality (1-100). Only applies to `jpg`, `jpeg`, and `webp` formats. |
| `transparent` | boolean | `false` | Render with a transparent background. Only works with `"png"` and `"webp"` formats. Your HTML must not set a background color on the `<html>` or `<body>` elements. |
| `trim` | boolean | `false` | Trim whitespace from all edges of the resulting image. Useful for generating tightly-cropped screenshots of specific content. |

### Base Parameters

These parameters control page loading behavior, viewport configuration, and general output settings.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `autoScroll` | boolean | `false` | Scroll the page to the bottom before capturing. Triggers lazy-loaded content. |
| `delay` | number | `0` | Wait time in milliseconds after the page loads before capturing the image. |
| `filename` | string | — | Set the `Content-Disposition` filename on the response. Include the appropriate extension (e.g., `"screenshot.png"`). |
| `height` | string | — | Set the capture area height. Accepts CSS units (e.g., `"600px"`, `"100vh"`). |
| `width` | string | — | Set the capture area width. Accepts CSS units (e.g., `"800px"`, `"100vw"`). |
| `inline` | boolean | `false` | Display the image inline in the browser instead of triggering a download. |
| `landscape` | boolean | `false` | Render in landscape orientation. |
| `preferCSSPageSize` | boolean | `false` | Use CSS `@page` size for dimensions. |
| `projectId` | string | — | Associate with a project for dashboard organization. |
| `scale` | number | `1` | Page rendering scale (`0.1` to `2`). Higher values produce higher-resolution images. |
| `timeout` | number | `30000` | Maximum time in milliseconds to wait for the page to load. |
| `timeZone` | string | — | Browser timezone (IANA name, e.g., `"America/New_York"`). |
| `viewPort` | object | — | Browser viewport configuration. See [HTML to PDF — viewPort](/docs/html-to-pdf/#viewport-object). |
| `waitForSelector` | object | — | Wait for a CSS selector before capturing. See [HTML to PDF — waitForSelector](/docs/html-to-pdf/#waitforselector-object). |
| `waitUntil` | string | — | Navigation completion strategy: `"load"`, `"domcontentloaded"`, `"networkidle0"`, `"networkidle2"`. |

---

## Examples

### Generate a WebP Image

```bash
curl -X POST "https://api.cloudlayer.io/v2/html/image" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d "{
    \"html\": \"$(echo '<h1>Hello</h1>' | base64)\",
    \"imageType\": \"webp\"
  }"
```

### Transparent Background (PNG)

```json
{
  "html": "PGRpdiBzdHlsZT0icGFkZGluZzogMjBweDsgZm9udC1zaXplOiAyNHB4OyI+VHJhbnNwYXJlbnQgQmFja2dyb3VuZDwvZGl2Pg==",
  "imageType": "png",
  "transparent": true,
  "trim": true
}
```

> **Important:** When using `transparent: true`, make sure your HTML does not set `background` or `background-color` on the `<html>` or `<body>` elements. Any background color will override transparency.

### High-Resolution Retina Image

Combine `scale` and `viewPort.deviceScaleFactor` for crisp, high-DPI images:

```json
{
  "html": "...",
  "imageType": "png",
  "scale": 2,
  "viewPort": {
    "width": 1200,
    "height": 630,
    "deviceScaleFactor": 2
  }
}
```

### Social Media Card (OG Image)

Generate Open Graph images for social media sharing:

**JavaScript (fetch)**

```javascript
const html = `<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0; padding: 0;
      width: 1200px; height: 630px;
      display: flex; align-items: center; justify-content: center;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      font-family: 'Arial', sans-serif;
      color: white;
    }
    .card {
      text-align: center;
      padding: 60px;
    }
    h1 { font-size: 56px; margin-bottom: 16px; }
    p { font-size: 24px; opacity: 0.9; }
  </style>
</head>
<body>
  <div class="card">
    <h1>${title}</h1>
    <p>${subtitle}</p>
  </div>
</body>
</html>`;

const response = await fetch("https://api.cloudlayer.io/v2/html/image", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html: btoa(html),
    imageType: "png",
    viewPort: {
      width: 1200,
      height: 630,
    },
    trim: false,
  }),
});

const job = await response.json();
```

### Trimmed Element Screenshot

Capture just the content with no extra whitespace:

```json
{
  "html": "...",
  "imageType": "png",
  "trim": true,
  "viewPort": {
    "width": 800,
    "height": 600
  }
}
```

---

## Tips

- **Image dimensions:** The output image size is determined by the viewport and content dimensions. Use `viewPort` to control the rendering area and `width`/`height` to set explicit capture dimensions.
- **Retina quality:** Set `scale: 2` or `viewPort.deviceScaleFactor: 2` for crisp images on high-DPI displays. This doubles the pixel dimensions.
- **File size:** Use `"jpg"` for photographs and complex images (smaller file size). Use `"png"` for graphics with text, logos, or transparency. Use `"webp"` for the best compression-to-quality ratio.
- **Transparency:** Only works with `"png"` and `"webp"`. Remove all background colors from your HTML/CSS.
- **Dynamic content:** Use `waitUntil: "networkidle0"` and `delay` for pages that load charts, maps, or other dynamic content.

### URL to Image

Source: https://cloudlayer.io/docs/url-to-image/

# URL to Image

Capture a screenshot of any web page as a PNG, JPG, or WebP image. Supports both simple GET requests and full-featured POST requests with authentication and cookies.

## Endpoints

The **GET** endpoint is a convenience method for simple screenshots. The
**POST** endpoint supports the full range of parameters.

<div class="v1-only">

```
GET  /v1/url/image
POST /v1/url/image
```

All v1 requests are processed synchronously. The response body is the raw image file.

</div>

<div class="v2-only">

```
GET  /v2/url/image
POST /v2/url/image
```

v2 requests default to **asynchronous** processing (`async: true`, `storage: true`). Add `"async": false` to receive the raw image directly. See [Sync vs. Async](/docs/api-overview/#synchronous-vs-asynchronous-requests) for details.

</div>

---

## Quick Start (GET)

Capture a web page screenshot with a single GET request:

<div class="v1-only">

**cURL**

```bash
curl -X GET "https://api.cloudlayer.io/v1/url/image?url=https://example.com" \
  -H "X-API-Key: your-api-key-here" \
  --output screenshot.png
```

**JavaScript (fetch)**

```javascript
const url = encodeURIComponent("https://example.com");

const response = await fetch(
  `https://api.cloudlayer.io/v1/url/image?url=${url}`,
  {
    headers: {
      "X-API-Key": "your-api-key-here",
    },
  }
);

const image = await response.arrayBuffer();
```

**Python (requests)**

```python
import requests

response = requests.get(
    "https://api.cloudlayer.io/v1/url/image",
    params={"url": "https://example.com"},
    headers={"X-API-Key": "your-api-key-here"},
)

with open("screenshot.png", "wb") as f:
    f.write(response.content)
```

</div>

<div class="v2-only">

**cURL**

```bash
curl -X GET "https://api.cloudlayer.io/v2/url/image?url=https://example.com" \
  -H "X-API-Key: your-api-key-here"
```

Response:

```json
{
  "id": "abc123",
  "status": "pending"
}
```

**JavaScript (fetch)**

```javascript
const url = encodeURIComponent("https://example.com");

const response = await fetch(
  `https://api.cloudlayer.io/v2/url/image?url=${url}`,
  {
    headers: {
      "X-API-Key": "your-api-key-here",
    },
  }
);

const job = await response.json();
```

**Python (requests)**

```python
import requests

response = requests.get(
    "https://api.cloudlayer.io/v2/url/image",
    params={"url": "https://example.com"},
    headers={"X-API-Key": "your-api-key-here"},
)

job = response.json()
```

</div>

---

## Quick Start (POST)

Use POST for full control over the screenshot:

<div class="v1-only">

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v1/url/image" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "imageType": "webp",
    "viewPort": {
      "width": 1920,
      "height": 1080,
      "deviceScaleFactor": 2
    },
    "waitUntil": "networkidle0"
  }' \
  --output screenshot.webp
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v1/url/image", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com",
    imageType: "webp",
    viewPort: {
      width: 1920,
      height: 1080,
      deviceScaleFactor: 2,
    },
    waitUntil: "networkidle0",
  }),
});

const image = await response.arrayBuffer();
```

**Python (requests)**

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v1/url/image",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "url": "https://example.com",
        "imageType": "webp",
        "viewPort": {
            "width": 1920,
            "height": 1080,
            "deviceScaleFactor": 2,
        },
        "waitUntil": "networkidle0",
    },
)

with open("screenshot.webp", "wb") as f:
    f.write(response.content)
```

</div>

<div class="v2-only">

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v2/url/image" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "imageType": "webp",
    "viewPort": {
      "width": 1920,
      "height": 1080,
      "deviceScaleFactor": 2
    },
    "waitUntil": "networkidle0"
  }'
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/url/image", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com",
    imageType: "webp",
    viewPort: {
      width: 1920,
      height: 1080,
      deviceScaleFactor: 2,
    },
    waitUntil: "networkidle0",
  }),
});

const job = await response.json();
```

**Python (requests)**

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v2/url/image",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "url": "https://example.com",
        "imageType": "webp",
        "viewPort": {
            "width": 1920,
            "height": 1080,
            "deviceScaleFactor": 2,
        },
        "waitUntil": "networkidle0",
    },
)

job = response.json()
```

> **Tip:** v2 always returns JSON. Use the job `id` to poll the [Jobs](/docs/jobs/) endpoint or configure [Webhooks](/docs/webhooks/) for completion notifications. The generated file is accessible via the [Assets](/docs/assets/) endpoint. Add `"async": false` to wait for processing to complete — the response is still JSON but includes the completed job details.

</div>

---

## GET Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `url` | string | Yes | — | The URL of the web page to capture. Must be URL-encoded. |
| `timeout` | number | No | `30000` | Maximum time in milliseconds to wait for the page to load. |

---

## POST Parameters

### URL Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `url` | string | Yes | — | The URL of the web page to capture. |
| `authentication` | object | No | — | Credentials for HTTP Basic Authentication. See [URL to PDF — authentication](/docs/url-to-pdf/#authentication-object). |
| `cookies` | array | No | — | Cookies to set before navigating. See [URL to PDF — cookies](/docs/url-to-pdf/#cookies-array). |

### Image Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `imageType` | string | `"png"` | Output image format: `"png"`, `"jpg"`, `"jpeg"`, `"webp"`, or `"svg"`. |
| `quality` | number | — | Image quality (1-100). Only applies to `jpg`, `jpeg`, and `webp` formats. |
| `transparent` | boolean | `false` | Render with a transparent background. Only works with `"png"` and `"webp"`. The target page must not set a background color. |
| `trim` | boolean | `false` | Trim whitespace from all edges of the resulting image. |

### Base Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `autoScroll` | boolean | `false` | Scroll the page to the bottom before capturing. Triggers lazy-loaded content and infinite scroll. |
| `delay` | number | `0` | Wait time in milliseconds after the page loads before capturing. |
| `filename` | string | — | Set the `Content-Disposition` filename on the response. |
| `height` | string | — | Set the capture area height with CSS units. |
| `width` | string | — | Set the capture area width with CSS units. |
| `inline` | boolean | `false` | Display the image inline in the browser instead of downloading. |
| `landscape` | boolean | `false` | Render in landscape orientation. |
| `preferCSSPageSize` | boolean | `false` | Use CSS `@page` size for dimensions. |
| `projectId` | string | — | Associate with a project for dashboard organization. |
| `scale` | number | `1` | Page rendering scale (`0.1` to `2`). |
| `timeout` | number | `30000` | Maximum time in milliseconds to wait for page load. |
| `timeZone` | string | — | Browser timezone (IANA name). |
| `viewPort` | object | — | Browser viewport configuration. See [HTML to PDF — viewPort](/docs/html-to-pdf/#viewport-object). |
| `waitForSelector` | object | — | Wait for a CSS selector before capturing. See [HTML to PDF — waitForSelector](/docs/html-to-pdf/#waitforselector-object). |
| `waitUntil` | string | `"networkidle2"` | Navigation completion strategy: `"load"`, `"domcontentloaded"`, `"networkidle0"`, `"networkidle2"`. |

---

## Examples

### Mobile Screenshot

Capture the mobile version of a responsive website:

```json
{
  "url": "https://example.com",
  "imageType": "png",
  "viewPort": {
    "width": 375,
    "height": 812,
    "deviceScaleFactor": 3,
    "isMobile": true,
    "hasTouch": true
  },
  "waitUntil": "networkidle0"
}
```

### Full-Page Screenshot with Auto-Scroll

Capture the entire page, including below-the-fold content and lazy-loaded images:

```json
{
  "url": "https://example.com/blog/long-article",
  "imageType": "jpg",
  "autoScroll": true,
  "waitUntil": "networkidle0",
  "delay": 1000
}
```

### Authenticated Dashboard Screenshot

Capture a dashboard behind cookie-based authentication:

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v2/url/image" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://app.example.com/dashboard",
    "cookies": [
      {
        "name": "session_token",
        "value": "abc123...",
        "domain": "app.example.com",
        "secure": true,
        "httpOnly": true
      }
    ],
    "imageType": "png",
    "viewPort": {
      "width": 1440,
      "height": 900
    },
    "waitUntil": "networkidle0",
    "delay": 2000,
    "filename": "dashboard-screenshot.png"
  }'
```

### High-Resolution Screenshot for Print

Generate a high-DPI screenshot suitable for print materials:

```json
{
  "url": "https://example.com",
  "imageType": "png",
  "scale": 2,
  "viewPort": {
    "width": 1200,
    "height": 800,
    "deviceScaleFactor": 2
  },
  "waitUntil": "networkidle0"
}
```

### Trimmed Element Capture

Capture just the rendered content with whitespace removed:

```json
{
  "url": "https://example.com/widget",
  "imageType": "png",
  "trim": true,
  "transparent": true,
  "viewPort": {
    "width": 600,
    "height": 400
  }
}
```

---

## Tips

- **Full-page screenshots:** Use `autoScroll: true` to load all lazy content, then the renderer captures the full scrollable area.
- **Dynamic content:** For SPAs and pages with charts/graphs, use `waitUntil: "networkidle0"` with a `delay` of 1-3 seconds to ensure all data has loaded and rendered.
- **File format choice:** Use PNG for screenshots with text and sharp edges. Use JPG for photograph-heavy pages (smaller file size). Use WebP for the best balance of quality and compression.
- **Viewport matters:** The default viewport is 1440x900. Set the viewport explicitly to control exactly what is captured, especially for responsive sites.
- **Transparent screenshots:** Only works with PNG and WebP. The target page must have no background color set on `<html>` or `<body>`. Many sites set a white background, so transparency works best with your own HTML content via the [HTML to Image](/docs/html-to-image/) endpoint.

### Template to Image

Source: https://cloudlayer.io/docs/template-to-image/

# Template to Image

Generate an image (PNG, JPG, or WebP) from a template with dynamic data. Use predefined templates from the cloudlayer.io gallery or provide your own custom HTML template with Nunjucks syntax.

## Endpoint

<div class="v1-only">

```
POST /v1/template/image
```

All requests are processed synchronously. The response body is the raw image file.

</div>

<div class="v2-only">

```
POST /v2/template/image
```

Requests default to **asynchronous** processing (`async: true`, `storage: true`). Add `"async": false` to receive the raw image directly. See [Sync vs. Async](/docs/api-overview/#synchronous-vs-asynchronous-requests) for details.

</div>

Supports two content types:

| Content Type | Use Case |
| --- | --- |
| `application/json` | Inline templates (base64-encoded) or predefined template IDs with JSON data. |
| `multipart/form-data` | Upload template files directly alongside JSON data. |

---

## Quick Start: Predefined Template

<div class="v1-only">

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v1/template/image" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "social-card",
    "data": {
      "title": "Introducing Our New API",
      "subtitle": "Generate images programmatically",
      "author": "cloudlayer.io"
    },
    "imageType": "png"
  }' \
  --output social-card.png
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v1/template/image", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    templateId: "social-card",
    data: {
      title: "Introducing Our New API",
      subtitle: "Generate images programmatically",
      author: "cloudlayer.io",
    },
    imageType: "png",
  }),
});

const image = await response.arrayBuffer();
```

**Python (requests)**

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v1/template/image",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "templateId": "social-card",
        "data": {
            "title": "Introducing Our New API",
            "subtitle": "Generate images programmatically",
            "author": "cloudlayer.io",
        },
        "imageType": "png",
    },
)

with open("social-card.png", "wb") as f:
    f.write(response.content)
```

</div>

<div class="v2-only">

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v2/template/image" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "social-card",
    "data": {
      "title": "Introducing Our New API",
      "subtitle": "Generate images programmatically",
      "author": "cloudlayer.io"
    },
    "imageType": "png"
  }'
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/template/image", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    templateId: "social-card",
    data: {
      title: "Introducing Our New API",
      subtitle: "Generate images programmatically",
      author: "cloudlayer.io",
    },
    imageType: "png",
  }),
});

const job = await response.json();
```

**Python (requests)**

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v2/template/image",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "templateId": "social-card",
        "data": {
            "title": "Introducing Our New API",
            "subtitle": "Generate images programmatically",
            "author": "cloudlayer.io",
        },
        "imageType": "png",
    },
)

job = response.json()
```

> **Tip:** v2 always returns JSON. Use the job `id` to poll the [Jobs](/docs/jobs/) endpoint or configure [Webhooks](/docs/webhooks/) for completion notifications. The generated file is accessible via the [Assets](/docs/assets/) endpoint. Add `"async": false` to wait for processing to complete — the response is still JSON but includes the completed job details.

</div>

---

## Quick Start: Custom Template

**cURL**

```bash
TEMPLATE=$(cat <<'HTML'
<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0; padding: 0;
      width: 1200px; height: 630px;
      display: flex; align-items: center; justify-content: center;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      font-family: Arial, sans-serif;
      color: white;
    }
    .card { text-align: center; padding: 60px; }
    h1 { font-size: 52px; margin-bottom: 12px; }
    p { font-size: 24px; opacity: 0.9; }
  </style>
</head>
<body>
  <div class="card">
    <h1>{{title}}</h1>
    <p>{{description}}</p>
  </div>
</body>
</html>
HTML
)

TEMPLATE_BASE64=$(echo "$TEMPLATE" | base64 -w 0)

curl -X POST "https://api.cloudlayer.io/v2/template/image" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d "{
    \"template\": \"$TEMPLATE_BASE64\",
    \"data\": {
      \"title\": \"Hello World\",
      \"description\": \"Generated with cloudlayer.io\"
    },
    \"imageType\": \"png\"
  }"
```

**JavaScript (fetch)**

```javascript
const template = `<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0; padding: 0;
      width: 1200px; height: 630px;
      display: flex; align-items: center; justify-content: center;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      font-family: Arial, sans-serif;
      color: white;
    }
    .card { text-align: center; padding: 60px; }
    h1 { font-size: 52px; margin-bottom: 12px; }
    p { font-size: 24px; opacity: 0.9; }
  </style>
</head>
<body>
  <div class="card">
    <h1>{{title}}</h1>
    <p>{{description}}</p>
  </div>
</body>
</html>`;

const response = await fetch("https://api.cloudlayer.io/v2/template/image", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    template: btoa(template),
    data: {
      title: "Hello World",
      description: "Generated with cloudlayer.io",
    },
    imageType: "png",
  }),
});

const job = await response.json();
```

**Python (requests)**

```python
import base64
import requests

template = """<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0; padding: 0;
      width: 1200px; height: 630px;
      display: flex; align-items: center; justify-content: center;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      font-family: Arial, sans-serif;
      color: white;
    }
    .card { text-align: center; padding: 60px; }
    h1 { font-size: 52px; margin-bottom: 12px; }
    p { font-size: 24px; opacity: 0.9; }
  </style>
</head>
<body>
  <div class="card">
    <h1>{{title}}</h1>
    <p>{{description}}</p>
  </div>
</body>
</html>"""

response = requests.post(
    "https://api.cloudlayer.io/v2/template/image",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "template": base64.b64encode(template.encode()).decode(),
        "data": {
            "title": "Hello World",
            "description": "Generated with cloudlayer.io",
        },
        "imageType": "png",
    },
)

job = response.json()
```

---

## Parameters

### Template Parameters

You must provide either `templateId` or `template`, but not both.

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `templateId` | string | Conditional | — | ID of a predefined template from the cloudlayer.io template gallery. |
| `template` | string | Conditional | — | Custom HTML template as a base64-encoded string. Supports Nunjucks syntax. |
| `data` | object | No | `{}` | Key-value pairs of data to inject into the template. Keys map to Nunjucks placeholders (e.g., `{{name}}`). |

### Image Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `imageType` | string | `"png"` | Output format: `"png"`, `"jpg"`, `"jpeg"`, `"webp"`, or `"svg"`. |
| `quality` | number | — | Image quality (1-100). Only applies to `jpg`, `jpeg`, and `webp` formats. |
| `transparent` | boolean | `false` | Render with a transparent background. Only works with `"png"` and `"webp"`. The template must not set a background color on `<html>` or `<body>`. |
| `trim` | boolean | `false` | Trim whitespace from all edges of the resulting image. |

### Base Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `autoScroll` | boolean | `false` | Scroll the page to trigger lazy-loaded content before capture. |
| `delay` | number | `0` | Wait time in milliseconds after the page loads before capturing. |
| `filename` | string | — | Set the `Content-Disposition` filename on the response. |
| `height` | string | — | Set the capture area height with CSS units. |
| `width` | string | — | Set the capture area width with CSS units. |
| `inline` | boolean | `false` | Display the image inline in the browser instead of downloading. |
| `landscape` | boolean | `false` | Render in landscape orientation. |
| `preferCSSPageSize` | boolean | `false` | Use CSS `@page` size for dimensions. |
| `projectId` | string | — | Associate with a project for dashboard organization. |
| `scale` | number | `1` | Page rendering scale (`0.1` to `2`). |
| `timeout` | number | `30000` | Maximum page load time in milliseconds. |
| `timeZone` | string | — | Browser timezone (IANA name). |
| `viewPort` | object | — | Browser viewport configuration. See [HTML to PDF — viewPort](/docs/html-to-pdf/#viewport-object). |
| `waitForSelector` | object | — | Wait for a CSS selector before capturing. See [HTML to PDF — waitForSelector](/docs/html-to-pdf/#waitforselector-object). |
| `waitUntil` | string | `"networkidle2"` | Navigation completion strategy. |

---

## Use Cases

### Open Graph (OG) Images

Generate dynamic social sharing images for blog posts, product pages, and marketing campaigns:

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/template/image", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    templateId: "og-image-blog",
    data: {
      title: "10 Tips for Better API Design",
      author: "Jane Smith",
      authorAvatar: "https://example.com/avatars/jane.jpg",
      readTime: "5 min read",
      category: "Engineering",
    },
    imageType: "png",
    viewPort: { width: 1200, height: 630 },
  }),
});
```

### Email Banners

Generate personalized email header images:

```json
{
  "templateId": "email-banner",
  "data": {
    "recipientName": "Jane",
    "promoCode": "SAVE20",
    "expiryDate": "March 31, 2024"
  },
  "imageType": "png",
  "viewPort": {
    "width": 600,
    "height": 200
  }
}
```

### Certificate Generation

Generate certificates with dynamic recipient data:

```json
{
  "templateId": "certificate-completion",
  "data": {
    "recipientName": "Jane Smith",
    "courseName": "Advanced JavaScript",
    "completionDate": "January 15, 2024",
    "instructorName": "John Doe",
    "certificateId": "CERT-2024-0042"
  },
  "imageType": "png",
  "viewPort": {
    "width": 1100,
    "height": 850
  },
  "scale": 2
}
```

### Product Labels and Badges

Generate dynamic product labels:

```json
{
  "template": "...",
  "data": {
    "productName": "Organic Green Tea",
    "weight": "250g",
    "price": "$12.99",
    "barcode": "1234567890"
  },
  "imageType": "png",
  "trim": true,
  "transparent": true
}
```

---

## Multipart Upload

Upload template files directly instead of base64-encoding:

**cURL**

```bash
curl -X POST "https://api.cloudlayer.io/v2/template/image" \
  -H "X-API-Key: your-api-key-here" \
  -F "template=@./social-card-template.html" \
  -F 'data={"title":"Hello World","description":"Dynamic image generation"}'
```

**Python (requests)**

```python
import json
import requests

with open("social-card-template.html", "rb") as f:
    template_file = f.read()

response = requests.post(
    "https://api.cloudlayer.io/v2/template/image",
    headers={"X-API-Key": "your-api-key-here"},
    files={"template": ("template.html", template_file, "text/html")},
    data={
        "data": json.dumps(
            {"title": "Hello World", "description": "Dynamic image generation"}
        ),
    },
)

job = response.json()
```

---

## Tips

- **Fixed dimensions:** For consistent image output (e.g., OG images), set explicit dimensions in both the template CSS (`width`/`height` on `<body>`) and the `viewPort` parameter.
- **Template syntax:** Templates use [Nunjucks](https://mozilla.github.io/nunjucks/) syntax. See [Template to PDF — Template Syntax](/docs/template-to-pdf/#template-syntax) for conditionals, loops, and more.
- **High-DPI images:** Set `scale: 2` or `viewPort.deviceScaleFactor: 2` for images that look crisp on retina displays.
- **Transparent images:** Use `"png"` or `"webp"` with `transparent: true`. Do not set any background color in your template CSS.
- **Batch generation:** To generate many images from the same template with different data, make parallel API calls. Consider using async mode for large batches to avoid timeout issues.

### Account

Source: https://cloudlayer.io/docs/account/

# Account

Retrieve information about your cloudlayer.io account, including API call counts, subscription details, storage usage, compute time, and credit balance.

## Endpoint

<div class="v1-only">

```
GET /v1/account
```

</div>

<div class="v2-only">

```
GET /v2/account
```

</div>

---

## Request

No parameters required. Authentication is via the `X-API-Key` header.

**cURL**

```bash
curl -X GET "https://api.cloudlayer.io/v2/account" \
  -H "X-API-Key: your-api-key-here"
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/account", {
  headers: {
    "X-API-Key": "your-api-key-here",
  },
});

const account = await response.json();
console.log(account);
```

**Python (requests)**

```python
import requests

response = requests.get(
    "https://api.cloudlayer.io/v2/account",
    headers={"X-API-Key": "your-api-key-here"},
)

account = response.json()
print(account)
```

---

## Response

The response shape varies slightly depending on your billing model. See [Billing Models](#billing-models) below.

**Subscription (limit-based) example:**

```json
{
  "email": "user@example.com",
  "uid": "aBcDeFgHiJkLmNoPqRsTuVwXyZ12",
  "subscription": "price-starter-1k",
  "subType": "limit",
  "subActive": true,
  "calls": 842,
  "callsLimit": 1000,
  "bytesTotal": 1073741824,
  "bytesLimit": 5368709120,
  "computeTimeTotal": 384200,
  "computeTimeLimit": 600000,
  "storageUsed": 52428800,
  "storageLimit": 1073741824,
  "totalJobs": 900,
  "successJobs": 858,
  "errorJobs": 42
}
```

**Usage (credit-based) example:**

```json
{
  "email": "user@example.com",
  "uid": "aBcDeFgHiJkLmNoPqRsTuVwXyZ12",
  "subscription": "price-growth-usage",
  "subType": "usage",
  "subActive": true,
  "calls": 15234,
  "callsLimit": -1,
  "credit": 4500,
  "bytesTotal": 2147483648,
  "bytesLimit": -1,
  "computeTimeTotal": 720000,
  "computeTimeLimit": -1,
  "storageUsed": 104857600,
  "storageLimit": -1,
  "totalJobs": 15300,
  "successJobs": 15234,
  "errorJobs": 66
}
```

## Response Fields

| Field | Type | Description |
| --- | --- | --- |
| `email` | string | The email address associated with the account. |
| `uid` | string | The unique user ID for the account. |
| `subscription` | string | The price ID of your current subscription (e.g., `"price-starter-1k"`). |
| `subType` | string | Billing model: `"limit"` (subscription call limits) or `"usage"` (credit-based pay-per-use). See [Billing Models](#billing-models). |
| `subActive` | boolean | Whether the subscription is currently active. |
| `calls` | number | Total number of API calls made in the current billing period. |
| `callsLimit` | number | Maximum API calls allowed per billing period. `-1` indicates unlimited (usage-based plans). |
| `credit` | number | Remaining API credits. **Only present for usage-based plans** (`subType: "usage"`). Each API call consumes one or more credits depending on the operation. |
| `bytesTotal` | number | Total output bytes generated across all jobs. Divide by `1048576` for megabytes or `1073741824` for gigabytes. |
| `bytesLimit` | number | Maximum output bytes allowed per billing period. `-1` indicates unlimited. |
| `computeTimeTotal` | number | Total compute time used in milliseconds across all jobs in the current billing period. |
| `computeTimeLimit` | number | Maximum compute time allowed in milliseconds per billing period. `-1` indicates unlimited. |
| `storageUsed` | number | Total cloud storage currently used in bytes for stored assets. |
| `storageLimit` | number | Maximum cloud storage allowed in bytes. `-1` indicates unlimited. |
| `totalJobs` | number | Total number of completed jobs (success + error). |
| `successJobs` | number | Number of successfully completed jobs. |
| `errorJobs` | number | Number of failed jobs. |

---

## Billing Models

cloudlayer.io offers two billing models. The `subType` field in the account response indicates which model your plan uses.

### Limit-Based (`subType: "limit"`)

Subscription plans with a fixed number of API calls per billing period. The `calls` field tracks usage against `callsLimit`. When `calls` reaches `callsLimit`, further API requests are rejected until the next billing period.

- The `credit` field is **not present** in the response.
- `callsLimit`, `bytesLimit`, `computeTimeLimit`, and `storageLimit` reflect your plan's caps.

### Usage-Based (`subType: "usage"`)

Credit-based plans where each API call deducts from your `credit` balance. There is no fixed call limit per billing period.

- The `credit` field **is present** in the response, showing your remaining balance.
- Limit fields (`callsLimit`, `bytesLimit`, etc.) are typically `-1` (unlimited).

---

## Use Cases

### Monitor Usage

Check your API usage to stay within plan limits:

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/account", {
  headers: { "X-API-Key": "your-api-key-here" },
});

const account = await response.json();

console.log(`Calls this period: ${account.calls}`);
console.log(`Storage used: ${(account.storageUsed / 1073741824).toFixed(2)} GB`);
console.log(`Jobs completed: ${account.successJobs}`);
console.log(`Jobs failed: ${account.errorJobs}`);

if (account.subType === "limit") {
  console.log(`Calls remaining: ${account.callsLimit - account.calls}`);
} else {
  console.log(`Credit balance: ${account.credit}`);
}
```

### Usage Alerts

Set up an automated check to alert you when usage is running low:

```python
import requests

response = requests.get(
    "https://api.cloudlayer.io/v2/account",
    headers={"X-API-Key": "your-api-key-here"},
)

account = response.json()

if account["subType"] == "usage" and account.get("credit", 0) < 100:
    print(f"WARNING: Only {account['credit']} credits remaining!")
    # Send alert via email, Slack, etc.

if account["subType"] == "limit" and account["callsLimit"] > 0:
    usage_pct = account["calls"] / account["callsLimit"] * 100
    if usage_pct > 90:
        print(f"WARNING: {usage_pct:.0f}% of call limit used!")
```

---

## Tips

- **Billing period:** The `calls` count resets at the beginning of each billing period. Check your dashboard for your billing cycle dates.
- **Credit consumption:** Different operations consume different amounts of credits. PDF generation with large HTML or many pages may consume more credits than a simple image capture.
- **Storage management:** If `storageUsed` is approaching your `storageLimit`, use the [Assets](/docs/assets/) endpoint to review and clean up old generated files.

### Assets

Source: https://cloudlayer.io/docs/assets/

# Assets

Assets are the files generated by cloudlayer.io API calls, PDFs, PNGs, JPGs, and WebP images. Every document generation request produces an asset that you can retrieve later by its ID.

Assets are particularly useful when using **async mode**, where the generation response returns a job ID instead of the file directly. Once the job completes, the asset is available for download.

## Endpoints

<div class="v1-only">

```
GET /v1/assets/:id
GET /v1/assets
```

</div>

<div class="v2-only">

```
GET /v2/assets/:id
GET /v2/assets
```

</div>

---

## Get a Single Asset

Retrieve a specific asset by its ID.

<div class="v1-only">

```
GET /v1/assets/:id
```

</div>

<div class="v2-only">

```
GET /v2/assets/:id
```

</div>

### Path Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | The unique asset ID. |

### Examples

**cURL**

```bash
curl -X GET "https://api.cloudlayer.io/v2/assets/aBcDeFgHiJk123" \
  -H "X-API-Key: your-api-key-here"
```

**JavaScript (fetch)**

```javascript
const assetId = "aBcDeFgHiJk123";

const response = await fetch(
  `https://api.cloudlayer.io/v2/assets/${assetId}`,
  {
    headers: {
      "X-API-Key": "your-api-key-here",
    },
  }
);

const asset = await response.json();
console.log(asset);
```

**Python (requests)**

```python
import requests

asset_id = "aBcDeFgHiJk123"

response = requests.get(
    f"https://api.cloudlayer.io/v2/assets/{asset_id}",
    headers={"X-API-Key": "your-api-key-here"},
)

asset = response.json()
print(asset)
```

### Response

```json
{
  "id": "aBcDeFgHiJk123",
  "jobId": "1705312200000",
  "ext": "pdf",
  "type": "application/pdf",
  "size": 245760,
  "url": "https://storage.cloudlayer.io/assets/aBcDeFgHiJk123.pdf",
  "timestamp": 1705312200000
}
```

---

## List Assets

Retrieve a list of your most recent assets, ordered by creation date (newest first). Results are limited to 10 items.

<div class="v1-only">

```
GET /v1/assets
```

</div>

<div class="v2-only">

```
GET /v2/assets
```

</div>

**Query Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | number | 10 | Number of assets to return (1–100) |
| `startAfterId` | string | — | Asset ID to paginate after (for cursor-based pagination) |

### Examples

**cURL**

```bash
curl -X GET "https://api.cloudlayer.io/v2/assets" \
  -H "X-API-Key: your-api-key-here"
```

**JavaScript (fetch)**

```javascript
const response = await fetch(
  "https://api.cloudlayer.io/v2/assets",
  {
    headers: {
      "X-API-Key": "your-api-key-here",
    },
  }
);

const assets = await response.json();
console.log(assets);
```

**Python (requests)**

```python
import requests

response = requests.get(
    "https://api.cloudlayer.io/v2/assets",
    headers={"X-API-Key": "your-api-key-here"},
)

assets = response.json()
for asset in assets:
    print(f"{asset['id']} - {asset['ext']} - {asset['size']} bytes")
```

### Response

```json
[
  {
    "id": "aBcDeFgHiJk123",
    "jobId": "1705312200000",
    "ext": "pdf",
    "type": "application/pdf",
    "size": 245760,
    "url": "https://storage.cloudlayer.io/assets/aBcDeFgHiJk123.pdf",
    "timestamp": 1705312200000
  },
  {
    "id": "xYzAbCdEfGh456",
    "jobId": "1705311900000",
    "ext": "png",
    "type": "image/png",
    "size": 102400,
    "url": "https://storage.cloudlayer.io/assets/xYzAbCdEfGh456.png",
    "timestamp": 1705311900000
  }
]
```

---

## Response Fields

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Unique asset identifier (e.g., `"aBcDeFgHiJk123"`). |
| `jobId` | string | The ID of the job that produced this asset. Use this to correlate assets with their generation requests via the [Jobs](/docs/jobs/) endpoint. |
| `ext` | string | File extension: `"pdf"`, `"png"`, `"jpg"`, or `"webp"`. |
| `type` | string | MIME type of the asset (e.g., `"application/pdf"`, `"image/png"`, `"image/jpeg"`, `"image/webp"`). |
| `size` | number | File size in bytes. |
| `url` | string | Direct download URL for the asset. This URL is pre-authenticated and can be used to download the file without an API key. URL expiration depends on your storage configuration. |
| `timestamp` | number | Unix epoch timestamp in milliseconds of when the asset was created. |

---

## Download an Asset

The `url` field in the asset response is a direct download link. You can use it to download the file without additional authentication:

**cURL**

```bash
# First, get the asset metadata
ASSET_URL=$(curl -s "https://api.cloudlayer.io/v2/assets/aBcDeFgHiJk123" \
  -H "X-API-Key: your-api-key-here" | jq -r '.url')

# Then download the file
curl -o downloaded-file.pdf "$ASSET_URL"
```

**JavaScript (fetch)**

```javascript
// Get asset metadata
const metaResponse = await fetch(
  "https://api.cloudlayer.io/v2/assets/aBcDeFgHiJk123",
  {
    headers: { "X-API-Key": "your-api-key-here" },
  }
);
const asset = await metaResponse.json();

// Download the file using the pre-authenticated URL
const fileResponse = await fetch(asset.url);
const fileBuffer = await fileResponse.arrayBuffer();
```

**Python (requests)**

```python
import requests

# Get asset metadata
meta_response = requests.get(
    "https://api.cloudlayer.io/v2/assets/aBcDeFgHiJk123",
    headers={"X-API-Key": "your-api-key-here"},
)
asset = meta_response.json()

# Download the file
file_response = requests.get(asset["url"])
with open(f"downloaded.{asset['ext']}", "wb") as f:
    f.write(file_response.content)
```

---

## Async Workflow

When using async mode for document generation, use the Assets endpoint to retrieve the result:

```javascript
// 1. Start an async generation job
const jobResponse = await fetch("https://api.cloudlayer.io/v2/html/pdf", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html: btoa("<h1>Hello World</h1>"),
    async: true,
  }),
});

const job = await jobResponse.json();
console.log(`Job started: ${job.id}`);

// 2. Poll the job until it completes
let jobStatus;
do {
  await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
  const statusResponse = await fetch(
    `https://api.cloudlayer.io/v2/jobs/${job.id}`,
    { headers: { "X-API-Key": "your-api-key-here" } }
  );
  jobStatus = await statusResponse.json();
} while (jobStatus.status === "pending");

// 3. Retrieve the generated asset
if (jobStatus.status === "success") {
  const assetsResponse = await fetch(
    "https://api.cloudlayer.io/v2/assets",
    { headers: { "X-API-Key": "your-api-key-here" } }
  );
  const assets = await assetsResponse.json();
  console.log(`Download URL: ${assets[0].url}`);
}
```

---

## Tips

- **Asset retention:** Assets are retained based on your subscription plan. Check your plan details for the retention period. Download and store important assets in your own storage if long-term retention is required.
- **Storage usage:** Use the [Account](/docs/account/) endpoint to monitor your total storage usage (`storageUsed`).
- **User storage:** Configure your own S3-compatible storage to have assets delivered directly to your bucket. See the [Storage](/docs/storage/) endpoint for configuration.
- **Result limit:** The list endpoint returns the 10 most recent assets.

### Jobs

Source: https://cloudlayer.io/docs/jobs/

# Jobs

Every API call to cloudlayer.io creates a **job**. Jobs track the status, parameters, processing time, and cost of each generation request. Use the Jobs API to monitor async requests, debug failed generations, and audit API usage.

## Endpoints

<div class="v1-only">

```
GET /v1/jobs/:id
GET /v1/jobs
```

</div>

<div class="v2-only">

```
GET /v2/jobs/:id
GET /v2/jobs
```

</div>

---

## Get a Single Job

Retrieve a specific job by its ID.

<div class="v1-only">

```
GET /v1/jobs/:id
```

</div>

<div class="v2-only">

```
GET /v2/jobs/:id
```

</div>

### Path Parameters

| Parameter | Type   | Required | Description        |
| --------- | ------ | -------- | ------------------ |
| `id`      | string | Yes      | The unique job ID. |

### Examples

**cURL**

```bash
curl -X GET "https://api.cloudlayer.io/v2/jobs/1705312200000" \
  -H "X-API-Key: your-api-key-here"
```

**JavaScript (fetch)**

```javascript
const jobId = '1705312200000';

const response = await fetch(`https://api.cloudlayer.io/v2/jobs/${jobId}`, {
  headers: {
    'X-API-Key': 'your-api-key-here'
  }
});

const job = await response.json();
console.log(job);
```

**Python (requests)**

```python
import requests

job_id = "1705312200000"

response = requests.get(
    f"https://api.cloudlayer.io/v2/jobs/{job_id}",
    headers={"X-API-Key": "your-api-key-here"},
)

job = response.json()
print(job)
```

### Response

```json
{
  "id": "1705312200000",
  "uid": "aBcDeFgHiJkLmNoPqRsTuVwXyZ12",
  "type": "html-pdf",
  "status": "success",
  "params": {
    "format": "letter",
    "margin": {
      "top": "1in",
      "bottom": "1in"
    },
    "printBackground": true
  },
  "size": 245760,
  "processTime": 2340,
  "apiCreditCost": 1,
  "workerName": "worker-us-east-1a",
  "timestamp": 1705312200000
}
```

---

## List Jobs

Retrieve a list of your most recent jobs, ordered by creation date (newest first). Results are limited to 10 items.

<div class="v1-only">

```
GET /v1/jobs
```

</div>

<div class="v2-only">

```
GET /v2/jobs
```

</div>

**Query Parameters:**

| Parameter      | Type   | Default | Description                                            |
| -------------- | ------ | ------- | ------------------------------------------------------ |
| `limit`        | number | 10      | Number of jobs to return (1–100)                       |
| `startAfterId` | string | —       | Job ID to paginate after (for cursor-based pagination) |

### Examples

**cURL**

```bash
curl -X GET "https://api.cloudlayer.io/v2/jobs" \
  -H "X-API-Key: your-api-key-here"
```

**JavaScript (fetch)**

```javascript
const response = await fetch('https://api.cloudlayer.io/v2/jobs', {
  headers: {
    'X-API-Key': 'your-api-key-here'
  }
});

const jobs = await response.json();

for (const job of jobs) {
  console.log(`${job.id} | ${job.type} | ${job.status} | ${job.processTime}ms`);
}
```

**Python (requests)**

```python
import requests

response = requests.get(
    "https://api.cloudlayer.io/v2/jobs",
    headers={"X-API-Key": "your-api-key-here"},
)

jobs = response.json()
for job in jobs:
    print(f"{job['id']} | {job['type']} | {job['status']} | {job['processTime']}ms")
```

### Response

```json
[
  {
    "id": "1705312200000",
    "uid": "aBcDeFgHiJkLmNoPqRsTuVwXyZ12",
    "type": "html-pdf",
    "status": "success",
    "params": {
      "format": "letter",
      "printBackground": true
    },
    "size": 245760,
    "processTime": 2340,
    "apiCreditCost": 1,
    "workerName": "worker-us-east-1a",
    "timestamp": 1705312200000
  },
  {
    "id": "1705311900000",
    "uid": "aBcDeFgHiJkLmNoPqRsTuVwXyZ12",
    "type": "url-image",
    "status": "success",
    "params": {
      "url": "https://example.com",
      "imageType": "png"
    },
    "size": 102400,
    "processTime": 3150,
    "apiCreditCost": 1,
    "workerName": "worker-us-east-1b",
    "timestamp": 1705311900000
  }
]
```

---

## Response Fields

| Field           | Type   | Description                                                                                                                                                                                  |
| --------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`            | string | Unique job identifier (e.g., `"1705312200000"`).                                                                                                                                             |
| `uid`           | string | The user ID that owns this job.                                                                                                                                                              |
| `type`          | string | The job type, matching the API endpoint used. See [Job Types](#job-types) below.                                                                                                             |
| `status`        | string | Current job status. See [Job Statuses](#job-statuses) below.                                                                                                                                 |
| `params`        | object | The parameters that were sent with the API request (excluding the `html` or `template` body for privacy). Useful for debugging and understanding what configuration produced a given result. |
| `size`          | number | Size of the generated output file in bytes. `0` for pending or failed jobs.                                                                                                                  |
| `processTime`   | number | Time in milliseconds from when the job started processing to completion. `0` for pending jobs.                                                                                               |
| `apiCreditCost` | number | Number of API credits consumed by this job.                                                                                                                                                  |
| `workerName`    | string | Identifier of the worker instance that processed this job. Useful for support and debugging.                                                                                                 |
| `timestamp`     | number | Unix epoch timestamp in milliseconds of when the job was created.                                                                                                                            |

### Job Types

| Type             | Description                  |
| ---------------- | ---------------------------- |
| `html-pdf`       | HTML to PDF generation       |
| `url-pdf`        | URL to PDF generation        |
| `template-pdf`   | Template to PDF generation   |
| `html-image`     | HTML to image generation     |
| `url-image`      | URL to image generation      |
| `template-image` | Template to image generation |

### Job Statuses

| Status    | Description                                                                   |
| --------- | ----------------------------------------------------------------------------- |
| `pending` | The job has been created and is waiting to be processed.                      |
| `success` | The job finished successfully. The generated asset is available for download. |
| `error`   | The job failed. Check the response for error details.                         |

---

## Polling for Async Jobs

When using async mode, poll the job endpoint to check when processing is complete:

**JavaScript (fetch)**

```javascript
async function waitForJob(jobId, apiKey, maxWaitMs = 60000) {
  const startTime = Date.now();
  const pollInterval = 1000; // 1 second

  while (Date.now() - startTime < maxWaitMs) {
    const response = await fetch(`https://api.cloudlayer.io/v2/jobs/${jobId}`, {
      headers: { 'X-API-Key': apiKey }
    });

    const job = await response.json();

    if (job.status === 'success') {
      return job;
    }

    if (job.status === 'error') {
      throw new Error(`Job ${jobId} failed`);
    }

    // Wait before polling again
    await new Promise((resolve) => setTimeout(resolve, pollInterval));
  }

  throw new Error(`Job ${jobId} timed out after ${maxWaitMs}ms`);
}

// Usage
const job = await waitForJob('1705312200000', 'your-api-key-here');
console.log(`Completed in ${job.processTime}ms, size: ${job.size} bytes`);
```

**Python (requests)**

```python
import time
import requests


def wait_for_job(job_id, api_key, max_wait_seconds=60):
    start_time = time.time()
    poll_interval = 1  # seconds

    while time.time() - start_time < max_wait_seconds:
        response = requests.get(
            f"https://api.cloudlayer.io/v2/jobs/{job_id}",
            headers={"X-API-Key": api_key},
        )
        job = response.json()

        if job["status"] == "success":
            return job

        if job["status"] == "error":
            raise Exception(f"Job {job_id} failed")

        time.sleep(poll_interval)

    raise TimeoutError(f"Job {job_id} timed out after {max_wait_seconds}s")


# Usage
job = wait_for_job("1705312200000", "your-api-key-here")
print(f"Completed in {job['processTime']}ms, size: {job['size']} bytes")
```

---

## Usage Analytics

Use the Jobs API to build your own usage dashboards and analytics:

```python
import requests

response = requests.get(
    "https://api.cloudlayer.io/v2/jobs",
    headers={"X-API-Key": "your-api-key-here"},
)

jobs = response.json()

# Compute statistics
total_credits = sum(job["apiCreditCost"] for job in jobs)
avg_process_time = sum(job["processTime"] for job in jobs) / len(jobs)
total_bytes = sum(job["size"] for job in jobs)
failed_count = sum(1 for job in jobs if job["status"] == "error")

print(f"Total credits used: {total_credits}")
print(f"Average processing time: {avg_process_time:.0f}ms")
print(f"Total output size: {total_bytes / 1048576:.2f} MB")
print(f"Failed jobs: {failed_count}")
```

---

## Tips

- **Debugging failures:** When a job fails, check the `params` field to see what configuration was used. Common issues include timeouts (increase the `timeout` parameter), unreachable URLs, and malformed HTML.
- **Process time optimization:** Monitor `processTime` across jobs to identify slow generation patterns. Consider reducing viewport size, using `waitUntil: "domcontentloaded"`, or simplifying your HTML content.
- **Result limit:** The list endpoint returns the 10 most recent jobs.

### Storage

Source: https://cloudlayer.io/docs/storage/

# Storage

cloudlayer.io supports three storage modes for generated files. You can use the built-in cloud storage (default), skip storage entirely, or connect your own S3-compatible bucket for direct delivery.

## Storage Types

| Type | Description | Availability |
| --- | --- | --- |
| **No Storage** | Generated files are returned in the HTTP response only. No files are stored on cloudlayer.io servers. | All plans |
| **Cloud Storage** | Generated files are stored on cloudlayer.io's managed cloud storage and accessible via the [Assets](/docs/assets/) API. This is the default in v2. | Included with all v2 plans |
| **User Storage** | Generated files are delivered directly to your own S3-compatible storage bucket (AWS S3, Google Cloud Storage, DigitalOcean Spaces, MinIO, Backblaze B2, etc.). | Growth and Business plans |

---

## No Storage

When you make a synchronous API request, the generated file is returned directly in the HTTP response body. v1 does not store it unless requested. In v2, set `"async": false` and `"storage": false` when you do not want the generated asset retained or available through the Assets API.

For synchronous requests, the file is always included in the response regardless of storage settings.

---

## Cloud Storage (Default)

Cloud storage is enabled by default for all v2 API requests. Generated files are automatically stored and accessible via the [Assets](/docs/assets/) endpoint. No configuration is needed.

- Files are stored securely on cloudlayer.io infrastructure
- Each file is accessible via a pre-authenticated download URL
- Retention period depends on your subscription plan
- Monitor usage via the [Account](/docs/account/) endpoint (`storageUsed`)

---

## User Storage (S3-Compatible)

Connect your own S3-compatible storage bucket to have generated files delivered directly to your infrastructure. This gives you full control over file retention, access policies, and geographic placement.

### Endpoints

Storage endpoints are available on both v1 and v2.

```
POST   /v2/storage
GET    /v2/storage
GET    /v2/storage/:id
DELETE /v2/storage/:id
```

---

### Create or Update Storage Configuration

Configure your S3-compatible storage bucket.

```
POST /v2/storage
```

#### Request Body

| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `title` | string | Yes | — | A unique name for this storage configuration (e.g., `"Production S3"`, `"Invoice Storage"`). Must be unique across your storage configurations. |
| `bucket` | string | Yes | — | The S3 bucket name (e.g., `"my-company-pdfs"`). |
| `region` | string | Yes | — | The AWS region or equivalent (e.g., `"us-east-1"`, `"eu-west-1"`). |
| `accessKeyId` | string | Yes | — | The access key ID for your S3-compatible storage. |
| `secretAccessKey` | string | Yes | — | The secret access key for your S3-compatible storage. |
| `endpoint` | string | No | — | Custom endpoint URL for non-AWS S3-compatible services (e.g., `"https://nyc3.digitaloceanspaces.com"`, `"https://s3.eu-central-1.wasabisys.com"`). Omit for AWS S3. |

#### Examples

**cURL (AWS S3)**

```bash
curl -X POST "https://api.cloudlayer.io/v2/storage" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Production S3",
    "bucket": "my-company-pdfs",
    "region": "us-east-1",
    "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
    "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
  }'
```

**cURL (DigitalOcean Spaces)**

```bash
curl -X POST "https://api.cloudlayer.io/v2/storage" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "DO Spaces",
    "bucket": "my-space-name",
    "region": "nyc3",
    "accessKeyId": "your-spaces-access-key",
    "secretAccessKey": "your-spaces-secret-key",
    "endpoint": "https://nyc3.digitaloceanspaces.com"
  }'
```

**cURL (Google Cloud Storage)**

```bash
curl -X POST "https://api.cloudlayer.io/v2/storage" \
  -H "X-API-Key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "GCS Bucket",
    "bucket": "my-gcs-bucket",
    "region": "us-central1",
    "accessKeyId": "GOOGTS7C7FUP3AIRVJTE2BCDKINBTES3HC2GY5CBFJDCQ2SYHIPAQKT6EXAMPLE",
    "secretAccessKey": "your-hmac-secret-key",
    "endpoint": "https://storage.googleapis.com"
  }'
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/storage", {
  method: "POST",
  headers: {
    "X-API-Key": "your-api-key-here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    title: "Production S3",
    bucket: "my-company-pdfs",
    region: "us-east-1",
    accessKeyId: "AKIAIOSFODNN7EXAMPLE",
    secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  }),
});

const result = await response.json();
console.log(result);
```

**Python (requests)**

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v2/storage",
    headers={
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
    },
    json={
        "title": "Production S3",
        "bucket": "my-company-pdfs",
        "region": "us-east-1",
        "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
        "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    },
)

result = response.json()
print(result)
```

#### Response

```json
{
  "title": "Production S3",
  "id": "aBcDeFgHiJk123"
}
```

---

### Get Storage Configuration

Retrieve your current storage configuration. Sensitive fields (`secretAccessKey`) are masked in the response.

```
GET /v2/storage
```

#### Examples

**cURL**

```bash
curl -X GET "https://api.cloudlayer.io/v2/storage" \
  -H "X-API-Key: your-api-key-here"
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/storage", {
  headers: {
    "X-API-Key": "your-api-key-here",
  },
});

const config = await response.json();
console.log(config);
```

**Python (requests)**

```python
import requests

response = requests.get(
    "https://api.cloudlayer.io/v2/storage",
    headers={"X-API-Key": "your-api-key-here"},
)

config = response.json()
print(config)
```

#### Response

```json
{
  "title": "Production S3",
  "bucket": "my-company-pdfs",
  "region": "us-east-1",
  "accessKeyId": "AKIA...MPLE",
  "secretAccessKey": "****",
  "endpoint": null
}
```

---

### Get Storage Configuration by ID

Retrieve a specific storage configuration by its ID.

```
GET /v2/storage/:id
```

#### Path Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | The unique storage configuration ID. |

#### Examples

**cURL**

```bash
curl -X GET "https://api.cloudlayer.io/v2/storage/aBcDeFgHiJk123" \
  -H "X-API-Key: your-api-key-here"
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/storage/aBcDeFgHiJk123", {
  headers: {
    "X-API-Key": "your-api-key-here",
  },
});

const config = await response.json();
console.log(config);
```

**Python (requests)**

```python
import requests

response = requests.get(
    "https://api.cloudlayer.io/v2/storage/aBcDeFgHiJk123",
    headers={"X-API-Key": "your-api-key-here"},
)

config = response.json()
print(config)
```

#### Response

```json
{
  "title": "Production S3",
  "bucket": "my-company-pdfs",
  "region": "us-east-1",
  "accessKeyId": "AKIA...MPLE",
  "secretAccessKey": "****",
  "endpoint": null
}
```

---

### Delete Storage Configuration

Remove a storage configuration by its ID and revert to the default cloud storage.

```
DELETE /v2/storage/:id
```

#### Path Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | The unique storage configuration ID. |

#### Examples

**cURL**

```bash
curl -X DELETE "https://api.cloudlayer.io/v2/storage/aBcDeFgHiJk123" \
  -H "X-API-Key: your-api-key-here"
```

**JavaScript (fetch)**

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/storage/aBcDeFgHiJk123", {
  method: "DELETE",
  headers: {
    "X-API-Key": "your-api-key-here",
  },
});

const result = await response.json();
console.log(result);
```

**Python (requests)**

```python
import requests

response = requests.delete(
    "https://api.cloudlayer.io/v2/storage/aBcDeFgHiJk123",
    headers={"X-API-Key": "your-api-key-here"},
)

result = response.json()
print(result)
```

#### Response

```json
{
  "message": "Storage configuration deleted. Reverting to default cloud storage."
}
```

---

## S3-Compatible Services

The following S3-compatible storage services have been tested and are fully supported:

| Provider | Endpoint Format | Notes |
| --- | --- | --- |
| **AWS S3** | Omit `endpoint` (default) | Standard S3 service. Use IAM credentials with `s3:PutObject` permission on the target bucket. |
| **Google Cloud Storage** | `https://storage.googleapis.com` | Requires [HMAC keys](https://cloud.google.com/storage/docs/authentication/hmackeys) for interoperability access. |
| **DigitalOcean Spaces** | `https://{region}.digitaloceanspaces.com` | Spaces API keys from the DO control panel. |
| **Backblaze B2** | `https://s3.{region}.backblazeb2.com` | B2 application keys with write access. |
| **MinIO** | `https://your-minio-server.com` | Self-hosted S3-compatible storage. |
| **Wasabi** | `https://s3.{region}.wasabisys.com` | Hot storage with no egress fees. |
| **Cloudflare R2** | `https://{account-id}.r2.cloudflarestorage.com` | No egress fees. Use R2 API tokens. |

---

## IAM Policy for AWS S3

If using AWS S3, create an IAM user with the minimum required permissions:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:PutObjectAcl"
      ],
      "Resource": "arn:aws:s3:::my-company-pdfs/*"
    }
  ]
}
```

> **Security tip:** Scope the IAM policy to the specific bucket. Do not grant `s3:*` or access to other buckets. Use a dedicated IAM user for cloudlayer.io, not your root account credentials.

---

## How User Storage Works

When user storage is configured:

1. You make an API request to generate a PDF or image (e.g., `POST /v2/html/pdf`).
2. cloudlayer.io generates the file as usual.
3. The file is uploaded to your S3-compatible bucket.
4. The API response includes the file as usual (for sync requests).
5. The [Assets](/docs/assets/) endpoint reflects the file's location in your bucket.

---

## Tips

- **CORS:** If you need to access files from a browser, configure CORS on your S3 bucket to allow requests from your domain.
- **Credentials rotation:** Rotate your S3 access keys periodically. Update the storage configuration via `POST /v2/storage` with new credentials.
- **Testing:** After configuring user storage, generate a test file and verify it appears in your bucket at the expected location.
- **Fallback:** If the upload to your bucket fails (e.g., invalid credentials, bucket does not exist), the API request will still succeed and the file will be available via cloudlayer.io cloud storage. Check your storage configuration if files are not appearing in your bucket.
- **Reverting:** Delete your storage configuration (`DELETE /v2/storage/:id`) at any time to revert to the default cloud storage.


## Guides

### Visual Template Editor

Source: https://cloudlayer.io/docs/visual-editor/

# Visual Template Editor

The visual editor lets you design PDF and image templates by dragging and dropping elements onto a canvas — no HTML or CSS required. Bind data fields to elements, preview with sample data, and generate documents via the API or directly from the editor.

## Getting Started

### Creating a Template

1. Go to **Templates** → **New Template**
2. Choose output type: **PDF** or **Image**
3. Choose creation mode:
   - **Blank** — Start with an empty canvas
   - **AI Generate** — Describe what you want in plain text (e.g., "A professional invoice with line items, subtotals, and company logo") and the AI generates a starting template you can refine

### Editor Layout

The editor has three main areas:

- **Left Panel** — Layers, insertable elements, and data field management
- **Center Canvas** — Your template. Drag, resize, and position elements here.
- **Right Panel** — Property inspector for the selected element or page settings

All panels are resizable and collapsible. Your layout preferences are saved automatically.

---

## Elements

Insert elements from the toolbar or the left panel's Elements tab.

### Basic Elements

| Element | Description |
|---------|-------------|
| **Text** | Editable text with full typography control. Supports data bindings like `{{ name }}`. |
| **Rectangle** | Shape with fill color, border, rounded corners, and shadow. |
| **Circle** | Round shape with fill, border, and shadow. |
| **Line** | Straight line with stroke color and width. |
| **Image** | Display an image from a URL. Supports cover, contain, and fill modes. |

### Data-Driven Elements

| Element | Description |
|---------|-------------|
| **Table** | Define columns bound to data fields. Automatic row generation from arrays. |
| **Chart** | Bar, line, pie, donut, or area chart. *(Coming soon — currently in preview.)* |
| **QR Code** | Generate a QR code from static text or a data field. Configurable error correction. |
| **Barcode** | Generate barcodes (Code128, EAN, UPC, etc.) from data. |

### Template Blocks

Template blocks add logic to your design — loops, conditionals, and reusable sections.

| Block | Description | Example |
|-------|-------------|---------|
| **For Loop** | Repeat elements for each item in a list | `for item in invoice.lineItems` |
| **Conditional** | Show or hide elements based on data | `if showLogo` |
| **Block** | Named reusable content section | `block header` |
| **Macro** | Reusable template function | `macro badge(text, color)` |
| **Set Variable** | Define a local variable | `set total = subtotal + tax` |

Blocks appear as colored containers on the canvas. Drag elements inside them to make them part of the block.

---

## Working with Elements

### Selecting & Moving

- **Click** an element to select it
- **Drag** to reposition
- **Resize** using corner and edge handles (hold **Shift** for aspect ratio lock)
- **Ctrl+Click** to select multiple elements
- **Ctrl+A** to select all
- **Right-click** for context menu (duplicate, copy, cut, delete, reorder layers, lock, hide, group/ungroup)

### Alignment & Snapping

- **Snap guides** appear automatically when elements align with each other
- **Distance labels** show spacing between elements
- Toggle snapping on/off from the toolbar
- Enable **grid snap** to lock positioning to a pixel grid (configurable grid size: 2–100px)

### Grouping

Select multiple elements → right-click → **Group**. Groups act as containers — move or resize the group and all children follow. Ungroup with **Ctrl+Shift+G**.

### Layers Panel

The left panel's Layers tab shows all elements in z-order. From here you can:

- **Reorder** elements (drag up/down to change layering)
- **Rename** elements for clarity
- **Show/hide** elements (eye icon)
- **Lock/unlock** elements to prevent accidental edits (lock icon)

---

## Styling Elements

Select any element and use the right panel's Property Inspector to style it.

### Text Properties

- **Font** — Choose from web-safe fonts or search Google Fonts
- **Size** — 1 to 999px
- **Weight** — Thin (100) through Black (900)
- **Style** — Normal, italic
- **Decoration** — Underline, strikethrough
- **Transform** — Uppercase, lowercase, capitalize
- **Alignment** — Left, center, right, justify (horizontal); top, middle, bottom (vertical)
- **Spacing** — Line height, letter spacing, word spacing
- **Color** — Full color picker
- **Padding** — Internal spacing
- **Text Fitting** — None (default), shrink to fit, or grow to fit

### Shape & Common Properties

- **Fill** — Background color with toggle
- **Border** — Color, width (0–20px), style (solid, dashed, dotted), corner radius
- **Shadow** — X/Y offset, blur, spread, color
- **Opacity** — 0–100%
- **Position** — X, Y coordinates (absolute)
- **Size** — Width, height
- **Rotation** — Angle in degrees

### Image Properties

- **Source** — Image URL, file upload, or data variable binding
- **Fit Mode** — Cover (fill, crop if needed), contain (fit within bounds), fill (stretch)
- **Alt Text** — Accessibility description
- **Filters** — Brightness, contrast, saturation, blur

---

## Data Binding

The visual editor supports dynamic data — connect your template elements to data fields that get populated when you generate documents via the API.

### Defining Data Fields

1. Open the **Data** tab in the left panel
2. Click **Configure Data** to open the field builder
3. Add fields with a name and type (text, number, date, image URL, boolean, list)
4. Set default values for preview
5. Save — sample data is generated automatically

### Binding Data to Elements

- **Drag** a data field from the left panel onto an element
- Or select an element and pick a field from the **Data Source** section in the property inspector
- Text elements use Nunjucks syntax: `{{ customerName }}`, `{{ invoice.total | formatCurrency }}`

### Variable Preview Mode

Toggle **Preview Data** in the toolbar to see actual sample data values rendered in place of variable placeholders. This shows exactly how your document will look with real data.

### Template Syntax

The editor uses [Nunjucks](https://mozilla.github.io/nunjucks/) template syntax:

- **Variables:** `{{ variableName }}`
- **Filters:** `{{ price | formatCurrency }}`, `{{ date | formatDateTime }}`
- **Loops:** `{% for item in items %}...{% endfor %}`
- **Conditionals:** `{% if showSection %}...{% endif %}`

See the [Templating Guide](/docs/templating/) for full syntax reference and available helpers.

---

## Page Settings

Click the page dimensions in the toolbar to configure:

- **Paper Size** — Letter, A4, A3, Legal, Tabloid, and more
- **Image Presets** — Open Graph (1200×630), Instagram (1080×1080), Twitter (1200×675), Facebook, Pinterest, YouTube Thumbnail, LinkedIn, Story
- **Custom Size** — Set any width and height
- **Orientation** — Portrait or landscape (toggle available for non-square sizes)
- **Background Color** — Set via the property inspector when no element is selected
- **Margins** — Top, right, bottom, left (0–500px)

---

## Preview & Generation

### Live Preview

The editor shows a live preview of your template rendered with sample data. In split view, the preview updates as you make changes.

### Generating Documents

Click **Generate** in the editor header to render your template as a PDF or image. The result opens in a new tab for download.

To generate via the API, use the **API Code** button to see the exact endpoint and request body for your template. Copy it into your application code.

### Using Templates via API

Once saved, use your template's ID with the conversion endpoints:

```
POST /v2/template/pdf
{
  "templateId": "your-template-id",
  "data": {
    "customerName": "Acme Corp",
    "items": [...]
  }
}
```

See [Template to PDF](/docs/template-to-pdf/) and [Template to Image](/docs/template-to-image/) for full API reference.

---

## Code Editor

The editor also includes a full code editor for users who prefer writing HTML directly:

- **HTML Tab** — Monaco code editor with Nunjucks syntax highlighting, autocomplete for template variables, and diagnostics for undefined variables
- **Data Tab** — JSON editor for sample data with validation
- **Options Tab** — PDF/image generation settings (format, viewport, etc.)

Switch between Visual and HTML tabs at any time. Both modes work with the same template.

---

## Keyboard Shortcuts

| Shortcut | Action |
|----------|--------|
| **V** | Select tool |
| **H** | Hand/pan tool |
| **T** | Text tool |
| **R** | Rectangle tool |
| **C** | Circle tool |
| **L** | Line tool |
| **Ctrl+S** | Save |
| **Ctrl+Z** | Undo |
| **Ctrl+Shift+Z** | Redo |
| **Ctrl+D** | Duplicate selected |
| **Delete** | Remove selected |
| **Ctrl+G** | Group selected |
| **Ctrl+Shift+G** | Ungroup |
| **Ctrl+A** | Select all |

Press **Ctrl+Shift+/** to see the full shortcut reference.

---

## Tips

- **Use snap guides** for precise alignment. They appear as you drag elements near each other.
- **Name your layers** — Double-click a layer name to rename it. Makes complex templates much easier to manage.
- **Lock finished elements** — Click the lock icon to prevent accidental changes while you work on other parts
- **Preview with real data** — Toggle Preview Data mode to catch layout issues with actual content lengths
- **Start with AI** — Use AI Generate to get a starting template, then refine in the visual editor

### Templating

Source: https://cloudlayer.io/docs/templating/

# Templating

cloudlayer.io uses the [Nunjucks](https://mozilla.github.io/nunjucks/) templating engine to power its template-based document generation. With templates, you can create professional, data-driven PDFs and images, invoices, certificates, reports, receipts, and more, by combining HTML/CSS layouts with dynamic JSON data.

## Overview

All templates support:

- **Nunjucks templating syntax**, variables, conditionals, loops, filters, and macros
- **Tailwind CSS via CDN**, link the CDN in your templates for utility-first styling
- **Custom formatting functions**, `formatCurrency()`, `formatDateTime()`, `sumSubtotals()`, and `numToWords()` for multicultural formatting and calculations
- **Auto calculations**, predefined templates can compute totals, taxes, and subtotals automatically
- **Smart page breaking**, PDF templates handle page breaks intelligently for multi-page documents
- **Thumbnail generation**, optionally generate image previews of your documents

## Template Types

There are two ways to use templates with cloudlayer.io: predefined templates from the gallery, or custom templates you build yourself.

### Predefined Templates

The fastest way to get started. Browse the template gallery, pick a design, and pass your data.

1. Choose a template from the [PDF Template Gallery](https://cloudlayer.io/templates/pdf/) or the [Image Template Gallery](https://cloudlayer.io/templates/image/).
2. Copy the `templateId` from the gallery page.
3. Use the sample JSON data as a starting point and customize it with your own values.

```json
{
  "templateId": "professional-invoice",
  "data": {
    "company_name": "Acme Inc.",
    "invoice_no": "INV-001",
    "items": [
      {
        "title": "Web Design",
        "quantity": 10,
        "unit_price": 150.00,
        "amount": null
      }
    ]
  }
}
```

> **Tip:** Fields set to `null` in the sample data are auto-calculated by the template. Set `"__auto_calculate": false` in your data to supply your own values instead.

### Custom Templates

For full control, write your own Nunjucks template and send it with your request. Custom templates are base64-encoded HTML strings that use Nunjucks syntax for dynamic content.

```json
{
  "template": "PGh0bWw+PGJvZHk+SGVsbG8ge3tuYW1lfX0hPC9ib2R5PjwvaHRtbD4=",
  "data": {
    "name": "Alice"
  }
}
```

The `template` value above is the base64-encoded version of:

```html
<html>
  <body>
    Hello {{name}}!
  </body>
</html>
```

## Nunjucks Template Syntax

### Variables

Use double curly braces to output dynamic values from your data.

```html
<h1>Invoice #{{invoice_no}}</h1>
<p>Bill to: {{customer.name}}</p>
<p>Email: {{customer.email}}</p>
```

Access nested properties with dot notation:

```html
<p>{{company.address.street}}</p>
<p>{{company.address.city}}, {{company.address.state}}</p>
```

### Conditionals

Use `{% if %}`, `{% elif %}`, and `{% else %}` for conditional rendering.

```html
{% if status == "paid" %}
  <span class="badge-green">Paid</span>
{% elif status == "pending" %}
  <span class="badge-yellow">Pending</span>
{% else %}
  <span class="badge-red">Overdue</span>
{% endif %}
```

You can also check for the existence of a value:

```html
{% if discount %}
  <tr>
    <td>Discount</td>
    <td>-{{formatCurrency(locale, currency, discount)}}</td>
  </tr>
{% endif %}
```

### Loops

Use `{% for %}` to iterate over arrays and objects.

**Array iteration:**

```html
<table>
  <thead>
    <tr>
      <th>Item</th>
      <th>Qty</th>
      <th>Price</th>
      <th>Amount</th>
    </tr>
  </thead>
  <tbody>
    {% for item in items %}
    <tr>
      <td>{{item.title}}</td>
      <td>{{item.quantity}}</td>
      <td>{{formatCurrency(locale, currency, item.unit_price)}}</td>
      <td>{{formatCurrency(locale, currency, item.amount)}}</td>
    </tr>
    {% endfor %}
  </tbody>
</table>
```

**Object iteration:**

```html
{% for key, value in metadata %}
  <p><strong>{{key}}:</strong> {{value}}</p>
{% endfor %}
```

**Loop variables:**

Inside a `{% for %}` block, Nunjucks provides special variables:

| Variable        | Description                               |
|-----------------|-------------------------------------------|
| `loop.index`    | Current iteration (1-based)               |
| `loop.index0`   | Current iteration (0-based)               |
| `loop.first`    | `true` if first iteration                 |
| `loop.last`     | `true` if last iteration                  |
| `loop.length`   | Total number of items                     |

```html
{% for item in items %}
  <div class="{% if loop.last %}last-item{% endif %}">
    {{loop.index}}. {{item.title}}
  </div>
{% endfor %}
```

### Filters

Nunjucks filters transform output values. Apply them with the pipe (`|`) character.

**Common built-in filters:**

| Filter         | Example                            | Result             |
|----------------|------------------------------------|--------------------|
| `upper`        | `{{ "hello" | upper }}`           | `HELLO`            |
| `lower`        | `{{ "HELLO" | lower }}`           | `hello`            |
| `capitalize`   | `{{ "hello world" | capitalize }}`| `Hello world`      |
| `title`        | `{{ "hello world" | title }}`     | `Hello World`      |
| `trim`         | `{{ "  hello  " | trim }}`        | `hello`            |
| `replace`      | `{{ "foo" | replace("o", "0") }}` | `f00`              |
| `truncate`     | `{{ text | truncate(50) }}`       | Truncated text...  |
| `round`        | `{{ 4.567 | round(2) }}`          | `4.57`             |
| `default`      | `{{ missing | default("N/A") }}`  | `N/A`              |
| `join`         | `{{ items | join(", ") }}`        | Comma-separated    |
| `length`       | `{{ items | length }}`            | Number of items    |
| `first`        | `{{ items | first }}`             | First item         |
| `last`         | `{{ items | last }}`              | Last item          |
| `safe`         | `{{ htmlContent | safe }}`        | Renders raw HTML   |

**Chaining filters:**

```html
<p>{{ description | truncate(100) | capitalize }}</p>
```

## Custom Functions

cloudlayer.io extends Nunjucks with four custom functions for formatting and calculations.

### `formatCurrency(locale, currency, amount)`

Formats a numeric amount as a localized currency string.

**Parameters:**

| Parameter  | Type     | Description                                                                                         |
|------------|----------|-----------------------------------------------------------------------------------------------------|
| `locale`   | `string` | Unicode locale identifier (e.g., `"en-US"`, `"en-GB"`, `"de-DE"`, `"ja-JP"`)                       |
| `currency` | `string` | ISO 4217 currency code (e.g., `"USD"`, `"EUR"`, `"GBP"`, `"JPY"`)                                  |
| `amount`   | `number` | The numeric amount to format                                                                         |

**Template usage:**

```html
<td>{{formatCurrency(locale, currency, item.unit_price)}}</td>
```

**Examples:**

| Locale    | Currency | Amount   | Output        |
|-----------|----------|----------|---------------|
| `en-US`   | `USD`    | `1500.50`| `$1,500.50`   |
| `en-GB`   | `EUR`    | `150.10` | `€150.10`     |
| `de-DE`   | `EUR`    | `1234.56`| `1.234,56 €`  |
| `ja-JP`   | `JPY`    | `9800`   | `¥9,800`      |
| `fr-FR`   | `EUR`    | `42.00`  | `42,00 €`     |

### `formatDateTime(locale, dateTime)`

Formats a date string into a localized date representation.

**Parameters:**

| Parameter  | Type     | Description                                                            |
|------------|----------|------------------------------------------------------------------------|
| `locale`   | `string` | Unicode locale identifier (e.g., `"en-US"`, `"fr-FR"`)                |
| `dateTime` | `string` | The string representation of the date                                  |

**Template usage:**

```html
<p>Invoice Date: {{formatDateTime(locale, invoice_date)}}</p>
```

**Examples:**

| Locale    | Input               | Output              |
|-----------|---------------------|----------------------|
| `en-US`   | `"27 March, 2020"`  | `March 27, 2020`    |
| `fr-FR`   | `"27 March, 2020"`  | `27 mars 2020`      |
| `de-DE`   | `"27 March, 2020"`  | `27. März 2020`     |
| `ja-JP`   | `"27 March, 2020"`  | `2020年3月27日`      |

### `sumSubtotals(items)`

Calculates the sum of all line item subtotals (unit price multiplied by quantity) in an array of items.

**Parameters:**

| Parameter | Type    | Description                                                                                      |
|-----------|---------|--------------------------------------------------------------------------------------------------|
| `items`   | `array` | Array of item objects, each with `unit_price` and `quantity` properties                          |

**Template usage:**

```html
<td>{{formatCurrency(locale, currency, sumSubtotals(items))}}</td>
```

Each item's subtotal is calculated as `unit_price * quantity` (quantity defaults to 1 if not provided), and all subtotals are summed together.

### `numToWords(locale, number)`

Converts a numeric value into its word representation for the given locale.

**Parameters:**

| Parameter | Type     | Description                                                            |
|-----------|----------|------------------------------------------------------------------------|
| `locale`  | `string` | Unicode locale identifier (e.g., `"en-US"`, `"fr-FR"`)                |
| `number`  | `number` | The numeric value to convert to words                                  |

**Template usage:**

```html
<p>Total in words: {{numToWords(locale, amount)}}</p>
```

**Examples:**

| Locale    | Input   | Output                              |
|-----------|---------|--------------------------------------|
| `en-US`   | `1590`  | `One Thousand Five Hundred Ninety`   |
| `en-US`   | `42`    | `Forty Two`                          |

## Smart Page Breaking for PDFs

PDF templates support intelligent page breaking so that content flows naturally across multiple pages without awkward mid-element splits.

### CSS Page Break Properties

Use standard CSS page break properties in your templates:

```css
/* Always start a new page before this element */
.section-header {
  page-break-before: always;
}

/* Avoid breaking inside this element */
.invoice-item {
  page-break-inside: avoid;
}

/* Avoid a page break right after this element */
.section-title {
  page-break-after: avoid;
}
```

### Practical Example: Multi-Page Invoice

```html
<style>
  .invoice-table tr {
    page-break-inside: avoid;
  }

  .totals-section {
    page-break-inside: avoid;
  }

  .terms-section {
    page-break-before: always;
  }
</style>

<div class="invoice-header">
  <!-- Header content -->
</div>

<table class="invoice-table">
  {% for item in items %}
  <tr>
    <td>{{item.title}}</td>
    <td>{{item.quantity}}</td>
    <td>{{formatCurrency(locale, currency, item.unit_price)}}</td>
  </tr>
  {% endfor %}
</table>

<div class="totals-section">
  <p>Subtotal: {{formatCurrency(locale, currency, subTotal)}}</p>
  <p>Tax: {{formatCurrency(locale, currency, totalTax)}}</p>
  <p>Total: {{formatCurrency(locale, currency, amount)}}</p>
</div>

<div class="terms-section">
  <h3>Terms & Conditions</h3>
  <p>{{terms}}</p>
</div>
```

## Tailwind CSS Support

All templates support Tailwind CSS. Link the CDN in your template's `<head>` to use utility classes:

```html
<html>
  <head>
    <script src="https://cdn.tailwindcss.com"></script>
  </head>
  <body>
    <div class="max-w-2xl mx-auto p-8">
      <h1 class="text-3xl font-bold text-gray-900">{{company_name}}</h1>
      <div class="mt-6 border-t pt-4">
        <div class="flex justify-between">
          <span class="text-gray-600">Invoice #</span>
          <span class="font-medium">{{invoice_no}}</span>
        </div>
      </div>
    </div>
  </body>
</html>
```

> **Tip:** Since cloudlayer.io fetches external resources during rendering, any publicly accessible CSS framework, font, or stylesheet can be used in your templates, not just Tailwind.

## Auto Calculations

Predefined templates from the gallery may support auto calculations. When enabled, the template automatically computes derived values like line item totals, subtotals, taxes, and grand totals.

### How It Works

1. **Auto-calculated fields** are represented as `null` in the sample data.
2. When `__auto_calculate` is `true` (the default), the template computes these fields.
3. Set `__auto_calculate` to `false` if you want to supply your own computed values.

```json
{
  "templateId": "professional-invoice",
  "data": {
    "__auto_calculate": true,
    "tax_percentage": 6,
    "items": [
      {
        "title": "Web Design",
        "quantity": 10,
        "unit_price": 150.00,
        "amount": null
      }
    ],
    "subTotal": null,
    "totalTax": null,
    "amount": null
  }
}
```

In this example, `amount`, `subTotal`, `totalTax`, and the item-level `amount` are all computed automatically:

- Item amount = `quantity * unit_price` = `1,500.00`
- Subtotal = sum of item amounts = `1,500.00`
- Tax = `subTotal * (tax_percentage / 100)` = `90.00`
- Total = `subTotal + totalTax` = `1,590.00`

To disable auto calculation and supply your own values:

```json
{
  "templateId": "professional-invoice",
  "data": {
    "__auto_calculate": false,
    "items": [
      {
        "title": "Web Design",
        "quantity": 10,
        "unit_price": 150.00,
        "amount": 1500.00
      }
    ],
    "subTotal": 1500.00,
    "totalTax": 90.00,
    "amount": 1590.00
  }
}
```

## Template Data Injection

Data is injected into templates via the `data` property in your API request. The data object's keys become variables available in the template.

### Inline Request

Send the template and data as part of a single JSON payload:

```json
{
  "templateId": "professional-invoice",
  "data": {
    "company_name": "Acme Inc.",
    "invoice_no": "INV-001",
    "locale": "en-US",
    "currency": "USD",
    "items": [...]
  }
}
```

### Multipart Request

For larger templates, use `multipart/form-data` to send the template as a file:

```shell
curl --request POST \
  --url https://api.cloudlayer.io/v2/template/pdf \
  --header 'Content-Type: multipart/form-data' \
  --header 'x-api-key: <YOUR-API-KEY>' \
  --form 'template=@./template.njk' \
  --form 'data=@./invoice-data.json'
```

With multipart requests, the template file does not need to be base64-encoded. The `template` field contains the Nunjucks template file and the `data` field contains the JSON data file.

## Practical Examples

### Invoice Template

A complete invoice template using Tailwind CSS, Nunjucks loops, and the custom formatting functions.

**Template (HTML):**

```html
<html>
  <head>
    <script src="https://cdn.tailwindcss.com"></script>
  </head>
  <body class="bg-white p-8 font-sans">
    <div class="max-w-3xl mx-auto">
      <!-- Header -->
      <div class="flex justify-between items-start mb-8">
        <div>
          <h1 class="text-2xl font-bold text-gray-900">{{company_name}}</h1>
          <p class="text-gray-600">{{address1}}</p>
          <p class="text-gray-600">{{city}}, {{state}} {{zip}}</p>
        </div>
        <div class="text-right">
          <h2 class="text-xl font-semibold text-blue-600">INVOICE</h2>
          <p class="text-gray-700">#{{invoice_no}}</p>
          <p class="text-gray-600">{{formatDateTime(locale, invoice_date)}}</p>
        </div>
      </div>

      <!-- Bill To -->
      <div class="mb-8 p-4 bg-gray-50 rounded">
        <h3 class="font-semibold text-gray-700 mb-2">Bill To</h3>
        <p class="font-medium">{{bill_to_fullname}}</p>
        <p class="text-gray-600">{{bill_to_address1}}</p>
        <p class="text-gray-600">
          {{bill_to_city}}, {{bill_to_state_province_region}} {{bill_to_zip_postal_code}}
        </p>
      </div>

      <!-- Line Items -->
      <table class="w-full mb-8">
        <thead>
          <tr class="border-b-2 border-gray-300">
            <th class="text-left py-2 text-gray-700">Description</th>
            <th class="text-right py-2 text-gray-700">Qty</th>
            <th class="text-right py-2 text-gray-700">Unit Price</th>
            <th class="text-right py-2 text-gray-700">Amount</th>
          </tr>
        </thead>
        <tbody>
          {% for item in items %}
          <tr class="border-b border-gray-200" style="page-break-inside: avoid;">
            <td class="py-3">
              <p class="font-medium">{{item.title}}</p>
              {% if item.description %}
              <p class="text-sm text-gray-500">{{item.description | truncate(80)}}</p>
              {% endif %}
            </td>
            <td class="text-right py-3">{{item.quantity}}</td>
            <td class="text-right py-3">
              {{formatCurrency(locale, currency, item.unit_price)}}
            </td>
            <td class="text-right py-3">
              {{formatCurrency(locale, currency, item.amount)}}
            </td>
          </tr>
          {% endfor %}
        </tbody>
      </table>

      <!-- Totals -->
      <div class="flex justify-end" style="page-break-inside: avoid;">
        <div class="w-64">
          <div class="flex justify-between py-2">
            <span class="text-gray-600">Subtotal</span>
            <span>{{formatCurrency(locale, currency, subTotal)}}</span>
          </div>
          <div class="flex justify-between py-2">
            <span class="text-gray-600">{{tax_label}} ({{tax_percentage}}%)</span>
            <span>{{formatCurrency(locale, currency, totalTax)}}</span>
          </div>
          <div class="flex justify-between py-2 border-t-2 border-gray-900 font-bold text-lg">
            <span>Total</span>
            <span>{{formatCurrency(locale, currency, amount)}}</span>
          </div>
        </div>
      </div>

      <!-- Notes -->
      {% if notes %}
      <div class="mt-8 p-4 bg-gray-50 rounded">
        <h3 class="font-semibold text-gray-700 mb-2">Notes</h3>
        <p class="text-gray-600 text-sm">{{notes}}</p>
      </div>
      {% endif %}
    </div>
  </body>
</html>
```

**Sample data:**

```json
{
  "templateId": "professional-invoice",
  "data": {
    "__auto_calculate": true,
    "locale": "en-US",
    "currency": "USD",
    "company_name": "Acme Inc.",
    "address1": "1711 Bushnell Avenue",
    "city": "South Pasadena",
    "state": "California",
    "zip": "91030",
    "invoice_no": "INV-621",
    "invoice_date": "27 March, 2020",
    "bill_to_fullname": "Terry G. Brown",
    "bill_to_address1": "1680 Ralph Drive",
    "bill_to_city": "Cleveland",
    "bill_to_state_province_region": "OH",
    "bill_to_zip_postal_code": "44114",
    "tax_label": "Tax",
    "tax_percentage": 6,
    "items": [
      {
        "title": "Homepage Design",
        "description": "Complete redesign of the main landing page",
        "quantity": 40,
        "unit_price": 150.10,
        "amount": null
      },
      {
        "title": "Backend Development",
        "description": "API development and database setup",
        "quantity": 40,
        "unit_price": 150.00,
        "amount": null
      },
      {
        "title": "QA Testing",
        "description": "End-to-end testing and bug fixes",
        "quantity": 10,
        "unit_price": 10.00,
        "amount": null
      }
    ],
    "subTotal": null,
    "totalTax": null,
    "amount": null,
    "notes": "Payment is due within 30 days. Please include the invoice number with your payment."
  }
}
```

### Certificate Template

A simple certificate of completion using custom styling.

**Template (HTML):**

```html
<html>
  <head>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=Inter&display=swap" rel="stylesheet">
  </head>
  <body class="bg-white">
    <div class="w-[800px] h-[600px] mx-auto border-8 border-double border-amber-600 p-12 flex flex-col items-center justify-center text-center">

      <p class="text-amber-600 uppercase tracking-widest text-sm mb-4">Certificate of Completion</p>

      <h1 class="text-4xl font-bold text-gray-900 mb-2" style="font-family: 'Playfair Display', serif;">
        {{recipient_name}}
      </h1>

      <div class="w-24 h-0.5 bg-amber-600 my-4"></div>

      <p class="text-gray-600 text-lg mb-2">
        has successfully completed the course
      </p>

      <h2 class="text-2xl font-semibold text-gray-800 mb-6">
        {{course_name}}
      </h2>

      <p class="text-gray-500 mb-8">
        Awarded on {{formatDateTime(locale, completion_date)}}
      </p>

      <div class="flex justify-between w-full mt-auto px-12">
        <div class="text-center">
          <div class="border-t border-gray-400 pt-2 px-8">
            <p class="font-medium">{{instructor_name}}</p>
            <p class="text-gray-500 text-sm">Instructor</p>
          </div>
        </div>
        <div class="text-center">
          <div class="border-t border-gray-400 pt-2 px-8">
            <p class="font-medium">{{organization}}</p>
            <p class="text-gray-500 text-sm">Organization</p>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>
```

**Sample data:**

```json
{
  "template": "<base64-encoded-template>",
  "data": {
    "locale": "en-US",
    "recipient_name": "Jane Smith",
    "course_name": "Advanced Web Development",
    "completion_date": "15 January, 2024",
    "instructor_name": "Dr. Alex Johnson",
    "organization": "Tech Academy"
  }
}
```

## Request Types

### Inline Request

Send everything as a single JSON payload. The template must be base64-encoded.

```shell
curl --request POST \
  --url https://api.cloudlayer.io/v2/template/pdf \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: <YOUR-API-KEY>' \
  --data '{
    "templateId": "professional-invoice",
    "data": { "company_name": "Acme Inc.", ... }
  }'
```

### Multipart Request

Send the template and data as separate files. The template does not need to be base64-encoded.

```shell
curl --request POST \
  --url https://api.cloudlayer.io/v2/template/pdf \
  --header 'Content-Type: multipart/form-data' \
  --header 'x-api-key: <YOUR-API-KEY>' \
  --form 'template=@./my-template.njk' \
  --form 'data=@./my-data.json'
```

## Tips and Best Practices

- **Use predefined templates** when possible, they are professionally designed and tested for print quality.
- **Test with small data sets** first, then scale up to production data volumes.
- **Use `page-break-inside: avoid`** on table rows and key sections to prevent awkward splits in PDFs.
- **Keep images external** (hosted URLs) when possible for smaller payload sizes, or embed as data URIs for fully self-contained documents.
- **Use `formatCurrency` and `formatDateTime`** instead of manual formatting to ensure correct localization across all locales.
- **Set margins to `0`** when using templates (this is the default for template-based generation) and control spacing with CSS padding instead.
- **Use the `safe` filter** (`{{ htmlContent | safe }}`) when you need to render raw HTML stored in your data, but only with trusted content.

### Webhooks & Async Processing

Source: https://cloudlayer.io/docs/webhooks/

# Webhooks & Async Processing

cloudlayer.io supports both synchronous and asynchronous processing modes. Understanding when to use each, and how to configure webhooks for async results, is key to building reliable document generation workflows.

## Sync vs Async Processing

### Synchronous Mode (`async: false`)

In synchronous mode, the API waits for the document to be generated and returns the result directly in the HTTP response.

```json
{
  "url": "https://example.com",
  "async": false
}
```

**Response (v2):**

```json
{
  "id": "1705312200000",
  "status": "success",
  "assetUrl": "https://storage.cloudlayer.io/assets/abc123/document.pdf",
  "timestamp": 1705312200000
}
```

> **Note:** v1 synchronous requests return the raw binary file (PDF, image, etc.) directly in the response body, not JSON. The JSON response shown above applies only to v2.

**Characteristics:**

- The HTTP connection stays open until the document is ready
- The `assetUrl` is populated in the response
- Simpler to implement, no webhook setup needed
- Subject to connection timeouts for long-running jobs

### Asynchronous Mode (`async: true`, default)

In asynchronous mode, the API immediately returns a job ID and processes the document in the background. When complete, the result is delivered to the `webhook` URL you provide in the request.

```json
{
  "url": "https://example.com",
  "async": true,
  "webhook": "https://yourserver.com/webhooks/cloudlayer"
}
```

**Immediate response:**

```json
{
  "status": "pending",
  "id": "1705312200000"
}
```

**Webhook delivery (when complete):**

```json
{
  "id": "1705312200000",
  "status": "success",
  "type": "url-pdf",
  "assetUrl": "https://storage.cloudlayer.io/assets/abc123/document.pdf",
  "previewUrl": "https://storage.cloudlayer.io/assets/abc123/document-preview.webp",
  "processTime": 2340,
  "size": 184320,
  "timestamp": 1705312200000
}
```

> **Note:** The webhook payload includes the full job object. Additional internal fields (cost data, worker info, etc.) may also be present but are not part of the public API contract.

**Characteristics:**

- The HTTP connection closes immediately after queuing
- No risk of connection timeouts
- Results delivered via webhook callback
- Better for long-running and batch operations

## When to Use Each Mode

| Scenario                                  | Recommended Mode | Reason                                       |
|-------------------------------------------|------------------|----------------------------------------------|
| Simple, fast conversions (single page)    | Sync             | Simpler implementation, immediate result     |
| User-facing "download PDF" button         | Sync             | User expects immediate response              |
| Batch processing (multiple documents)     | Async            | Avoids timeouts, handles volume              |
| Large or complex documents                | Async            | May exceed sync timeout limits               |
| Background/scheduled generation           | Async            | No user waiting for the result               |
| High-volume automated pipelines           | Async            | Better throughput, reliable delivery         |
| Pages with heavy JavaScript rendering     | Async            | Render time may be unpredictable             |

## Webhook Configuration

### Providing Your Webhook URL

In v2 of the API, you provide a webhook URL directly in each request using the `webhook` parameter. When the async job completes, cloudlayer.io sends the result to that URL.

```json
{
  "url": "https://example.com",
  "async": true,
  "webhook": "https://yourserver.com/webhooks/cloudlayer"
}
```

The `webhook` URL must use HTTPS.

### Webhook Endpoint Requirements

Your endpoint must:

1. Accept HTTP POST requests
2. Return a `200` status code to acknowledge receipt
3. Process the webhook payload

### Webhook Payload

When a job completes, cloudlayer.io sends a POST request to your webhook URL with a JSON body:

**Success payload:**

```json
{
  "id": "1705312200000",
  "status": "success",
  "type": "url-pdf",
  "assetUrl": "https://storage.cloudlayer.io/assets/abc123/document.pdf",
  "previewUrl": "https://storage.cloudlayer.io/assets/abc123/document-preview.webp",
  "processTime": 2340,
  "size": 184320,
  "timestamp": 1705312200000
}
```

**Error payload:**

```json
{
  "id": "1705312200000",
  "status": "error",
  "type": "url-pdf",
  "error": "Timeout: page took longer than 30000ms to load",
  "timestamp": 1705312205000
}
```

### Payload Fields

| Field         | Type     | Description                                                       |
|---------------|----------|-------------------------------------------------------------------|
| `id`          | `string` | Unique identifier for the generation job                          |
| `status`      | `string` | Job outcome: `success` or `error`                                 |
| `type`        | `string` | The job type (e.g., `url-pdf`, `html-image`, `template-pdf`)     |
| `assetUrl`    | `string` | URL to the generated document (on success)                        |
| `previewUrl`  | `string` | URL to the preview thumbnail, if `generatePreview` was used       |
| `processTime` | `number` | Processing time in milliseconds                                   |
| `size`        | `number` | File size of the generated document in bytes                      |
| `error`       | `string` | Error description (on failure)                                    |
| `timestamp`   | `number` | Unix epoch timestamp in milliseconds of when the job completed    |

### Example Webhook Handler

#### Node.js (Express)

```javascript
app.post("/webhooks/cloudlayer", (req, res) => {
  const { id, status, assetUrl, error } = req.body;

  if (status === "success") {
    console.log(`Job ${id} completed: ${assetUrl}`);
    // Process the generated document -- download it, email it, etc.
  } else {
    console.error(`Job ${id} failed: ${error}`);
    // Handle the error -- retry, notify, log, etc.
  }

  // Always return 200 to acknowledge receipt
  res.status(200).send("OK");
});
```

#### Python (Flask)

```python
from flask import Flask, request

app = Flask(__name__)


@app.route("/webhooks/cloudlayer", methods=["POST"])
def handle_webhook():
    payload = request.get_json()
    job_id = payload["id"]
    status = payload["status"]

    if status == "success":
        asset_url = payload["assetUrl"]
        print(f"Job {job_id} completed: {asset_url}")
        # Process the generated document
    else:
        error = payload.get("error", "Unknown error")
        print(f"Job {job_id} failed: {error}")
        # Handle the error

    return "OK", 200
```

## Webhook Delivery

cloudlayer.io makes a **single delivery attempt** to your webhook URL. There are no automatic retries. If your endpoint is unreachable or returns a non-`200` status code, the webhook delivery fails silently, the job status is **not affected**. The job remains `success` or `error` based on the generation result.

If webhook delivery fails, you can still retrieve the job result via the [Jobs API](/docs/jobs/).

### Ensuring Reliable Delivery

- **Return `200` quickly.** Process the webhook payload asynchronously (e.g., push to a queue) rather than doing heavy work inside the handler.
- **Make your handler idempotent.** Use the `id` to deduplicate in case of unexpected duplicate deliveries.
- **Implement your own fallback.** Since there are no automatic retries, consider polling the Jobs API as a backup if webhook delivery is critical to your workflow.
- **Log all webhook deliveries** for debugging and auditing.

## Best Practices

### Use Async for Production Pipelines

Synchronous mode is convenient for development and simple use cases, but async with webhooks is more robust for production:

- No risk of HTTP timeouts cutting off long-running jobs
- Your application does not need to hold open connections
- Better scalability for high-volume generation

### Correlate Jobs with Your Data

Pass a `projectId` or use a naming convention in your `filename` to correlate generated documents with your internal records:

```json
{
  "url": "https://example.com/invoice/INV-001",
  "filename": "invoice-INV-001.pdf",
  "projectId": "N77VCoAVTHHwgmUfCYlD",
  "async": true
}
```

When the webhook fires, use the `id` to look up which internal record triggered the generation.

### Handle Errors Gracefully

Your webhook handler should account for:

- **Timeout errors**, the page took too long to render. Consider increasing the `timeout` parameter or simplifying the page.
- **Navigation errors**, the URL was unreachable. Verify the URL is accessible and correctly spelled.
- **Rendering errors**, the page had JavaScript errors or missing resources. Test the page in a browser first.

### Monitor Webhook Health

- Set up alerts for webhook delivery failures
- Track success/failure ratios over time
- Use a service like [Hookdeck](https://hookdeck.com) or [ngrok](https://ngrok.com) for local webhook testing during development

### Privacy & Data Handling

Source: https://cloudlayer.io/docs/privacy/

# Privacy & Data Handling

This guide explains the product controls that affect data handling. It is technical documentation,
not a substitute for the [Privacy Notice](/privacy/) or [Data Processing Addendum](/dpa/).
Cloudlayer’s [Subprocessor List](/subprocessors/) identifies providers, data categories, roles, and
processing locations. The [Security](/security/) page records the security posture without claiming
an unverified certification.

## Roles and responsibility

Cloudlayer acts as a processor or service provider when it handles content for your configured
workflow. You decide what to capture, upload, collect, generate, sign, deliver, and retain, and you
remain responsible for the notices, rights, permissions, lawful basis, and data minimization that
your use requires.

Cloudlayer separately acts as controller for account administration, billing, security, support,
and its own website and service operations. The Privacy Notice explains that distinction and how a
person can exercise a privacy right.

## Data paths

A Cloudlayer workflow can involve several data classes:

| Data                | Examples                                                                                                                     | Control point                                                                                       |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Source content      | HTML, CSS, a URL, cookies, headers, template variables, uploaded files, or imported documents                                | Send only content you are authorized to use; avoid unnecessary personal data and secrets            |
| Workflow records    | Job state, artifact references, form definitions, submissions, signature events, delivery attempts, audit events, and errors | Workspace roles, tenant scope, retention, export, deletion, and audit controls                      |
| Output              | PDF, image, video, captured page, completed form document, or signed record                                                  | Cloudlayer storage, customer storage, synchronous return, recipient delivery, and configured expiry |
| Destinations        | Customer S3-compatible storage, Google Cloud Storage, webhook, email recipient, or integration                               | You choose the provider, account, endpoint, region, permissions, and transfer basis                 |
| AI input and output | Prompt, selected source content, instructions, suggestion, and feedback                                                      | An AI feature sends only the selected material to the disclosed model provider; review all output   |

Customer-directed storage and webhooks are not Cloudlayer-appointed subprocessors. They receive data
only after you configure the destination. Test destination permissions with non-sensitive data before
using it in production.

## Storage choices

Choose a storage mode according to the workflow and your retention obligations.

| Mode                          | Behavior                                                                                                                                             | Use when                                                                                                |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Cloudlayer-managed storage    | Stores the artifact under the workspace’s access and retention controls                                                                              | The artifact must remain available in Cloudlayer for download, delivery, audit, or later workflow steps |
| Customer-directed storage     | Sends the artifact to a configured storage provider and region                                                                                       | Your organization manages the destination, lifecycle, access, and residency decision                    |
| Secure or zero-retention mode | Returns or delivers the result without making it a Cloudlayer-durable artifact; bounded operational records and temporary processing can still exist | The workflow does not need a retained Cloudlayer artifact and the selected API path supports the mode   |

For a synchronous request that supports it, `storage: false` asks Cloudlayer not to retain the
generated output as a managed asset:

```json
{
  "async": false,
  "storage": false
}
```

This setting does not mean that no data is processed. Cloudlayer still has to admit, secure, render,
meter, and return the job. Bounded job, security, audit, billing, failure, and legal records can remain
under their own retention rules. Temporary payloads remain only through the processing and recovery
horizon and are then removed.

For a customer-directed destination, reference the saved configuration rather than sending storage
credentials in each job:

```json
{
  "storage": {
    "id": "your-storage-config-id"
  }
}
```

The storage adapter supports S3-compatible destinations and Google Cloud Storage where configured.
Availability and limits follow your plan and order. A custom endpoint must pass the same destination,
network, timeout, and secret-redaction checks as a named provider.

## Synchronous and asynchronous processing

Synchronous work holds the request open until the result or terminal error is returned. Asynchronous
work records an accepted job, processes it through the queue, and exposes status and delivery results.
Retries use the same logical job and artifact identity so a retry does not create an additional billed
result.

Both paths can create operational metadata. Output storage is a separate choice from job history,
audit evidence, billing records, and security logs. A webhook receives only the bounded completion or
failure contract sent to the endpoint you configured.

## Secrets and capture credentials

URLs, proxy credentials, basic-auth values, cookies, headers, API keys, storage credentials, and
webhook secrets require different handling from ordinary template variables.

- Use saved encrypted configurations or secret controls where the product provides them.
- Grant only the permissions and lifetime needed for the job or destination.
- Do not place private keys or account passwords in HTML, template data, prompts, filenames, or
  support messages.
- Rotate a credential after suspected disclosure and revoke the affected API key or destination.
- Treat a webpage capture as access by your organization. Supplying a cookie or credential does not
  establish the legal right to use it.

Cloudlayer redacts secret fields from customer-visible history, errors, and application logs where
those fields can appear. Cloudlayer does not promise that arbitrary personal data embedded in free
text can always be recognized. Do not include a secret in an unrestricted text field.

## Access and tenant controls

Data belongs to an account, organization, or workspace scope. Membership and role checks apply to
templates, forms, submissions, contacts, artifacts, storage configurations, signatures, workflows,
and audit history. Creator attribution does not replace tenant authorization.

Use separate API keys for separate systems and environments. Give service identities the minimum
workspace and operation scope. Remove members and revoke keys when access is no longer required.
Public forms, recipient links, and signing sessions use purpose-bound access rather than exposing a
workspace credential.

## Forms and signatures

A form owner controls questions, prefills, attachments, respondents, consent text, and retention.
Collect only what the stated workflow needs. Do not put confidential values in a prefilled URL. Use
authentication and save/resume controls that fit the sensitivity of the response.

Signature event history records the actions needed for workflow and audit evidence. That evidence
does not decide whether a document or authentication method satisfies the law for a particular
transaction. The [Terms of Service](/terms/) explain the customer’s responsibility for electronic
consent, signer authority, record delivery, and transaction-specific formalities.

## AI features

An AI feature sends the prompt and source material selected for that operation to the model provider
identified in the Subprocessor List. Do not submit sensitive or regulated data unless the feature and
your order expressly permit it. Check facts, calculations, rights, bias, accessibility, and legal or
technical requirements before using the output. See the [AI Additional Terms](/ai-terms/).

## Retention, export, and deletion

Retention depends on the data class, selected storage mode, workspace policy, plan, legal hold, and
configured workflow. Product controls expose expiry, deletion, trash or recovery, and export where
applicable. Customer-directed output follows the destination’s policy after delivery.

Account cancellation stops renewal but does not silently destroy Customer Data. Account deletion,
workspace deletion, individual resource deletion, and retention expiry are distinct operations. Some
billing, security, consent, dispute, backup, and audit records can remain for a limited legal or
recovery purpose with use restricted to that purpose.

Plan your lifecycle before collecting data:

1. classify the source, respondent, signer, and output data;
2. select the minimum storage and history needed;
3. configure workspace access and customer destinations;
4. test export, deletion, destination failure, and credential rotation; and
5. document how your organization answers access, correction, deletion, and objection requests.

## Regulated data

Cloudlayer is not enabled for protected health information or electronic protected health
information. Do not send PHI or ePHI unless Cloudlayer has approved an eligible service scope in
writing and both parties have executed the required business associate agreement.

Do not send payment-card data through templates, forms, captures, or ordinary API fields. Payment
entry belongs in Stripe-hosted controls. Other sensitive data requires the relevant product support,
contract, legal basis, and safeguards.


## SDKs

### SDKs Overview

Source: https://cloudlayer.io/docs/sdks/

# PDF Generation SDKs

cloudlayer.io provides official PDF generation SDKs for .NET (C# and F#), Go, Java, JavaScript/TypeScript, PHP, Python, and Ruby. Convert HTML to PDF, capture URLs as documents, and render templates from your application. Since all document generation is driven by a REST API, you can also integrate directly from any language that supports HTTP requests.

## Official SDKs

| Language | Package | Source |
|----------|---------|--------|
| [.NET C#](/docs/sdk-dotnet/) | [NuGet](https://www.nuget.org/packages/cloudlayerio-dotnet/) | [GitHub](https://github.com/cloudlayerio/cloudlayerio-dotnet) |
| [Go](/docs/sdk-go/) | [pkg.go.dev](https://pkg.go.dev/github.com/cloudlayerio/cloudlayerio-go) | [GitHub](https://github.com/cloudlayerio/cloudlayerio-go) |
| [Java](/docs/sdk-java/) | [Maven Central](https://central.sonatype.com/artifact/io.cloudlayer/cloudlayerio-java) | [GitHub](https://github.com/cloudlayerio/cloudlayerio-java) |
| [JavaScript / TypeScript](/docs/sdk-javascript/) | [npm](https://www.npmjs.com/package/@cloudlayerio/sdk) | [GitHub](https://github.com/cloudlayerio/cloudlayerio-js) |
| [PHP](/docs/sdk-php/) | [Packagist](https://packagist.org/packages/cloudlayerio/cloudlayerio-php) | [GitHub](https://github.com/cloudlayerio/cloudlayerio-php) |
| [Python](/docs/sdk-python/) | [PyPI](https://pypi.org/project/cloudlayerio/) | [GitHub](https://github.com/cloudlayerio/cloudlayerio-python) |
| [Ruby](/docs/sdk-ruby/) | [RubyGems](https://rubygems.org/gems/cloudlayerio) | [GitHub](https://github.com/cloudlayerio/cloudlayerio-ruby) |
| .NET F# | [NuGet](https://www.nuget.org/packages/Cloudlayer.FSharp/) | [GitHub](https://github.com/cloudlayerio/cloudlayerio-fsharp) |

## Clients in development

Rust and Swift clients exist as source on [GitHub](https://github.com/cloudlayerio) and are not yet published to a package manager: [Rust](https://github.com/cloudlayerio/rust) and [Swift](https://github.com/cloudlayerio/swift). Until they ship, call the REST API directly from those languages.

## Using the REST API Directly

The cloudlayer.io REST API can be called from any language. All you need is:

1. Your API key (passed via the `X-API-Key` header)
2. The ability to make HTTP POST requests with JSON bodies
3. Base URL: `https://api.cloudlayer.io/v2`

### Quick Reference: API Endpoints

| Endpoint              | Description                            |
|-----------------------|----------------------------------------|
| `POST /url/pdf`       | Convert a URL to PDF                   |
| `POST /url/image`     | Convert a URL to an image              |
| `POST /html/pdf`      | Convert HTML to PDF                    |
| `POST /html/image`    | Convert HTML to an image               |
| `POST /template/pdf`  | Generate a PDF from a template         |
| `POST /template/image`| Generate an image from a template      |
| `GET /url/pdf`        | Simple URL to PDF (query params only)  |
| `GET /url/image`      | Simple URL to image (query params only) |

### cURL Example

```shell
curl --request POST \
  --url https://api.cloudlayer.io/v2/url/pdf \
  --header 'Content-Type: application/json' \
  --header 'X-API-Key: <YOUR-API-KEY>' \
  --data '{
    "url": "https://example.com",
    "async": false
  }'
```

### JavaScript (Node.js / Bun / Deno)

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/url/pdf", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": "<YOUR-API-KEY>",
  },
  body: JSON.stringify({
    url: "https://example.com",
    async: false,
  }),
});

const result = await response.json();
console.log(result.assetUrl);
```

### Python

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v2/url/pdf",
    headers={
        "Content-Type": "application/json",
        "X-API-Key": "<YOUR-API-KEY>",
    },
    json={
        "url": "https://example.com",
        "async": False,
    },
)

result = response.json()
print(result["assetUrl"])
```

> **Tip:** Even if your language does not have a dedicated SDK, the REST API is straightforward to use. See the [Authentication Examples](/docs/authentication-examples/), [HTML Examples](/docs/html-examples/), and [URL Examples](/docs/url-examples/) for more language-specific code samples.

### .NET C# SDK

Source: https://cloudlayer.io/docs/sdk-dotnet/

# .NET C# PDF Generation SDK

The official `cloudlayerio-dotnet` package provides C# PDF generation for .NET 6+ applications. Convert HTML to PDF, capture URLs as documents, and render templates — available on NuGet with full IntelliSense and typed request/response models.

- **NuGet Package:** [cloudlayerio-dotnet](https://www.nuget.org/packages/cloudlayerio-dotnet/)
- **Source Code:** [github.com/cloudlayerio/cloudlayerio-dotnet](https://github.com/cloudlayerio/cloudlayerio-dotnet)

## Installation

### Visual Studio

Using the Package Manager Console:

```powershell
Install-Package cloudlayerio-dotnet
```

Or search for `cloudlayerio-dotnet` in the [NuGet Package Manager](https://docs.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio).

### JetBrains Rider

See [JetBrains NuGet documentation](https://www.jetbrains.com/help/rider/Using_NuGet.html).

### .NET CLI

```shell
dotnet add package cloudlayerio-dotnet
```

## Setup

Get your API key by creating a free account at [cloudlayer.io](https://beta-app.cloudlayer.io/auth/sign-up). The free account includes a monthly render allowance, with no card. See [pricing](https://cloudlayer.io/pricing/) for the current figures.

Initialize the manager with your API key:

```csharp
var manager = new CloudlayerioManager("<YOUR-API-KEY>");
```

## URL to PDF

```csharp
var rsp = await manager.UrlToPdf(new UrlToPdf
{
    Url = "https://example.com",
    Async = false
});

// Save the JSON response to the filesystem
await rsp.SaveToFileSystem("C:\\output\\example.json");

// Access the asset URL (available when async is false)
var url = rsp.Response.AssetUrl;
```

## URL to Image

```csharp
var rsp = await manager.UrlToImage(new UrlToImage
{
    Url = "https://example.com",
    Async = false,
    AutoScroll = true,
    ViewPort = new ViewPort
    {
        Width = 1440,
        Height = 900,
        DeviceScaleFactor = 2
    }
});

var url = rsp.Response.AssetUrl;
```

## HTML to PDF

```csharp
// HTML must be base64-encoded
var html = Convert.ToBase64String(
    Encoding.UTF8.GetBytes("<html><body><h1>Hello!</h1></body></html>")
);

var rsp = await manager.HtmlToPdf(new HtmlToPdf
{
    Html = html
});

await rsp.SaveToFileSystem("C:\\output\\hello.json");
```

## Template to PDF

```csharp
var rsp = await manager.TemplateToPdf(new TemplateToPdf
{
    TemplateId = "professional-invoice",
    Data = new Dictionary<string, object>
    {
        ["company_name"] = "Acme Inc.",
        ["invoice_no"] = "INV-001",
        ["locale"] = "en-US",
        ["currency"] = "USD",
        ["items"] = new[]
        {
            new Dictionary<string, object>
            {
                ["title"] = "Web Design",
                ["quantity"] = 10,
                ["unit_price"] = 150.00,
                ["amount"] = null
            }
        }
    }
});

await rsp.SaveToFileSystem("C:\\output\\invoice.json");
```

## Synchronous vs Asynchronous

By default, requests are processed asynchronously. The response is delivered to your configured webhook. To get the result immediately in the response, set `Async = false`:

```csharp
var rsp = await manager.UrlToPdf(new UrlToPdf
{
    Url = "https://example.com",
    Async = false
});

// AssetUrl is populated in the synchronous response
var url = rsp.Response.AssetUrl;
```

> **Note:** For long-running requests (large documents, batch processing), use the default async mode with webhooks to avoid connection timeouts.

## Advanced Options

The SDK provides typed classes for all API options, with full IntelliSense support.

### Custom Margins

```csharp
var rsp = await manager.UrlToPdf(new UrlToPdf
{
    Url = "https://example.com",
    Margin = new Margin
    {
        Top = new LayoutDimension(UnitTypes.Pixels, 100),
        Bottom = new LayoutDimension(UnitTypes.Pixels, 100),
        Left = new LayoutDimension(UnitTypes.Pixels, 50),
        Right = new LayoutDimension(UnitTypes.Pixels, 50)
    }
});
```

### Header and Footer Templates

```csharp
var rsp = await manager.UrlToPdf(new UrlToPdf
{
    Url = "https://example.com",
    Margin = new Margin
    {
        Top = new LayoutDimension(UnitTypes.Pixels, 120),
        Bottom = new LayoutDimension(UnitTypes.Pixels, 80)
    },
    HeaderTemplate = new HeaderFooterTemplate
    {
        Method = "extract",
        Selector = ".page-header",
        Style = new Dictionary<string, string>
        {
            ["width"] = "100%",
            ["text-align"] = "center"
        }
    },
    FooterTemplate = new HeaderFooterTemplate
    {
        Selector = ".page-footer",
        Style = new Dictionary<string, string>
        {
            ["width"] = "100%",
            ["font-size"] = "10px",
            ["text-align"] = "center"
        }
    }
});
```

### Viewport Configuration

```csharp
var rsp = await manager.UrlToImage(new UrlToImage
{
    Url = "https://example.com",
    ViewPort = new ViewPort
    {
        Width = 1920,
        Height = 1080,
        DeviceScaleFactor = 2,
        IsMobile = false,
        IsLandscape = true
    }
});
```

## Response Format

As of v2, the API returns a JSON response rather than binary content. The `SaveToFileSystem` helper saves this JSON response to disk. The response object is fully typed with properties including:

| Property      | Type     | Description                                              |
|---------------|----------|----------------------------------------------------------|
| `AssetUrl`    | `string` | URL to the generated asset (populated for sync calls)    |
| `Status`      | `string` | Job status (`success`, `error`, etc.)                    |
| `JobId`       | `string` | Unique identifier for the generation job                 |

For the full set of response properties and request options, see the [source code](https://github.com/cloudlayerio/cloudlayerio-dotnet/tree/main/cloudlayerio-dotnet).

### JavaScript SDK

Source: https://cloudlayer.io/docs/sdk-javascript/

# JavaScript PDF Generation SDK

The official `@cloudlayerio/sdk` provides JavaScript PDF generation for Node.js, Bun, and browser environments. Convert HTML to PDF, capture URLs as documents, and render templates — with full TypeScript type safety, zero runtime dependencies, and dual ESM/CJS support.

- **npm Package:** [@cloudlayerio/sdk](https://www.npmjs.com/package/@cloudlayerio/sdk)
- **Source Code:** [github.com/cloudlayerio/cloudlayerio-js](https://github.com/cloudlayerio/cloudlayerio-js)

## Installation

```bash
npm install @cloudlayerio/sdk
```

```bash
yarn add @cloudlayerio/sdk
```

```bash
pnpm add @cloudlayerio/sdk
```

**Requirements:** Node.js 20+ (uses native `fetch`) or any modern browser.

## Setup

Get your API key by creating a free account at [cloudlayer.io](https://beta-app.cloudlayer.io/auth/sign-up). The free account includes a monthly render allowance, with no card. See [pricing](https://cloudlayer.io/pricing/) for the current figures.

```typescript
import { CloudLayer } from "@cloudlayerio/sdk";

const client = new CloudLayer({
  apiKey: "<YOUR-API-KEY>",
  apiVersion: "v2",
});
```

The SDK requires you to choose an API version:

| | v1 | v2 |
|---|---|---|
| **Default mode** | Synchronous (returns binary) | Asynchronous (returns Job) |
| **Sync response** | Raw binary (PDF/image) | JSON Job object |
| **Binary access** | Direct from response | Via `downloadJobResult()` |

## URL to PDF

### v2 (recommended)

v2 returns a Job object. Use `downloadJobResult()` to get the binary:

```typescript
const { data: job } = await client.urlToPdf({
  url: "https://example.com",
  format: "a4",
  margin: { top: "20px", bottom: "20px" },
});

// Download the PDF binary
const pdfBuffer = await client.downloadJobResult(job);
```

### v1 (legacy)

v1 returns the binary directly:

```typescript
const v1 = new CloudLayer({ apiKey: "<YOUR-API-KEY>", apiVersion: "v1" });

const { data: pdfBuffer } = await v1.urlToPdf({
  url: "https://example.com",
});
```

## URL to Image

```typescript
const { data } = await client.urlToImage({
  url: "https://example.com",
  imageType: "png",
  quality: 90,
});
```

## HTML to PDF

HTML content must be Base64-encoded:

```typescript
const html = btoa("<html><body><h1>Hello World</h1></body></html>");

const { data: job } = await client.htmlToPdf({ html });
```

## HTML to Image

```typescript
const html = btoa("<html><body><h1>Screenshot</h1></body></html>");

const { data } = await client.htmlToImage({
  html,
  imageType: "webp",
  quality: 85,
});
```

## Template to PDF

```typescript
const { data: job } = await client.templateToPdf({
  templateId: "professional-invoice",
  data: {
    company_name: "Acme Inc.",
    invoice_no: "INV-001",
    items: [{ title: "Web Design", quantity: 10, unit_price: 150.0 }],
  },
});
```

## PDF Merge

```typescript
const { data: job } = await client.mergePdfs({
  batch: {
    urls: [
      "https://example.com/page1.pdf",
      "https://example.com/page2.pdf",
    ],
  },
});
```

## Batch Processing

Process up to 20 URLs in one request (always async):

```typescript
const { data: job } = await client.urlToPdf({
  batch: {
    urls: ["https://a.com", "https://b.com", "https://c.com"],
  },
  storage: true,
});
```

## Async Mode & Job Polling

```typescript
const { data: job } = await client.urlToPdf({
  url: "https://example.com",
  storage: true,
});

// Poll until complete (5s interval, 5 min timeout)
const completed = await client.waitForJob(job.id);

// Download the result
const pdf = await client.downloadJobResult(completed);
```

## Data Management

```typescript
// Jobs
const jobs = await client.listJobs();
const job = await client.getJob("job-id");

// Assets
const assets = await client.listAssets();
const asset = await client.getAsset("asset-id");

// Storage
const storages = await client.listStorage();
await client.addStorage({
  title: "My S3",
  region: "us-east-1",
  accessKeyId: "AKIA...",
  secretAccessKey: "...",
  bucket: "my-bucket",
});

// Account
const account = await client.getAccount();
const status = await client.getStatus();
```

## Error Handling

```typescript
import {
  CloudLayerAuthError,
  CloudLayerRateLimitError,
  CloudLayerApiError,
  CloudLayerTimeoutError,
  CloudLayerValidationError,
} from "@cloudlayerio/sdk";

try {
  await client.urlToPdf({ url: "https://example.com" });
} catch (error) {
  if (error instanceof CloudLayerAuthError) {
    console.error("Invalid API key");
  } else if (error instanceof CloudLayerRateLimitError) {
    console.error(`Rate limited. Retry after ${error.retryAfter}s`);
  } else if (error instanceof CloudLayerApiError) {
    console.error(`API error ${error.status}: ${error.message}`);
  } else if (error instanceof CloudLayerTimeoutError) {
    console.error(`Timed out after ${error.timeout}ms`);
  } else if (error instanceof CloudLayerValidationError) {
    console.error(`Invalid input: ${error.field}`);
  }
}
```

## Advanced Options

### Viewport & Puppeteer

```typescript
const { data } = await client.urlToPdf({
  url: "https://example.com",
  viewPort: {
    width: 1920,
    height: 1080,
    deviceScaleFactor: 2,
    isMobile: false,
    hasTouch: false,
    isLandscape: false,
  },
  waitUntil: "networkidle0",
  cookies: [{ name: "session", value: "abc123", domain: "example.com" }],
  authentication: { username: "user", password: "pass" },
});
```

### Custom Margins & Headers/Footers

```typescript
const { data } = await client.urlToPdf({
  url: "https://example.com",
  format: "letter",
  margin: { top: "1in", bottom: "1in", left: "0.5in", right: "0.5in" },
  printBackground: true,
  headerTemplate: {
    method: "extract",
    selector: ".page-header",
  },
  footerTemplate: {
    method: "template",
    template: "<div style='font-size:10px;text-align:center;width:100%'>Page <span class='pageNumber'></span></div>",
  },
});
```

## TypeScript

Full type safety for all request options and responses:

```typescript
import type {
  UrlToPdfOptions,
  Job,
  AccountInfo,
  CloudLayerResponseHeaders,
} from "@cloudlayerio/sdk";
```

### Python SDK

Source: https://cloudlayer.io/docs/sdk-python/

# Python PDF Generation SDK

The official `cloudlayerio` package provides Python PDF generation with sync and async support. Convert HTML to PDF, capture URLs as documents, and render templates — with full type safety (mypy strict) and a single runtime dependency (`httpx`).

- **PyPI Package:** [cloudlayerio](https://pypi.org/project/cloudlayerio/)
- **Source Code:** [github.com/cloudlayerio/cloudlayerio-python](https://github.com/cloudlayerio/cloudlayerio-python)

## Installation

```bash
pip install cloudlayerio
```

**Requirements:** Python 3.9+

## Setup

Get your API key by creating a free account at [cloudlayer.io](https://beta-app.cloudlayer.io/auth/sign-up). The free account includes a monthly render allowance, with no card. See [pricing](https://cloudlayer.io/pricing/) for the current figures.

```python
from cloudlayerio import CloudLayer

client = CloudLayer("YOUR-API-KEY", api_version="v2")
```

The SDK requires you to choose an API version:

| | v1 | v2 |
|---|---|---|
| **Default mode** | Synchronous (returns binary) | Asynchronous (returns Job) |
| **Sync response** | Raw binary (PDF/image bytes) | JSON Job object |
| **Binary access** | Direct from `result.data` | Via `download_job_result()` |

## URL to PDF

### v2 (recommended)

v2 returns a Job object. Use `download_job_result()` to get the binary:

```python
with CloudLayer("YOUR-API-KEY", api_version="v2") as client:
    result = client.url_to_pdf({"url": "https://example.com"})
    completed = client.wait_for_job(result.data.id)
    pdf_bytes = client.download_job_result(completed)

    with open("output.pdf", "wb") as f:
        f.write(pdf_bytes)
```

### v1 (direct binary)

v1 returns the binary directly:

```python
with CloudLayer("YOUR-API-KEY", api_version="v1") as client:
    result = client.url_to_pdf({"url": "https://example.com"})

    with open("output.pdf", "wb") as f:
        f.write(result.data)  # bytes
```

## HTML to PDF

```python
import base64

html = base64.b64encode(b"<h1>Hello World</h1>").decode()

result = client.html_to_pdf({"html": html})
```

## Template Rendering

```python
result = client.template_to_pdf(
    {
        "template_id": "your-template-id",
        "data": {"name": "John", "total": "$100"},
    }
)
```

## Async Client

```python
import asyncio
from cloudlayerio import AsyncCloudLayer


async def main():
    async with AsyncCloudLayer("YOUR-API-KEY", api_version="v2") as client:
        result = await client.url_to_pdf({"url": "https://example.com"})
        completed = await client.wait_for_job(result.data.id)
        pdf_bytes = await client.download_job_result(completed)


asyncio.run(main())
```

## Error Handling

```python
from cloudlayerio.errors import (
    CloudLayerAuthError,
    CloudLayerRateLimitError,
    CloudLayerError,
)

try:
    result = client.url_to_pdf({"url": "https://example.com"})
except CloudLayerAuthError:
    print("Invalid API key")
except CloudLayerRateLimitError as e:
    print(f"Rate limited, retry after {e.retry_after}s")
except CloudLayerError as e:
    print(f"Error: {e}")
```

## Full Documentation

See the [README on GitHub](https://github.com/cloudlayerio/cloudlayerio-python) for complete documentation including all conversion methods, data management, storage configuration, and performance notes.

### PHP SDK

Source: https://cloudlayer.io/docs/sdk-php/

# PHP PDF Generation SDK

The official `cloudlayerio/cloudlayerio-php` package provides PHP PDF generation for any PHP 8.1+ application. Convert HTML to PDF, capture URLs as documents, and render templates — with full type safety (PHPStan level 8), strict types, and a single runtime dependency (`guzzlehttp/guzzle`).

- **Packagist Package:** [cloudlayerio/cloudlayerio-php](https://packagist.org/packages/cloudlayerio/cloudlayerio-php)
- **Source Code:** [github.com/cloudlayerio/cloudlayerio-php](https://github.com/cloudlayerio/cloudlayerio-php)

## Installation

```bash
composer require cloudlayerio/cloudlayerio-php
```

**Requirements:** PHP 8.1+

## Setup

Get your API key by creating a free account at [cloudlayer.io](https://beta-app.cloudlayer.io/auth/sign-up). The free account includes a monthly render allowance, with no card. See [pricing](https://cloudlayer.io/pricing/) for the current figures.

```php
use CloudLayer\CloudLayer;

$client = new CloudLayer(
    apiKey: 'YOUR-API-KEY',
    apiVersion: 'v2',
);
```

The SDK requires you to choose an API version:

| | v1 | v2 |
|---|---|---|
| **Default mode** | Synchronous (returns binary) | Asynchronous (returns Job) |
| **Sync response** | Raw binary (PDF/image bytes) | JSON Job object |
| **Binary access** | Direct from `$result->data` | Via `downloadJobResult()` |

## URL to PDF

### v2 (recommended)

v2 returns a Job object. Use `downloadJobResult()` to get the binary:

```php
$result = $client->urlToPdf(['url' => 'https://example.com']);
$job = $client->waitForJob($result->data->id);
$pdfBytes = $client->downloadJobResult($job);

file_put_contents('output.pdf', $pdfBytes);
```

### v1 (direct binary)

v1 returns the binary directly:

```php
$client = new CloudLayer(apiKey: 'YOUR-API-KEY', apiVersion: 'v1');

$result = $client->urlToPdf(['url' => 'https://example.com']);
file_put_contents('output.pdf', $result->data); // raw PDF bytes
```

## HTML to PDF

```php
$html = base64_encode('<h1>Hello World</h1>');

$result = $client->htmlToPdf(['html' => $html]);
```

## Template Rendering

```php
$result = $client->templateToPdf([
    'templateId' => 'your-template-id',
    'data' => ['name' => 'John', 'total' => '$100'],
]);
```

## Error Handling

```php
use CloudLayer\Errors\AuthException;
use CloudLayer\Errors\RateLimitException;
use Cloudlayer\Errors\CloudLayerException;

try {
    $result = $client->urlToPdf(['url' => 'https://example.com']);
} catch (AuthException $e) {
    echo "Invalid API key\n";
} catch (RateLimitException $e) {
    echo "Rate limited, retry after {$e->retryAfter}s\n";
} catch (CloudLayerException $e) {
    echo "Error: {$e->getMessage()}\n";
}
```

## Full Documentation

See the [README on GitHub](https://github.com/cloudlayerio/cloudlayerio-php) for complete documentation including all conversion methods, data management, storage configuration, and performance notes.

### Go SDK

Source: https://cloudlayer.io/docs/sdk-go/

# Go PDF Generation SDK

The official `cloudlayerio-go` package provides Go PDF generation with context support and zero external dependencies. Convert HTML to PDF, capture URLs as documents, and render templates — using only the Go standard library.

- **Go Package:** [github.com/cloudlayerio/cloudlayerio-go](https://pkg.go.dev/github.com/cloudlayerio/cloudlayerio-go)
- **Source Code:** [github.com/cloudlayerio/cloudlayerio-go](https://github.com/cloudlayerio/cloudlayerio-go)

## Installation

```bash
go get github.com/cloudlayerio/cloudlayerio-go
```

**Requirements:** Go 1.21+, zero external dependencies

## Setup

Get your API key by creating a free account at [cloudlayer.io](https://beta-app.cloudlayer.io/auth/sign-up). The free account includes a monthly render allowance, with no card. See [pricing](https://cloudlayer.io/pricing/) for the current figures.

```go
import cloudlayer "github.com/cloudlayerio/cloudlayerio-go"

client, err := cloudlayer.NewClient("YOUR-API-KEY", cloudlayer.V2)
```

The SDK requires you to choose an API version:

| | v1 | v2 |
|---|---|---|
| **Default mode** | Synchronous (returns binary) | Asynchronous (returns Job) |
| **Sync response** | Raw binary (PDF/image bytes) | JSON Job object |
| **Binary access** | Direct from `result.Data` | Via `DownloadJobResult()` |

## URL to PDF

### v2 (recommended)

v2 returns a Job object. Use `DownloadJobResult()` to get the binary:

```go
result, err := client.URLToPDF(ctx, &cloudlayer.URLToPDFOptions{
    URLOptions: cloudlayer.URLOptions{
        URL: cloudlayer.StringPtr("https://example.com"),
    },
})
if err != nil {
    log.Fatal(err)
}

// Wait for the job to complete
job, err := client.WaitForJob(ctx, result.Job.ID)
if err != nil {
    log.Fatal(err)
}

// Download the PDF binary
data, err := client.DownloadJobResult(ctx, job)
if err != nil {
    log.Fatal(err)
}

os.WriteFile("output.pdf", data, 0644)
```

### v1 (legacy)

v1 returns raw binary directly:

```go
client, _ := cloudlayer.NewClient("YOUR-API-KEY", cloudlayer.V1)

result, err := client.URLToPDF(ctx, &cloudlayer.URLToPDFOptions{
    URLOptions: cloudlayer.URLOptions{
        URL: cloudlayer.StringPtr("https://example.com"),
    },
})
if err != nil {
    log.Fatal(err)
}

os.WriteFile("output.pdf", result.Data, 0644)
```

## HTML to PDF

```go
result, err := client.HTMLToPDF(ctx, &cloudlayer.HTMLToPDFOptions{
    HTMLOptions: cloudlayer.HTMLOptions{
        HTML: cloudlayer.EncodeHTML("<h1>Hello World</h1><p>Generated by cloudlayer.io</p>"),
    },
    PDFOptions: cloudlayer.PDFOptions{
        Format:          &cloudlayer.FormatA4,
        PrintBackground: cloudlayer.BoolPtr(true),
    },
})
```

## Template to PDF

```go
result, err := client.TemplateToPDF(ctx, &cloudlayer.TemplateToPDFOptions{
    TemplateOptions: cloudlayer.TemplateOptions{
        TemplateID: cloudlayer.StringPtr("professional-invoice"),
        Data: map[string]interface{}{
            "invoiceNumber": "INV-2024-001",
            "companyName":   "Acme Corp",
            "items": []map[string]interface{}{
                {"name": "Widget A", "quantity": 10, "price": 25.00},
            },
            "total": 250.00,
        },
    },
})
```

## Error Handling

All errors are concrete types supporting `errors.As`:

```go
result, err := client.HTMLToPDF(ctx, opts)
if err != nil {
    var authErr *cloudlayer.AuthError
    if errors.As(err, &authErr) {
        log.Fatal("Invalid API key")
    }

    var rateLimitErr *cloudlayer.RateLimitError
    if errors.As(err, &rateLimitErr) {
        log.Printf("Rate limited, retry after %ds", *rateLimitErr.RetryAfter)
    }

    log.Fatal(err)
}
```

| Error Type | When |
|------------|------|
| `*AuthError` | Invalid or missing API key (401/403) |
| `*RateLimitError` | Rate limit exceeded (429) |
| `*APIError` | Other API errors |
| `*ValidationError` | Invalid input parameters |
| `*NetworkError` | Connection failures |

## All Conversion Methods

| Method | Description |
|--------|-------------|
| `URLToPDF()` | Convert a URL to PDF |
| `URLToImage()` | Screenshot a URL |
| `HTMLToPDF()` | Convert HTML to PDF |
| `HTMLToImage()` | Convert HTML to image |
| `TemplateToPDF()` | Render template as PDF |
| `TemplateToImage()` | Render template as image |
| `MergePDFs()` | Merge multiple PDFs |

## Data Management

```go
// List recent jobs
jobs, _ := client.ListJobs(ctx)

// Get account usage
account, _ := client.GetAccount(ctx)

// Configure custom storage
client.AddStorage(ctx, &cloudlayer.StorageParams{
    Title:          "My S3 Bucket",
    Bucket:         "my-pdfs",
    Region:         "us-east-1",
    AccessKeyID:    "AKIA...",
    SecretAccessKey: "...",
})
```

### Java SDK

Source: https://cloudlayer.io/docs/sdk-java/

# Java PDF Generation SDK

The official `cloudlayerio-java` library provides Java PDF generation with builder pattern options, automatic retry logic, and thread-safe clients. Convert HTML to PDF, capture URLs as documents, and render templates — with only Jackson as a runtime dependency.

- **Maven Central:** [io.cloudlayer:cloudlayerio-java](https://central.sonatype.com/artifact/io.cloudlayer/cloudlayerio-java)
- **Source Code:** [github.com/cloudlayerio/cloudlayerio-java](https://github.com/cloudlayerio/cloudlayerio-java)

## Installation

### Maven

```xml
<dependency>
    <groupId>io.cloudlayer</groupId>
    <artifactId>cloudlayerio-java</artifactId>
    <version>0.1.0</version>
</dependency>
```

### Gradle

```groovy
implementation 'io.cloudlayer:cloudlayerio-java:0.1.0'
```

**Requirements:** Java 11+, only Jackson for JSON (no other runtime dependencies)

## Setup

Get your API key by creating a free account at [cloudlayer.io](https://beta-app.cloudlayer.io/auth/sign-up). The free account includes a monthly render allowance, with no card. See [pricing](https://cloudlayer.io/pricing/) for the current figures.

```java
import io.cloudlayer.sdk.*;
import io.cloudlayer.sdk.model.endpoint.*;
import io.cloudlayer.sdk.model.constants.*;
import io.cloudlayer.sdk.model.response.*;

CloudLayer client = CloudLayer.builder("YOUR-API-KEY", ApiVersion.V2).build();
```

The SDK requires you to choose an API version:

| | v1 | v2 |
|---|---|---|
| **Default mode** | Synchronous (returns binary) | Asynchronous (returns Job) |
| **Sync response** | Raw binary (PDF/image bytes) | JSON Job object |
| **Binary access** | Direct from `result.getBytes()` | Via `downloadJobResult()` |

## URL to PDF

### v2 (recommended)

v2 returns a Job object. Use `waitForJob()` + `downloadJobResult()` to get the binary:

```java
ConversionResult result = client.urlToPdf(UrlToPdfOptions.builder()
        .url("https://example.com")
        .format(PdfFormat.A4)
        .printBackground(true)
        .build());

Job job = client.waitForJob(result.getJob().getId());
byte[] pdf = client.downloadJobResult(job);
Files.write(Path.of("output.pdf"), pdf);
```

### v1

v1 returns binary directly:

```java
CloudLayer v1Client = CloudLayer.builder("YOUR-API-KEY", ApiVersion.V1).build();

ConversionResult result = v1Client.urlToPdf(UrlToPdfOptions.builder()
        .url("https://example.com")
        .format(PdfFormat.A4)
        .build());

byte[] pdf = result.getBytes();
```

## URL to Image

```java
ConversionResult result = client.urlToImage(UrlToImageOptions.builder()
        .url("https://example.com")
        .imageType(ImageType.PNG)
        .quality(90)
        .build());

Job job = client.waitForJob(result.getJob().getId());
byte[] image = client.downloadJobResult(job);
```

## HTML to PDF

HTML must be Base64-encoded. Use the included `HtmlUtil.encodeHtml()` helper:

```java
import io.cloudlayer.sdk.util.HtmlUtil;

String html = "<html><body><h1>Hello World</h1></body></html>";

ConversionResult result = client.htmlToPdf(HtmlToPdfOptions.builder()
        .html(HtmlUtil.encodeHtml(html))
        .format(PdfFormat.LETTER)
        .printBackground(true)
        .build());
```

## Template to PDF

```java
ConversionResult result = client.templateToPdf(TemplateToPdfOptions.builder()
        .templateId("your-template-id")
        .data(Map.of(
                "name", "John Doe",
                "invoiceNumber", "INV-001",
                "amount", 1500
        ))
        .build());
```

## Merge PDFs

```java
ConversionResult result = client.mergePdfs(MergePdfsOptions.builder()
        .batch(Batch.of(List.of(
                "https://example.com/doc1.pdf",
                "https://example.com/doc2.pdf"
        )))
        .build());
```

## Data Management

```java
// Jobs
List<Job> jobs = client.listJobs();
Job job = client.getJob("job-id");

// Assets
List<Asset> assets = client.listAssets();
byte[] data = client.downloadJobResult(job);

// Account
AccountInfo account = client.getAccount();
StatusResponse status = client.getStatus();

// Templates
List<PublicTemplate> templates = client.listTemplates();
PublicTemplate template = client.getTemplate("template-id");
```

## Configuration Options

```java
CloudLayer client = CloudLayer.builder("key", ApiVersion.V2)
        .timeout(Duration.ofSeconds(60))          // request timeout (default: 30s)
        .maxRetries(3)                             // retry count, range [0, 5] (default: 2)
        .userAgent("my-app/1.0")                   // custom user agent
        .headers(Map.of("X-Custom", "value"))      // additional headers
        .httpClient(customHttpClient)              // inject custom java.net.http.HttpClient
        .build();
```

## Error Handling

```java
try {
    client.urlToPdf(options);
} catch (AuthException e) {
    // 401 or 403
} catch (RateLimitException e) {
    // 429 — check e.getRetryAfterSeconds()
} catch (ValidationException e) {
    // Client-side validation error
} catch (ApiException e) {
    // Other HTTP error — e.getStatusCode(), e.getResponseBody()
} catch (TimeoutException e) {
    // Request or poll timeout
} catch (NetworkException e) {
    // Connection failure
}
```

## Working with v2 Results

The recommended v2 workflow:

```java
// 1. Start conversion (returns immediately with pending Job)
ConversionResult result = client.urlToPdf(options);

// 2. Wait for completion (polls every 5s, timeout 5min by default)
Job job = client.waitForJob(result.getJob().getId());

// 3. Download the binary
byte[] pdf = client.downloadJobResult(job);

// Custom polling options
Job job = client.waitForJob(jobId,
        WaitForJobOptions.builder()
                .interval(Duration.ofSeconds(3))
                .maxWait(Duration.ofMinutes(10))
                .build());
```

### Ruby SDK

Source: https://cloudlayer.io/docs/sdk-ruby/

# Ruby SDK

The official Ruby gem for the [cloudlayer.io](https://cloudlayer.io) visual document and image platform. Convert HTML, URLs, and templates to PDF and images, manage jobs, assets, and storage configurations.

**[RubyGems](https://rubygems.org/gems/cloudlayerio)** · **[GitHub Source](https://github.com/cloudlayerio/cloudlayerio-ruby)**

## Installation

Add to your Gemfile:

```ruby
gem "cloudlayerio", "~> 0.1"
```

Then run:

```bash
bundle install
```

Or install directly:

```bash
gem install cloudlayerio
```

## Setup

```ruby
require "cloudlayerio"

client = CloudLayerio::Client.new(
  api_key: "your-api-key",
  api_version: :v2
)
```

Get your API key from the [cloudlayer.io dashboard](https://beta-app.cloudlayer.io).

**API Versions:**
- `:v1` — synchronous, returns binary PDF/image data directly
- `:v2` — asynchronous, returns a Job object (poll for completion, then download)

## URL to PDF

```ruby
# v2 (async)
result = client.url_to_pdf(
  url: "https://example.com",
  format: "a4",
  print_background: true,
  async: true,
  storage: true
)

job = client.wait_for_job(result.job.id)
data = client.download_job_result(job)
File.binwrite("output.pdf", data)
```

```ruby
# v1 (sync) — returns binary directly
v1_client = CloudLayerio::Client.new(api_key: "your-key", api_version: :v1)
result = v1_client.url_to_pdf(url: "https://example.com")
File.binwrite("output.pdf", result.bytes)
```

## URL to Image

```ruby
result = client.url_to_image(
  url: "https://example.com",
  image_type: "png",
  quality: 90
)
```

## HTML to PDF

```ruby
html = CloudLayerio::Util::HtmlUtil.encode_html(<<~HTML)
  <html>
    <body>
      <h1>Invoice #001</h1>
      <p>Thank you for your purchase.</p>
    </body>
  </html>
HTML

result = client.html_to_pdf(
  html: html,
  format: "letter",
  print_background: true
)
```

## HTML to Image

```ruby
html = CloudLayerio::Util::HtmlUtil.encode_html("<div style='padding:40px'><h1>Hello</h1></div>")
result = client.html_to_image(html: html, image_type: "png")
```

## Template to PDF

```ruby
result = client.template_to_pdf(
  template_id: "your-template-id",
  data: {
    company: "Acme Corp",
    invoice_number: "INV-001",
    items: [{ name: "Widget", price: 9.99 }]
  }
)
```

## Template to Image

```ruby
result = client.template_to_image(
  template_id: "your-template-id",
  data: { title: "Certificate of Completion" },
  image_type: "png"
)
```

## Merge PDFs

```ruby
result = client.merge_pdfs(
  batch: CloudLayerio::Options::Batch.new(urls: [
    "https://example.com/page1.pdf",
    "https://example.com/page2.pdf"
  ])
)
```

## Data Management

### Jobs & Assets

```ruby
jobs = client.list_jobs          # Up to 10 most recent
job = client.get_job("job-id")

assets = client.list_assets      # Up to 10 most recent
asset = client.get_asset("asset-id")
```

### Account

```ruby
account = client.get_account
puts "Calls: #{account.calls}/#{account.calls_limit}"
```

### Templates

```ruby
templates = client.list_templates(type: "pdf", category: "invoice")
template = client.get_template("template-id")
```

### Storage

```ruby
storages = client.list_storage
detail = client.get_storage("storage-id")

resp = client.add_storage(
  title: "My S3",
  region: "us-east-1",
  access_key_id: "AKIA...",
  secret_access_key: "...",
  bucket: "my-bucket"
)

client.delete_storage("storage-id")
```

## Configuration Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `api_key` | String | *required* | Your cloudlayer.io API key |
| `api_version` | Symbol | *required* | `:v1` or `:v2` |
| `base_url` | String | `https://api.cloudlayer.io` | API base URL |
| `timeout` | Numeric | `30` | Request timeout (seconds) |
| `max_retries` | Integer | `2` | Retry attempts for 429/5xx (0-5) |
| `user_agent` | String | `cloudlayerio-ruby/VERSION` | User-Agent header |
| `headers` | Hash | `{}` | Additional HTTP headers |

## Error Handling

```ruby
begin
  result = client.url_to_pdf(url: "https://example.com")
rescue CloudLayerio::AuthError => e
  puts "Auth failed (#{e.status_code}): #{e.message}"
rescue CloudLayerio::RateLimitError => e
  puts "Rate limited, retry after #{e.retry_after}s"
rescue CloudLayerio::ApiError => e
  puts "API error #{e.status_code}: #{e.message}"
rescue CloudLayerio::TimeoutError
  puts "Request timed out"
rescue CloudLayerio::NetworkError
  puts "Connection failed"
rescue CloudLayerio::ValidationError => e
  puts "Invalid input: #{e.message}"
end
```

## Requirements

- Ruby >= 3.1
- One runtime dependency: `base64` gem (bundled in Ruby stdlib; extracted in Ruby 3.4+)


## Examples

### Authentication

Source: https://cloudlayer.io/docs/authentication-examples/

# Authentication

When generating PDFs or images from URLs, your target page may be behind authentication. cloudlayer.io supports two authentication methods for accessing protected pages:

- **Basic Auth**, HTTP Basic Authentication using a username and password
- **Cookie Auth**, Session cookies that establish an authenticated user state

Both methods work with all URL-based endpoints (`/url/pdf`, `/url/image`).

## Basic Auth

If your URL is protected behind [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication), pass the credentials using the `authentication` object.

### JSON Payload

```json
{
  "url": "https://internal.example.com/dashboard",
  "authentication": {
    "username": "admin",
    "password": "secretpassword"
  }
}
```

### Parameters

| Parameter                    | Type     | Required | Description                |
|------------------------------|----------|----------|----------------------------|
| `authentication.username`    | `string` | Yes      | The username for Basic Auth |
| `authentication.password`    | `string` | Yes      | The password for Basic Auth |

### cURL Example

```shell
curl --request POST \
  --url https://api.cloudlayer.io/v2/url/pdf \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: <YOUR-API-KEY>' \
  --data '{
    "url": "https://internal.example.com/dashboard",
    "authentication": {
      "username": "admin",
      "password": "secretpassword"
    },
    "async": false
  }'
```

### JavaScript Example

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/url/pdf", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": "<YOUR-API-KEY>",
  },
  body: JSON.stringify({
    url: "https://internal.example.com/dashboard",
    authentication: {
      username: "admin",
      password: "secretpassword",
    },
    async: false,
  }),
});

const result = await response.json();
console.log(result.assetUrl);
```

### Python Example

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v2/url/pdf",
    headers={
        "Content-Type": "application/json",
        "x-api-key": "<YOUR-API-KEY>",
    },
    json={
        "url": "https://internal.example.com/dashboard",
        "authentication": {
            "username": "admin",
            "password": "secretpassword",
        },
        "async": False,
    },
)

result = response.json()
print(result["assetUrl"])
```

### Practical Example: Capturing a Protected Dashboard

A common use case is generating PDF reports from internal dashboards that require login:

```json
{
  "url": "https://analytics.yourcompany.com/reports/monthly",
  "authentication": {
    "username": "report-bot",
    "password": "bot-secret-key"
  },
  "format": "letter",
  "landscape": true,
  "printBackground": true,
  "margin": {
    "top": "0.5in",
    "bottom": "0.5in",
    "left": "0.5in",
    "right": "0.5in"
  },
  "waitUntil": "networkidle0",
  "delay": 2000,
  "async": false
}
```

This configuration:

1. Authenticates to the dashboard with Basic Auth credentials
2. Waits for all network requests to complete (`networkidle0`)
3. Adds an extra 2-second delay for any JavaScript-rendered charts to finish
4. Generates a letter-sized landscape PDF with backgrounds and custom margins

## Cookie Auth

For sites that use session-based authentication (login forms, OAuth, etc.), you can pass session cookies to establish an authenticated state before the page is loaded.

### JSON Payload

```json
{
  "url": "https://app.example.com/account",
  "cookies": [
    {
      "name": "_session_id",
      "value": "ad9a8u90f0df7d87fdas9fa892342",
      "domain": "app.example.com",
      "path": "/",
      "expires": 1768752280,
      "httpOnly": true,
      "secure": true
    }
  ]
}
```

### Cookie Parameters

| Parameter   | Type      | Required | Description                                                         |
|-------------|-----------|----------|---------------------------------------------------------------------|
| `name`      | `string`  | Yes      | Cookie name                                                         |
| `value`     | `string`  | Yes      | Cookie value                                                        |
| `domain`    | `string`  | No       | Host the cookie is sent to. Defaults to the URL's host.             |
| `path`      | `string`  | No       | Path that must exist in the URL for the cookie to be sent           |
| `url`       | `string`  | No       | URL to associate with the cookie (usually left empty)               |
| `expires`   | `number`  | No       | Cookie expiration as a Unix timestamp                               |
| `httpOnly`  | `boolean` | No       | Prevent JavaScript from accessing the cookie                        |
| `secure`    | `boolean` | No       | Only send the cookie over HTTPS                                     |
| `sameSite`  | `string`  | No       | Cross-origin policy: `"Strict"` or `"Lax"`                         |

### cURL Example

```shell
curl --request POST \
  --url https://api.cloudlayer.io/v2/url/pdf \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: <YOUR-API-KEY>' \
  --data '{
    "url": "https://app.example.com/account",
    "cookies": [
      {
        "name": "_session_id",
        "value": "ad9a8u90f0df7d87fdas9fa892342",
        "domain": "app.example.com",
        "path": "/",
        "expires": 1768752280,
        "httpOnly": true
      }
    ],
    "async": false
  }'
```

### JavaScript Example

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/url/pdf", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": "<YOUR-API-KEY>",
  },
  body: JSON.stringify({
    url: "https://app.example.com/account",
    cookies: [
      {
        name: "_session_id",
        value: "ad9a8u90f0df7d87fdas9fa892342",
        domain: "app.example.com",
        path: "/",
        httpOnly: true,
      },
    ],
    async: false,
  }),
});

const result = await response.json();
console.log(result.assetUrl);
```

### Python Example

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v2/url/pdf",
    headers={
        "Content-Type": "application/json",
        "x-api-key": "<YOUR-API-KEY>",
    },
    json={
        "url": "https://app.example.com/account",
        "cookies": [
            {
                "name": "_session_id",
                "value": "ad9a8u90f0df7d87fdas9fa892342",
                "domain": "app.example.com",
                "path": "/",
                "httpOnly": True,
            },
        ],
        "async": False,
    },
)

result = response.json()
print(result["assetUrl"])
```

### Multiple Cookies

You can pass multiple cookies in a single request to replicate a full browser session:

```json
{
  "url": "https://app.example.com/dashboard",
  "cookies": [
    {
      "name": "_session_id",
      "value": "abc123def456",
      "domain": "app.example.com",
      "path": "/",
      "httpOnly": true
    },
    {
      "name": "cookie_consent",
      "value": "accepted",
      "domain": "app.example.com",
      "path": "/"
    },
    {
      "name": "theme",
      "value": "dark",
      "domain": "app.example.com",
      "path": "/"
    }
  ]
}
```

### How to Obtain Session Cookies

To capture cookies from an authenticated session:

1. Log in to the target site in your browser.
2. Open **Developer Tools** (F12 or Ctrl+Shift+I).
3. Go to the **Application** tab (Chrome) or **Storage** tab (Firefox).
4. Navigate to **Cookies** and select the site's domain.
5. Copy the relevant cookie name/value pairs (look for session identifiers like `_session_id`, `connect.sid`, `PHPSESSID`, etc.).

> **Note:** Session cookies typically have a limited lifespan. For automated workflows, consider using a dedicated API token or service account instead of short-lived session cookies.

## Security and Privacy

cloudlayer.io takes authentication credentials seriously:

- **Credentials are never stored.** Authentication usernames, passwords, and cookie values are redacted from all logs and database records.
- **Activity logs show `...`** in place of any sensitive information.
- **Use HTTPS URLs** whenever passing authentication credentials to ensure they are encrypted in transit to the target page.
- **Use short-lived credentials** or service accounts with minimal permissions for automated workflows.

For more details, see the [Privacy & Data Handling](/docs/privacy/) guide.

## Tips

- **Basic Auth is simpler** and preferred when the target site supports it. Cookie auth requires managing session state.
- **Use `waitUntil: "networkidle0"`** with authenticated pages to ensure all authenticated API calls complete before capture.
- **Add a `delay`** (in milliseconds) if the page has JavaScript-heavy content that takes time to render after authentication.
- **Test your authentication** by first trying the URL in a browser with the same credentials to verify the page loads correctly.
- **For single-page apps (SPAs)**, cookie auth combined with `waitForSelector` is often more reliable than a simple delay.

### Working with HTML

Source: https://cloudlayer.io/docs/html-examples/

# Working with HTML

Converting HTML to PDFs and images gives you complete control over the output. You can use any valid HTML, CSS, JavaScript, Sass, and externally hosted resources like fonts, stylesheets, and images. As long as the base64-encoded payload is under 10MB, cloudlayer.io will accept and convert it.

## HTML Encoding

HTML must be **base64-encoded** before being included in the JSON request body. This avoids issues with special characters (double quotes, angle brackets, etc.) that would otherwise break JSON parsing.

**Original HTML:**

```html
<html>
  <body>
    <h1>Hello world!</h1>
  </body>
</html>
```

**Base64-encoded:**

```
PGh0bWw+PGJvZHk+PGgxPkhlbGxvIHdvcmxkITwvaDE+PC9ib2R5PjwvaHRtbD4=
```

**JSON payload:**

```json
{
  "html": "PGh0bWw+PGJvZHk+PGgxPkhlbGxvIHdvcmxkITwvaDE+PC9ib2R5PjwvaHRtbD4="
}
```

### Encoding HTML in Different Languages

#### JavaScript (Node.js / Browser)

```javascript
// Browser
const encoded = btoa("<html><body><h1>Hello!</h1></body></html>");

// Node.js / Bun
const encoded = Buffer.from(
  "<html><body><h1>Hello!</h1></body></html>"
).toString("base64");
```

#### Python

```python
import base64

html = "<html><body><h1>Hello!</h1></body></html>"
encoded = base64.b64encode(html.encode("utf-8")).decode("utf-8")
```

#### C\#

```csharp
using System.Text;

var html = "<html><body><h1>Hello!</h1></body></html>";
var encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(html));
```

#### Ruby

```ruby
require "base64"

html = "<html><body><h1>Hello!</h1></body></html>"
encoded = Base64.strict_encode64(html)
```

#### PHP

```php
$html = "<html><body><h1>Hello!</h1></body></html>";
$encoded = base64_encode($html);
```

#### Go

```go
import "encoding/base64"

html := "<html><body><h1>Hello!</h1></body></html>"
encoded := base64.StdEncoding.EncodeToString([]byte(html))
```

#### Java

```java
import java.util.Base64;

String html = "<html><body><h1>Hello!</h1></body></html>";
String encoded = Base64.getEncoder().encodeToString(html.getBytes("UTF-8"));
```

> **Tip:** If you are using [VS Code](https://code.visualstudio.com/), the [vscode-base64](https://marketplace.visualstudio.com/items?itemName=adamhartford.vscode-base64) extension lets you encode and decode HTML interactively while developing.

## Basic Examples

### HTML to PDF

```shell
curl --request POST \
  --url https://api.cloudlayer.io/v2/html/pdf \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: <YOUR-API-KEY>' \
  --data '{
    "html": "PGh0bWw+PGJvZHk+PGgxPkhlbGxvIHdvcmxkITwvaDE+PC9ib2R5PjwvaHRtbD4=",
    "async": false
  }'
```

### HTML to Image

```shell
curl --request POST \
  --url https://api.cloudlayer.io/v2/html/image \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: <YOUR-API-KEY>' \
  --data '{
    "html": "PGh0bWw+PGJvZHk+PGgxPkhlbGxvIHdvcmxkITwvaDE+PC9ib2R5PjwvaHRtbD4=",
    "async": false
  }'
```

### With Viewport Options

```json
{
  "html": "PGh0bWw+PGJvZHk+PGgxPkhlbGxvIHdvcmxkITwvaDE+PC9ib2R5PjwvaHRtbD4=",
  "viewPort": {
    "width": 800,
    "height": 600
  }
}
```

## Embedding Images

For fully self-contained HTML documents, you can embed images as [data URIs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) instead of referencing external URLs.

### Data URI Format

```html
<img src="data:image/{format};base64,{base64-encoded-image-data}" />
```

Supported formats include `png`, `jpeg`, `gif`, `svg+xml`, and `webp`.

### Example: Embedded Logo

```html
<html>
  <body style="margin: 10px; background-color: #4E279C;">
    <div style="display: flex; align-items: center;">
      <img
        src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA..."
        width="36"
        height="36"
      />
      <div style="
        margin-left: 10px;
        font-size: 24px;
        color: #fff;
        font-weight: 500;
        font-family: sans-serif;
      ">
        cloudlayer.io
      </div>
    </div>
  </body>
</html>
```

### How to Generate Data URIs

#### JavaScript

```javascript
const fs = require("fs");
const imageBuffer = fs.readFileSync("logo.png");
const base64 = imageBuffer.toString("base64");
const dataUri = `data:image/png;base64,${base64}`;
```

#### Python

```python
import base64

with open("logo.png", "rb") as f:
    base64_data = base64.b64encode(f.read()).decode("utf-8")
    data_uri = f"data:image/png;base64,{base64_data}"
```

> **Note:** Having a base64-encoded image embedded inside a base64-encoded HTML string is perfectly valid. The outer encoding is for the JSON transport; the inner encoding is part of the HTML specification.

## Using External Stylesheets

Since cloudlayer.io fetches external resources during rendering, you can use any publicly accessible CSS framework, font library, or stylesheet.

### Tailwind CSS

```html
<html>
  <head>
    <script src="https://cdn.tailwindcss.com"></script>
  </head>
  <body class="p-8 bg-white">
    <div class="max-w-md mx-auto bg-gray-50 rounded-lg shadow p-6">
      <h1 class="text-2xl font-bold text-gray-900">Order Confirmation</h1>
      <p class="text-gray-600 mt-2">Thank you for your purchase.</p>
    </div>
  </body>
</html>
```

### Bootstrap

```html
<html>
  <head>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
      rel="stylesheet"
    />
  </head>
  <body>
    <div class="container mt-4">
      <div class="card">
        <div class="card-body">
          <h5 class="card-title">Report Summary</h5>
          <p class="card-text">Generated on 2024-01-15</p>
        </div>
      </div>
    </div>
  </body>
</html>
```

### Google Fonts

```html
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
      rel="stylesheet"
    />
    <style>
      body { font-family: 'Inter', sans-serif; }
    </style>
  </head>
  <body>
    <h1 style="font-weight: 700;">Beautiful Typography</h1>
    <p style="font-weight: 400;">Using Google Fonts in your documents.</p>
  </body>
</html>
```

## Complex Layouts

### CSS Grid

```html
<html>
  <head>
    <style>
      .dashboard {
        display: grid;
        grid-template-columns: repeat(3, 1fr);
        gap: 16px;
        padding: 24px;
      }
      .card {
        background: #f9fafb;
        border: 1px solid #e5e7eb;
        border-radius: 8px;
        padding: 20px;
      }
      .card h3 {
        margin: 0 0 8px 0;
        color: #374151;
        font-size: 14px;
      }
      .card .value {
        font-size: 28px;
        font-weight: 700;
        color: #111827;
      }
      .full-width {
        grid-column: 1 / -1;
      }
    </style>
  </head>
  <body>
    <div class="dashboard">
      <div class="card">
        <h3>Total Revenue</h3>
        <div class="value">$45,231</div>
      </div>
      <div class="card">
        <h3>Orders</h3>
        <div class="value">1,247</div>
      </div>
      <div class="card">
        <h3>Customers</h3>
        <div class="value">892</div>
      </div>
      <div class="card full-width">
        <h3>Monthly Sales Chart</h3>
        <!-- Chart content here -->
      </div>
    </div>
  </body>
</html>
```

### Flexbox

```html
<html>
  <head>
    <style>
      .header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 20px 40px;
        border-bottom: 2px solid #e5e7eb;
      }
      .logo { font-size: 24px; font-weight: 700; }
      .meta { text-align: right; color: #6b7280; }
      .two-column {
        display: flex;
        gap: 40px;
        padding: 40px;
      }
      .two-column > div { flex: 1; }
    </style>
  </head>
  <body>
    <div class="header">
      <div class="logo">Acme Inc.</div>
      <div class="meta">
        <div>Invoice #INV-001</div>
        <div>January 15, 2024</div>
      </div>
    </div>
    <div class="two-column">
      <div>
        <h3>From</h3>
        <p>Acme Inc.</p>
        <p>123 Business St.</p>
      </div>
      <div>
        <h3>Bill To</h3>
        <p>Jane Smith</p>
        <p>456 Customer Ave.</p>
      </div>
    </div>
  </body>
</html>
```

## Practical Examples

### Generating an Invoice PDF from HTML

A complete example that generates a styled invoice PDF using JavaScript.

```javascript
const html = `
<html>
  <head>
    <script src="https://cdn.tailwindcss.com"></script>
  </head>
  <body class="bg-white p-8 font-sans">
    <div class="max-w-2xl mx-auto">
      <div class="flex justify-between mb-8">
        <div>
          <h1 class="text-2xl font-bold">Acme Inc.</h1>
          <p class="text-gray-600">123 Business Street</p>
          <p class="text-gray-600">New York, NY 10001</p>
        </div>
        <div class="text-right">
          <h2 class="text-xl font-semibold text-blue-600">INVOICE</h2>
          <p>#INV-2024-001</p>
          <p class="text-gray-600">January 15, 2024</p>
        </div>
      </div>

      <table class="w-full mb-8">
        <thead>
          <tr class="border-b-2">
            <th class="text-left py-2">Item</th>
            <th class="text-right py-2">Qty</th>
            <th class="text-right py-2">Price</th>
            <th class="text-right py-2">Total</th>
          </tr>
        </thead>
        <tbody>
          <tr class="border-b">
            <td class="py-3">Web Development</td>
            <td class="text-right">40</td>
            <td class="text-right">$150.00</td>
            <td class="text-right">$6,000.00</td>
          </tr>
          <tr class="border-b">
            <td class="py-3">UI Design</td>
            <td class="text-right">20</td>
            <td class="text-right">$120.00</td>
            <td class="text-right">$2,400.00</td>
          </tr>
        </tbody>
      </table>

      <div class="flex justify-end">
        <div class="w-64">
          <div class="flex justify-between py-1">
            <span>Subtotal</span>
            <span>$8,400.00</span>
          </div>
          <div class="flex justify-between py-1">
            <span>Tax (8%)</span>
            <span>$672.00</span>
          </div>
          <div class="flex justify-between py-2 border-t-2 font-bold text-lg">
            <span>Total</span>
            <span>$9,072.00</span>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>
`;

const encoded = Buffer.from(html).toString("base64");

const response = await fetch("https://api.cloudlayer.io/v2/html/pdf", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": "<YOUR-API-KEY>",
  },
  body: JSON.stringify({
    html: encoded,
    format: "letter",
    printBackground: true,
    margin: {
      top: "0.5in",
      bottom: "0.5in",
      left: "0.5in",
      right: "0.5in",
    },
    async: false,
  }),
});

const result = await response.json();
console.log("PDF URL:", result.assetUrl);
```

### Creating a Social Media Image

Generate an Open Graph or social media card image from HTML.

```python
import base64
import requests

html = """
<html>
  <head>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@700;400&display=swap" rel="stylesheet">
    <style>
      body {
        margin: 0;
        width: 1200px;
        height: 630px;
        display: flex;
        align-items: center;
        justify-content: center;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        font-family: 'Inter', sans-serif;
      }
      .card {
        text-align: center;
        color: white;
        padding: 60px;
      }
      h1 {
        font-size: 48px;
        font-weight: 700;
        margin-bottom: 16px;
      }
      p {
        font-size: 24px;
        opacity: 0.9;
      }
    </style>
  </head>
  <body>
    <div class="card">
      <h1>Automate Your Document Generation</h1>
      <p>PDFs, images, and more -- powered by cloudlayer.io</p>
    </div>
  </body>
</html>
"""

encoded = base64.b64encode(html.encode("utf-8")).decode("utf-8")

response = requests.post(
    "https://api.cloudlayer.io/v2/html/image",
    headers={
        "Content-Type": "application/json",
        "x-api-key": "<YOUR-API-KEY>",
    },
    json={
        "html": encoded,
        "viewPort": {
            "width": 1200,
            "height": 630,
        },
        "async": False,
    },
)

result = response.json()
print("Image URL:", result["assetUrl"])
```

## PDF-Specific Options

When converting HTML to PDF, you can control the output with these common options:

| Parameter          | Type              | Default       | Description                                        |
|--------------------|-------------------|---------------|----------------------------------------------------|
| `format`           | `string`          | none            | Paper format: `letter`, `legal`, `a4`, etc.        |
| `landscape`        | `boolean`         | `false`       | Use landscape orientation                          |
| `printBackground`  | `boolean`         | `false`       | Include background colors and images               |
| `margin`           | `object`          | `0.4in`       | Page margins (`top`, `bottom`, `left`, `right`)    |
| `pageRanges`       | `string`          | all pages     | Which pages to include (e.g., `"1-3, 5"`)          |
| `scale`            | `number`          | `1`           | Scale factor (0.1 to 2)                            |
| `preferCSSPageSize`| `boolean`         | `false`       | Use CSS `@page` size instead of format             |

### Paper Formats

| Format    | Dimensions       |
|-----------|------------------|
| `letter`  | 8.5in x 11in     |
| `legal`   | 8.5in x 14in     |
| `tabloid` | 11in x 17in      |
| `ledger`  | 17in x 11in      |
| `a0`      | 33.1in x 46.8in  |
| `a1`      | 23.4in x 33.1in  |
| `a2`      | 16.54in x 23.4in |
| `a3`      | 11.7in x 16.54in |
| `a4`      | 8.27in x 11.7in  |
| `a5`      | 5.83in x 8.27in  |
| `a6`      | 4.13in x 5.83in  |

## Tips

- **Always base64-encode your HTML** before placing it in the JSON payload. Raw HTML in JSON leads to parsing errors.
- **Use `printBackground: true`** if your HTML uses background colors or images, Chrome's PDF renderer omits backgrounds by default.
- **External resources are fetched at render time.** Any publicly accessible image, font, or stylesheet URL will be loaded. Private or localhost URLs will not work.
- **For large HTML documents**, consider using the multipart form upload with the template endpoint instead of base64-encoding everything into a single JSON string.
- **Test locally first.** Open your HTML in a browser, use Ctrl+P (print), and verify it looks correct before sending it to the API.

### Working with URLs

Source: https://cloudlayer.io/docs/url-examples/

# Working with URLs

Convert any publicly accessible web page to a PDF or image by providing its URL. This is the simplest way to use cloudlayer.io, just pass a URL and get back a document.

## Simple URL Capture

At its most basic, only a `url` parameter is required.

### JSON Payload

```json
{
  "url": "https://example.com"
}
```

### cURL

```shell
curl --request POST \
  --url https://api.cloudlayer.io/v2/url/pdf \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: <YOUR-API-KEY>' \
  --data '{
    "url": "https://example.com",
    "async": false
  }'
```

### JavaScript

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/url/pdf", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": "<YOUR-API-KEY>",
  },
  body: JSON.stringify({
    url: "https://example.com",
    async: false,
  }),
});

const result = await response.json();
console.log("PDF URL:", result.assetUrl);
```

### Python

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v2/url/pdf",
    headers={
        "Content-Type": "application/json",
        "x-api-key": "<YOUR-API-KEY>",
    },
    json={
        "url": "https://example.com",
        "async": False,
    },
)

result = response.json()
print("PDF URL:", result["assetUrl"])
```

### URL to Image

Use the `/url/image` endpoint instead:

```shell
curl --request POST \
  --url https://api.cloudlayer.io/v2/url/image \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: <YOUR-API-KEY>' \
  --data '{
    "url": "https://example.com",
    "async": false
  }'
```

### GET Request (Simple)

For quick captures with minimal options, use the GET endpoint:

```shell
curl --request GET \
  --url 'https://api.cloudlayer.io/v2/url/pdf?url=https://example.com&timeout=30000' \
  --header 'x-api-key: <YOUR-API-KEY>'
```

The GET endpoint only supports two parameters: `url` (required) and `timeout` (optional). Use the POST endpoint for all other options.

## AutoScroll for Lazy-Loaded Content

Many modern websites use lazy loading, images and content only load when the user scrolls to them. The `autoScroll` option scrolls the page from top to bottom before capture, forcing all lazy-loaded content to load.

### JSON Payload

```json
{
  "url": "https://apple.com",
  "autoScroll": true
}
```

### cURL

```shell
curl --request POST \
  --url https://api.cloudlayer.io/v2/url/image \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: <YOUR-API-KEY>' \
  --data '{
    "url": "https://apple.com",
    "autoScroll": true,
    "async": false
  }'
```

### JavaScript

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/url/image", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": "<YOUR-API-KEY>",
  },
  body: JSON.stringify({
    url: "https://apple.com",
    autoScroll: true,
    async: false,
  }),
});

const result = await response.json();
console.log("Image URL:", result.assetUrl);
```

### Python

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v2/url/image",
    headers={
        "Content-Type": "application/json",
        "x-api-key": "<YOUR-API-KEY>",
    },
    json={
        "url": "https://apple.com",
        "autoScroll": True,
        "async": False,
    },
)

result = response.json()
print("Image URL:", result["assetUrl"])
```

## Viewport Configuration

Control the browser viewport dimensions used for rendering. This affects how the page layout responds to different screen sizes.

### Desktop Viewport

```json
{
  "url": "https://example.com",
  "viewPort": {
    "width": 1920,
    "height": 1080,
    "deviceScaleFactor": 2
  }
}
```

### Mobile Viewport

```json
{
  "url": "https://example.com",
  "viewPort": {
    "width": 375,
    "height": 812,
    "isMobile": true,
    "hasTouch": true,
    "deviceScaleFactor": 3
  }
}
```

### Tablet Viewport

```json
{
  "url": "https://example.com",
  "viewPort": {
    "width": 768,
    "height": 1024,
    "isMobile": true,
    "hasTouch": true,
    "deviceScaleFactor": 2
  }
}
```

### Viewport Parameters

| Parameter           | Type      | Default | Description                                    |
|---------------------|-----------|---------|------------------------------------------------|
| `width`             | `number`  | none      | Page width in pixels (required)                |
| `height`            | `number`  | none      | Page height in pixels (required)               |
| `deviceScaleFactor` | `number`  | `1`     | Device pixel ratio (2 for Retina-quality)      |
| `isMobile`          | `boolean` | `false` | Emulate a mobile device (affects meta viewport)|
| `hasTouch`          | `boolean` | `false` | Enable touch event support                     |
| `isLandscape`       | `boolean` | `false` | Use landscape orientation                      |

## Waiting for Dynamic Content

### waitForSelector

For single-page applications (SPAs) and pages with dynamically loaded content, use `waitForSelector` to wait for a specific element before capturing.

**Wait for an element to appear:**

```json
{
  "url": "https://app.example.com/dashboard",
  "waitForSelector": {
    "selector": "#chart-container"
  }
}
```

**Wait for an element to be visible:**

```json
{
  "url": "https://app.example.com/dashboard",
  "waitForSelector": {
    "selector": "#chart-container",
    "options": {
      "visible": true,
      "timeout": 15000
    }
  }
}
```

**Wait for a loading spinner to disappear:**

```json
{
  "url": "https://app.example.com/report",
  "waitForSelector": {
    "selector": ".loading-spinner",
    "options": {
      "hidden": true,
      "timeout": 30000
    }
  }
}
```

### waitForSelector Options

| Parameter  | Type      | Default | Description                                                |
|------------|-----------|---------|------------------------------------------------------------|
| `selector` | `string`  | none      | CSS selector of the element to wait for                    |
| `visible`  | `boolean` | `false` | Wait for the element to be visible (not just in the DOM)   |
| `hidden`   | `boolean` | `false` | Wait for the element to be hidden or removed               |
| `timeout`  | `number`  | `30000` | Maximum wait time in milliseconds                          |

### waitUntil

Control when the Chrome renderer considers the page "loaded" and begins conversion.

```json
{
  "url": "https://example.com",
  "waitUntil": "networkidle0"
}
```

| Value              | Description                                                          |
|--------------------|----------------------------------------------------------------------|
| `load`             | Wait for the `load` event                                            |
| `domcontentloaded` | Wait for the `DOMContentLoaded` event                                |
| `networkidle0`     | No network connections for at least 500ms (strictest)                |
| `networkidle2`     | No more than 2 network connections for at least 500ms (default)      |

### delay

Add a fixed delay (in milliseconds) after the page loads and before conversion begins. Useful when JavaScript-rendered content needs extra time.

```json
{
  "url": "https://example.com",
  "waitUntil": "networkidle0",
  "delay": 3000
}
```

## Batch URL Processing

Convert multiple URLs into a single combined PDF using the `batch` parameter. Each URL is converted to PDF individually, then all pages are merged into one document.

### JSON Payload

```json
{
  "batch": {
    "urls": [
      "https://example.com/page-1",
      "https://example.com/page-2",
      "https://example.com/page-3"
    ]
  }
}
```

> **Note:** The `batch` parameter replaces the `url` parameter, do not use both.

### cURL

```shell
curl --request POST \
  --url https://api.cloudlayer.io/v2/url/pdf \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: <YOUR-API-KEY>' \
  --data '{
    "batch": {
      "urls": [
        "https://example.com/page-1",
        "https://example.com/page-2",
        "https://example.com/page-3"
      ]
    },
    "async": false
  }'
```

### JavaScript

```javascript
const response = await fetch("https://api.cloudlayer.io/v2/url/pdf", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": "<YOUR-API-KEY>",
  },
  body: JSON.stringify({
    batch: {
      urls: [
        "https://example.com/page-1",
        "https://example.com/page-2",
        "https://example.com/page-3",
      ],
    },
    async: false,
  }),
});

const result = await response.json();
console.log("Combined PDF URL:", result.assetUrl);
```

### Python

```python
import requests

response = requests.post(
    "https://api.cloudlayer.io/v2/url/pdf",
    headers={
        "Content-Type": "application/json",
        "x-api-key": "<YOUR-API-KEY>",
    },
    json={
        "batch": {
            "urls": [
                "https://example.com/page-1",
                "https://example.com/page-2",
                "https://example.com/page-3",
            ],
        },
        "async": False,
    },
)

result = response.json()
print("Combined PDF URL:", result["assetUrl"])
```

### Credit Usage

Batch processing uses multiple API credits:

- **1 credit per URL** in the batch for individual PDF conversion
- **1 credit** for the merge operation that combines all PDFs

For example, a batch of 3 URLs uses 4 credits total (3 conversions + 1 merge).

## Common Options Reference

| Parameter          | Type              | Default         | Description                                              |
|--------------------|-------------------|-----------------|----------------------------------------------------------|
| `url`              | `string`          | none              | URL to capture (required unless using `batch`)           |
| `batch`            | `object`          | none              | Object with `urls` array of URLs to convert and merge into one PDF |
| `autoScroll`       | `boolean`         | `false`         | Scroll page to load lazy content                         |
| `viewPort`         | `object`          | none              | Browser viewport dimensions                              |
| `waitForSelector`  | `object`          | none              | Wait for a CSS selector before capture                   |
| `waitUntil`        | `string`          | `networkidle2`  | When to consider navigation complete                     |
| `delay`            | `number`          | `0`             | Additional delay in ms after page load                   |
| `timeout`          | `number`          | `30000`         | Maximum time in ms for Chrome to run                     |
| `format`           | `string`          | none              | PDF paper format (e.g., `letter`, `a4`)                  |
| `landscape`        | `boolean`         | `false`         | Landscape orientation                                    |
| `printBackground`  | `boolean`         | `false`         | Include CSS backgrounds in PDF                           |
| `margin`           | `object`          | `0.4in`         | PDF page margins                                         |
| `scale`            | `number`          | `1`             | Page scale factor (0.1 to 2)                             |
| `timeZone`         | `string`          | none              | Override timezone (e.g., `Europe/Rome`)                  |
| `async`            | `boolean`         | `true`          | Async mode (use webhook) or sync (return result)         |
| `authentication`   | `object`          | none              | Basic auth credentials (see [Authentication](/docs/authentication-examples/)) |
| `cookies`          | `array`           | none              | Session cookies (see [Authentication](/docs/authentication-examples/))        |

## Tips

- **Use `autoScroll: true`** for image-heavy sites like portfolios, product pages, and social media feeds.
- **Combine `waitForSelector` with `waitUntil: "networkidle0"`** for the most reliable capture of JavaScript-heavy pages.
- **Set `printBackground: true`** for PDFs, background colors and images are omitted by default.
- **Use a higher `deviceScaleFactor`** (2 or 3) when generating images for high-DPI displays or print.
- **For batch processing**, use async mode with webhooks to avoid connection timeouts on large batches.
- **If a page requires authentication**, see the [Authentication](/docs/authentication-examples/) examples for Basic Auth and Cookie Auth methods.
- **Set `timeZone`** when capturing pages that display time-sensitive content to ensure consistent output across regions.


## Integrations

### MCP Server Setup

Source: https://cloudlayer.io/docs/mcp-setup/

# MCP Server Setup

Connect cloudlayer.io to your AI assistant so it can generate PDFs, images, and manage documents through natural language. The cloudlayer.io MCP server supports OAuth authentication — authorize once in your browser and your AI gets secure access to your account.

**Server URL:** `https://mcp.cloudlayer.io/mcp`

---

## Claude Code

The fastest way to get started. Claude Code has built-in OAuth support — it handles authentication automatically.

### Install

```bash
claude mcp add --transport http cloudlayer https://mcp.cloudlayer.io/mcp
```

### Authenticate

1. Run `/mcp` in Claude Code and select **cloudlayer** to authenticate
2. Your browser opens to the cloudlayer.io consent page
3. Sign in (or use your existing session) and click **Authorize**
4. You're connected — start using cloudlayer.io tools in conversation

### Verify

Ask Claude Code something like:

- "Check my cloudlayer account status"
- "Generate a PDF from this HTML: `<h1>Hello World</h1>`"
- "What templates are available for invoices?"

### Remove

```bash
claude mcp remove cloudlayer
```

---

## Claude Desktop

1. Open **Settings → Connectors**
2. Click **Add custom connector** at the bottom
3. Paste the server URL: `https://mcp.cloudlayer.io/mcp`
4. Click **Add**

Claude Desktop handles OAuth automatically — it opens your browser to authorize when you first use a cloudlayer.io tool. Sign in and click **Authorize**.

> **Note:** Remote MCP servers must be added via Settings → Connectors. The `claude_desktop_config.json` file is only for local servers.

---

## Cursor

### Option 1: Settings UI

1. Open Settings: **Cmd+,** (Mac) or **Ctrl+,** (Windows)
2. Navigate to **Features → MCP**
3. Click **+ Add New MCP Server**
4. Enter name `cloudlayer` and URL `https://mcp.cloudlayer.io/mcp`

### Option 2: Config File

Add to `.cursor/mcp.json` in your project root (or `~/.cursor/mcp.json` for global):

```json
{
  "cloudlayer": {
    "type": "http",
    "url": "https://mcp.cloudlayer.io/mcp"
  }
}
```

> **Note:** Cursor has a limit of ~40 active tools across all MCP servers. cloudlayer.io registers 20 tools, so keep this in mind if you have other MCP servers configured.

---

## Windsurf

### Option 1: Settings UI

1. Open **Windsurf → Settings → Advanced Settings** (or use Command Palette: **Open Windsurf Settings Page**)
2. Scroll to the **Cascade** section
3. Add a new MCP server

### Option 2: Config File

Edit `~/.codeium/windsurf/mcp_config.json`:

```json
{
  "mcpServers": {
    "cloudlayer": {
      "type": "http",
      "url": "https://mcp.cloudlayer.io/mcp"
    }
  }
}
```

Windsurf automatically reloads the config when you save.

---

## VS Code + GitHub Copilot

Requires VS Code 1.99+ with GitHub Copilot.

1. Open Settings: **Cmd+,** (Mac) or **Ctrl+,** (Windows)
2. Search for **MCP**
3. Click **Add MCP Server**
4. Enter name `cloudlayer` and URL `https://mcp.cloudlayer.io/mcp`

Or add to `.vscode/settings.json`:

```json
{
  "mcp": {
    "servers": {
      "cloudlayer": {
        "type": "http",
        "url": "https://mcp.cloudlayer.io/mcp"
      }
    }
  }
}
```

---

## Cline

### Option 1: Config UI

1. Click the **MCP Servers** icon in Cline's top navigation
2. Select the **Configure** tab
3. Click **Configure MCP Servers**
4. Add the cloudlayer.io server

### Option 2: Config File

Edit the Cline MCP settings file:
- **macOS:** `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
- **Windows:** `%APPDATA%/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
- **Linux:** `~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`

```json
{
  "mcpServers": {
    "cloudlayer": {
      "type": "http",
      "url": "https://mcp.cloudlayer.io/mcp"
    }
  }
}
```

---

## Available Tools

Once connected, your AI assistant gets access to these tools:

### Document Generation
- **html_to_pdf** / **html_to_image** — Convert HTML to PDF or image
- **url_to_pdf** / **url_to_image** — Capture a web page as PDF or image
- **template_to_pdf** / **template_to_image** — Render a template with data
- **merge_pdfs** — Merge multiple PDFs

### Account & Resources
- **get_account** — View usage and subscription info
- **list_jobs** / **get_job** — Track conversion jobs
- **list_assets** / **get_asset** — Browse generated documents
- **list_storage** / **get_storage** / **create_storage** / **delete_storage** — Manage S3 storage
- **list_templates** / **get_template** — Browse the template gallery
- **get_status** — Check API status

### Documentation
- **get_doc** — Look up cloudlayer.io documentation by topic (e.g., `get_doc "html-to-pdf"`)

---

## Revoking Access

Each MCP authorization creates a dedicated API key. To revoke access:

1. Go to [app.cloudlayer.io/settings/api-keys](https://beta-app.cloudlayer.io/settings/api-keys)
2. Find the key created for your MCP connection (most recent)
3. Click **Revoke**

The AI assistant will need to re-authorize next time it tries to use cloudlayer.io tools.

---

## Troubleshooting

**"Unauthorized" errors after connecting**
Your token may have expired or the API key was revoked. Remove and re-add the MCP server to trigger a fresh authorization.

**Connection timeout**
Verify the server URL is exactly `https://mcp.cloudlayer.io/mcp` — including the `/mcp` path.

**Tools not appearing**
Some clients need a restart after adding an MCP server. Try restarting your AI assistant.

**Too many tools warning (Cursor)**
Cursor limits active tools to ~40. If you have other MCP servers, you may hit this limit. Consider removing unused servers.

### Zapier Integration

Source: https://cloudlayer.io/docs/zapier/

# Zapier Integration

[Zapier](https://zapier.com) lets you connect cloudlayer.io with thousands of apps so you can automate document generation without writing any code. Create PDFs, images, invoices, certificates, and more, triggered by events in the tools you already use.

Visit the [cloudlayer.io Zapier integration page](https://zapier.com/apps/cloudlayerio/integrations) to get started.

## What You Can Do

With the cloudlayer.io Zapier integration, you can:

- **Generate PDFs automatically** when a new row is added to a spreadsheet
- **Create invoices** when a Stripe payment is received
- **Produce certificates** when a student completes a course
- **Convert web pages to PDF** on a schedule or triggered by an event
- **Send generated documents** via email, Slack, or store them in Google Drive, Dropbox, or S3

## Available Actions

The cloudlayer.io Zapier integration provides create actions, searches, and a completed-job trigger:

### Create Actions

| Action               | Description                                              |
|----------------------|----------------------------------------------------------|
| Create PDF From URL  | Convert a publicly accessible URL to a PDF document      |
| Create Image From URL | Capture a screenshot of a URL as a PNG, JPEG, or WebP   |
| Create PDF From HTML | Convert HTML content to a PDF document                   |
| Create Image From HTML | Convert HTML content to an image                       |
| Create PDF From Template | Generate a PDF from a cloudlayer.io template with data |
| Create Image From Template | Generate an image from a cloudlayer.io template      |
| Create Invoice PDF   | Generate an invoice from a predefined invoice template   |
| Merge PDFs           | Combine multiple PDF files into a single document        |

### Search Actions

| Search               | Description                                              |
|----------------------|----------------------------------------------------------|
| Find Template        | Find a template by name or type                          |
| Find Job             | Look up a conversion job by ID                           |
| Find Asset           | Look up a generated asset by ID                          |

### Triggers

| Trigger              | Description                                              |
|----------------------|----------------------------------------------------------|
| New Completed Job    | Triggers when a document conversion job completes        |

### Dynamic Template Fields

When you select a template in the Template to PDF or Template to Image actions, cloudlayer.io automatically generates input fields based on the template's data schema. Instead of writing raw JSON, you can fill in individual form fields directly in the Zapier editor.

## Common Workflows

### Google Sheets to Invoice PDF

Automatically generate a PDF invoice whenever a new row is added to a Google Sheet.

**Trigger:** New Spreadsheet Row in Google Sheets
**Action:** Template to PDF in cloudlayer.io

1. A new row is added to your "Orders" spreadsheet
2. Zapier maps the spreadsheet columns (customer name, items, amounts) to the invoice template data fields
3. cloudlayer.io generates a professional PDF invoice
4. Optionally, a follow-up step emails the PDF to the customer via Gmail or SendGrid

### Stripe Payment to Invoice

Generate and deliver an invoice PDF when a Stripe payment succeeds.

**Trigger:** New Payment in Stripe
**Action:** Template to PDF in cloudlayer.io

1. A payment is completed in Stripe
2. Zapier extracts the payment details (amount, customer info, description)
3. cloudlayer.io generates an invoice PDF using your branded template
4. The invoice is stored in Google Drive and/or emailed to the customer

### Form Submission to Certificate

Create a certificate of completion when someone submits a form.

**Trigger:** New Form Submission in Typeform / Google Forms / JotForm
**Action:** Template to PDF in cloudlayer.io

1. A student submits a course completion form
2. Zapier maps the form fields (name, course, date) to the certificate template
3. cloudlayer.io generates a personalized certificate PDF
4. The certificate is emailed to the student and saved to Dropbox

### Scheduled Website Archival

Capture a website as a PDF on a recurring schedule.

**Trigger:** Schedule by Zapier (daily, weekly, etc.)
**Action:** URL to PDF in cloudlayer.io

1. Zapier triggers on your chosen schedule
2. cloudlayer.io converts the target URL to PDF
3. The PDF is stored in Google Drive or S3 for archival

## Setup Guide

### Step 1: Create a Zapier Account

If you do not have one already, sign up at [zapier.com](https://zapier.com).

### Step 2: Get Your cloudlayer.io API Key

1. Log in to your [cloudlayer.io dashboard](https://beta-app.cloudlayer.io).
2. Navigate to **Settings** > **API Keys**.
3. Copy your API key. If you do not have one, create a new key.

### Step 3: Create a New Zap

1. In Zapier, click **Create Zap** (or **Make a Zap**).
2. **Choose your trigger app**, this is the app that starts the workflow (e.g., Google Sheets, Stripe, Typeform).
3. Configure the trigger event and connect your account.
4. Test the trigger to make sure Zapier can pull in sample data.

### Step 4: Add the cloudlayer.io Action

1. For the action step, search for **cloudlayer.io**.
2. Choose the action you want (e.g., Template to PDF, URL to PDF).
3. Connect your cloudlayer.io account by entering your API key when prompted.
4. Configure the action fields:
   - **For Template to PDF/Image:** Select or enter your `templateId`, then map data fields from your trigger to the template's data properties.
   - **For URL to PDF/Image:** Map the URL field from your trigger, or enter a static URL.
   - **For HTML to PDF/Image:** Provide the HTML content (Zapier will handle the base64 encoding).

### Step 5: Test and Activate

1. Click **Test** to run the Zap with sample data and verify the output.
2. Review the generated document to ensure it looks correct.
3. Turn on the Zap to activate it.

## Mapping Data Fields

When using Template to PDF or Template to Image actions, you need to map data from your trigger to the template's expected data fields.

**Example:** Mapping Google Sheets columns to an invoice template:

| Sheets Column     | Template Data Field          |
|-------------------|------------------------------|
| Customer Name     | `bill_to_fullname`           |
| Customer Address  | `bill_to_address1`           |
| Customer City     | `bill_to_city`               |
| Invoice Number    | `invoice_no`                 |
| Item Description  | `items[0].title`             |
| Quantity          | `items[0].quantity`          |
| Unit Price        | `items[0].unit_price`        |

> **Tip:** Use the sample data from the [template gallery](https://cloudlayer.io/templates/pdf/) to understand what data fields each template expects.

## Tips and Best Practices

- **Test with sample data first** before activating your Zap on live data.
- **Use predefined templates** from the cloudlayer.io gallery for the easiest setup, they have well-documented data schemas.
- **Add a delay step** if your trigger fires rapidly (e.g., batch spreadsheet imports) to avoid hitting rate limits.
- **Use Zapier's formatter** step to transform data (dates, numbers, text) before sending it to cloudlayer.io.
- **Add error handling** with Zapier Paths to handle cases where document generation might fail (e.g., missing required fields).
- **Store generated documents** in cloud storage (Google Drive, Dropbox, S3) as a follow-up step for easy access and archival.
- **Use filters** to only generate documents when specific conditions are met (e.g., order amount > $100).